Compare commits
771 Commits
version-20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
18778ce0d9 | |||
14973fb595 | |||
9171bd6d6f | |||
4e5eeacf0a | |||
d1d28722d2 | |||
a6e528d209 | |||
2c70301f56 | |||
07b9923bf6 | |||
8b3923200d | |||
3dcd67c1a3 | |||
2a9feafb90 | |||
580e88c6fc | |||
d82c01aa61 | |||
1af3357826 | |||
ed49d7824e | |||
378402fcf0 | |||
50f0c11c0b | |||
58712828a4 | |||
b2b9093c95 | |||
afa2afe1d4 | |||
5f58a4566c | |||
d616bdd5d6 | |||
5112ef9331 | |||
7a49377caf | |||
5b3941a425 | |||
c1ab5c5556 | |||
3282b34431 | |||
392d9bb10b | |||
82f6c515ea | |||
d67d5f73c5 | |||
799d186510 | |||
3983b7fbe4 | |||
d75284a587 | |||
71e4936dc3 | |||
9d3b6f7a4d | |||
003df44a34 | |||
a7598c6ee5 | |||
0891e43040 | |||
1f49aea48d | |||
499b52df6a | |||
b8a566f4a0 | |||
aa0e8edb8b | |||
0e35bb18c7 | |||
4a06ebf4f9 | |||
11584af425 | |||
a31da9e1d3 | |||
8d6d49834b | |||
2825710262 | |||
7346ccf2b7 | |||
57072dd6ce | |||
fec098a823 | |||
73950b72e5 | |||
b40afb9b7d | |||
1f783dfc01 | |||
7ccf8bcdc8 | |||
76131e40ec | |||
5955394c1d | |||
a8998a6356 | |||
dc75d7b7f0 | |||
34a191f216 | |||
299931985e | |||
b946fbf9e7 | |||
e20bb7d636 | |||
5db3409efc | |||
649db054a6 | |||
15d5b91642 | |||
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 | |||
2397cb162a | |||
80bcd09cec | |||
1e10f37370 | |||
bf253643a6 | |||
ab4569e5d6 | |||
8df29235bb | |||
cb048764f4 | |||
5627848fad | |||
fb53dc826a | |||
335c5a0b80 | |||
d76db3caba | |||
32d88c3a49 | |||
5522c94b65 | |||
19e73630ab | |||
97364ad102 | |||
55fd7cd151 | |||
c9cc1629d6 | |||
f4ec678587 | |||
115274e691 | |||
96d3d536be | |||
f156c0f05d | |||
5d64b0cafd | |||
182256c53e | |||
c44aa2a204 | |||
c133f16371 | |||
ca2a4ffb59 | |||
75bc7c1cbd | |||
7c761ff3d9 | |||
f6b8dc5cea | |||
6f7fb4c919 | |||
1fbf6be6c2 | |||
f3aea29324 | |||
f5921f8480 | |||
c82cd4fbcf | |||
83bb3f8b0b | |||
c887139367 | |||
34b8a97ae9 | |||
5dd29d45d8 | |||
43ad4f58ac | |||
23f269d676 | |||
e7346317bb | |||
98318953cd | |||
5a5a32ff83 | |||
232a5a8ad0 | |||
6049d91f7c | |||
118f55d95c | |||
1494394a78 | |||
963af1ac1e | |||
e7b7186f4b | |||
33fb06a299 | |||
66e0c545ac | |||
1fbc7ed5fa | |||
9c081ae417 | |||
17faffd78e | |||
16885b064e | |||
65bee361a2 | |||
aff192dbbe | |||
d37c33d941 | |||
7b0005ac42 | |||
aefeb5bacf | |||
7d0e7bcf75 | |||
dbc75428a0 | |||
e33a1ea0c7 | |||
ca35204e0c | |||
4a74d16388 | |||
3c47555276 | |||
c5abecf578 | |||
8793bb1358 | |||
37632bd0c7 | |||
fb09c8f863 | |||
f14d0aade4 | |||
29eda41eed | |||
5eaead60b6 | |||
4054e6da8c | |||
12b1f53948 | |||
35232afa7e | |||
17de0ff24e | |||
c5b56fd4e6 | |||
8f20376804 | |||
a2a35e49a9 | |||
fb409a73a1 | |||
a13d89fcde | |||
a31fc8319d | |||
b09943e106 | |||
d5169504ea | |||
e678e3553b | |||
4b2119510c | |||
e903582f96 | |||
20de845f2b | |||
5fc052a384 | |||
7b523d8be2 | |||
af15e32d30 | |||
b6900e498c | |||
dfc1cc08bb | |||
80e426a4b8 | |||
2196468804 | |||
5ccbc17e65 | |||
b98b4f2ae7 | |||
dcc873b88b | |||
d48badbca3 | |||
f0ef2eea4f | |||
61652406c7 | |||
11859c8cea | |||
a6608c140e | |||
3da23829d3 | |||
ab8c954e00 | |||
c89ec88751 | |||
c0dbb738bd | |||
d0230c0b54 | |||
a9336d0983 | |||
2c4239d79a | |||
1a0a62975c | |||
e06d729fe5 | |||
a66b832154 | |||
b2189374e2 | |||
ff40ab0c49 | |||
002c048d0b | |||
52029f55e4 | |||
85121de9d7 | |||
93b362570d | |||
597bd472ea | |||
e2f01ce740 | |||
d4982b276c | |||
c1d93bfd7c | |||
469b6b64bc | |||
c0bdb2407a | |||
596431cae7 | |||
6b085a58be | |||
bd514dcce6 | |||
d83756b4d9 | |||
16d989dbfa | |||
9517c890b5 | |||
8cae1f2ab5 | |||
90e7856efb | |||
37a14858ad | |||
5b5d7e4997 | |||
67fef02d71 | |||
b8c41f54c5 | |||
97ea859315 | |||
616b1f4a05 | |||
d1cde64214 | |||
d061868fdc | |||
a2cfe9c2a7 | |||
8a7c414031 | |||
46e0571ed0 | |||
1835981f3d | |||
87fdb591ce | |||
195951a61a | |||
1f781eb78a | |||
1b63e461cc | |||
e8dc6b259f | |||
a7f751f3b3 | |||
ed18e623db | |||
b37470b3de | |||
e246071aac | |||
4554c468bc | |||
5923edc69a | |||
55c24de8c7 | |||
25300c1928 | |||
fc1caf1469 | |||
44d33ed96e | |||
650b084c72 | |||
82c2a202cb | |||
aaa1f92945 | |||
66d7d598fb | |||
8d2aecd687 | |||
6eff2fe0d1 | |||
eeb9449c11 | |||
94a5a6c4c0 | |||
a291063b9c | |||
c17eb00e3b | |||
43f37e4776 | |||
42cb55d78a | |||
aaebd01058 | |||
d7698343ae | |||
0b057ccb34 | |||
995f3a13d1 | |||
ab7f4c5ba2 | |||
be4288fb46 | |||
75d8641a38 | |||
1d72019645 | |||
c1c47c5f30 | |||
fc47af12be | |||
a9bee998f2 | |||
31226e3c75 | |||
f7aabe8ca9 | |||
8ac82b97d3 | |||
128af67011 | |||
fb9a4ec461 | |||
2a261cfaf8 | |||
224ad46a21 | |||
05cc8e2b51 | |||
ffe3ec0cb4 | |||
448dd7ed54 | |||
1dc01ef857 | |||
0f76e80341 | |||
6acfbb7d66 | |||
fcdc064cac | |||
0c92f4a74d | |||
ac136ec5f6 | |||
f75f6a8404 | |||
415bb4cc88 | |||
6a3e1da986 | |||
5a6b6c369e | |||
66d342880c | |||
7fad2b6563 | |||
22f50aae45 | |||
1daba5db87 | |||
83fc22005c | |||
7eb7fc2e12 | |||
07702afe68 | |||
0aa21c007a | |||
c659e40df7 | |||
ffacd4d021 | |||
54ad6b8dd9 | |||
70fc4c0d88 | |||
742f570c4c | |||
75d67e0e05 | |||
7bd7ae41b4 | |||
5f9a9b80f0 | |||
94208477e9 | |||
4da0803f15 | |||
72201c296b | |||
ed2e9b88e7 | |||
dd88d9254e | |||
509f21a9b4 | |||
b299451cab | |||
7e63a18d37 | |||
b9e718f5b8 | |||
b4a6f8350b | |||
5eb9b95ab5 | |||
4e3701ca8d | |||
7a0ebbdc53 | |||
051c5672b9 | |||
57f242ccf8 | |||
0c2903f33f | |||
d7cbebcb02 | |||
d3f2f987e0 | |||
221e6190c8 | |||
6a69425688 | |||
656fe00302 | |||
884c91062d | |||
a7d9857a69 | |||
f814f7792c | |||
e264e10ad6 | |||
f2d5d62c9c | |||
af438af8ac | |||
041b51a7f8 | |||
330d5047e7 | |||
e476186cbc | |||
3124b0f39c | |||
55f68a9197 | |||
c92a2ecbf5 | |||
d248b30eb3 | |||
c71009fea9 | |||
b15aca80ca | |||
25e043afea | |||
0395c84270 | |||
e66c46ff59 | |||
46f4493f04 | |||
da5de30d7b | |||
5cbcd89369 | |||
32f5cc7fba | |||
c6005ea389 | |||
60b6a7cdfc | |||
f5bc5fa24a | |||
f9382ed32e | |||
c0cfd75a2e | |||
64fa04306c | |||
7a583cb7e6 | |||
cb0b5f7146 | |||
8a3b1ae29d | |||
717282b4b7 | |||
78a4a167ac | |||
23d7ef36d2 | |||
d1dd6b7a8f | |||
9c65fd814b | |||
58a7d67922 | |||
b1fb2982ef | |||
f206baf3f0 | |||
6916c59483 | |||
41914d9b7a | |||
1f89b94f66 | |||
80b0aef210 | |||
b1214f6c35 | |||
c7dcf92a2e | |||
50ce5aa2b4 | |||
b3b8e71caa | |||
3686cba6b4 | |||
b1967b42e3 | |||
bfa0c46588 | |||
69ee18e13d | |||
c180a521ec | |||
59f5846d1a | |||
7e85524e51 | |||
59e1811187 | |||
120332924b | |||
01ae3334ee | |||
03cf8799c4 | |||
54c50f6446 | |||
09aa5d6350 | |||
e5ff416c2d | |||
21ea527623 | |||
36c34e05f8 | |||
7a93b9e565 | |||
3945dc9f3f | |||
e96d2fa666 | |||
3a2f285a87 | |||
a09481dea2 | |||
03ff495011 | |||
657b0089b1 | |||
7d74e1d2c4 | |||
81ac53ff0a | |||
6c999d10c3 | |||
1e58941323 | |||
a52b57cc38 | |||
bffa51f7df | |||
d5281d2023 | |||
5b8e3b4189 | |||
372cf4a8cb | |||
fc17580d9a | |||
dfff2a1134 | |||
b3d54b7620 | |||
a445b03523 | |||
5d37012075 | |||
a9db538c63 | |||
526af26536 | |||
fac8d53163 | |||
0804b5e6c5 | |||
464a56ad52 | |||
0793fff222 | |||
4fa122b827 | |||
583b6cc20b | |||
ed17920bd4 | |||
3cc7d54cc1 | |||
d71d45b958 | |||
e7c6ff9499 | |||
1b496dd472 | |||
c1781d89df | |||
12bfa404c8 | |||
76e571ea0a | |||
48ee582f37 | |||
9d0398f81d | |||
d2d0e99f9d | |||
e165b3dae5 | |||
6abd8a0ca0 | |||
78acfc18fc | |||
aced8b507c | |||
fbc33815a3 | |||
768d72ec24 | |||
bd9c0efab7 | |||
d358dc1182 | |||
956d868106 | |||
0fcef494a6 | |||
6f6fe6ad06 | |||
926636c331 | |||
2e6a264f98 | |||
95ecad8382 | |||
035771de81 | |||
1a53bc3de5 | |||
e621eb7455 | |||
261583cb92 | |||
1bc48d2bea | |||
9bab708e6e | |||
103e0f3b06 | |||
c8608db4ee | |||
869f18483f | |||
32fb90e056 | |||
f636414fb7 | |||
a4fd0dc597 | |||
2a437536d4 | |||
a39f42974f | |||
2e58982419 | |||
72cca0473a | |||
02212406c4 | |||
2fade4e604 | |||
469ba3a391 | |||
0b3980e564 | |||
cfcf7aa2ae | |||
fc6f242f86 | |||
ec8dee3588 | |||
e7fd37efeb | |||
ccd4665d82 | |||
fe4791c216 | |||
6e46124c94 | |||
1275f22599 | |||
533a719914 | |||
a085632b8e | |||
1ef5a8e6c5 | |||
ab5d6dbea1 | |||
ffd8c59c8e | |||
83c3a116f3 | |||
f695a3f40a | |||
f41f2bfdab | |||
17f7a97ef3 | |||
3698c6431c | |||
4d88af4601 | |||
dce869b566 | |||
1d641b2432 | |||
5a5539da97 | |||
e12d99ba63 | |||
4612cea970 | |||
da4fa96499 | |||
4137266041 | |||
9427942ea8 | |||
5b8b973345 | |||
d44dc00757 | |||
37655e1e21 | |||
a1f961db97 | |||
62d0e020db | |||
fa5f379a53 | |||
3f6174e8cc | |||
1fd949d4ec | |||
de6fa63d21 | |||
cfe7bc8155 | |||
c6c4636b9b | |||
bd74e07ce1 | |||
45c1072291 | |||
33787d0685 | |||
068d281b19 | |||
56344cadeb | |||
3c2d541d60 | |||
0671d712fa | |||
6961089425 | |||
b6d797fc78 | |||
3e5a756016 | |||
d24cbae39a | |||
480113e080 | |||
3167426b53 | |||
863124efbb | |||
80cc0fcc61 | |||
ddf09a4cf5 | |||
012a045c8e | |||
145ef8b071 | |||
3157bf63a6 | |||
e202fd988b | |||
8155d88db7 | |||
6ce3d2916b | |||
450bb9040d | |||
4f8b882554 | |||
8a451bb5f6 | |||
fe7f23238c | |||
936e2fb4e2 | |||
bb743a4d30 | |||
3238c85514 | |||
e2c0fa8d8a | |||
50f946e4a7 | |||
556a0d5d84 | |||
25c82d80f5 | |||
7e47906475 | |||
24ac6d2c25 | |||
68449a0d21 | |||
bb9fbb55b6 | |||
c834f0a372 | |||
1414322f71 | |||
17f46c291b | |||
18594c4886 | |||
d906738097 | |||
43f19f78bb | |||
3eacd8b754 | |||
3d45956f15 | |||
fb20ae7e1a | |||
5c85c3315d | |||
d0529e76ba | |||
4c49209f71 | |||
3668850e8f | |||
4525a43e63 | |||
077abdb602 | |||
b6087c0f10 | |||
972972a4d9 | |||
45a397bd77 | |||
f54cc79f6b | |||
2cad208038 | |||
f1a4754568 | |||
d8841911de | |||
fe054136b1 | |||
e7a8371cbb | |||
d82dfc65b7 | |||
2de869d9c3 | |||
080282a0bc | |||
8242c139c2 | |||
5b4c5d0f31 | |||
9ad10863de | |||
14f2522c3e | |||
01fc63fc98 | |||
a57d524273 | |||
93bd95436f | |||
db9aa5d9dc | |||
48443e3e09 | |||
dae60b5a08 | |||
013a192485 | |||
bc37480f0d | |||
a95b6e0e61 | |||
ac78e3e2ec | |||
77a484e698 | |||
f1f706dd0d | |||
a6123cfbe4 | |||
07142cab8b | |||
9a27bc8627 | |||
e6cb60b793 | |||
706ffb56f7 | |||
8cadee28c1 | |||
ef58020fd4 | |||
a54fa7c9b1 | |||
a8d411a77b | |||
5f6f5dbfc4 | |||
aeb4b6b412 | |||
9efc4dec18 | |||
7b826b696c | |||
b1c21c405a | |||
cd1218c78e | |||
a8c1fd1e4e | |||
14d990df7f | |||
93e8f9cb36 | |||
04d2e769bb | |||
5b0d875a42 | |||
820f4be02f | |||
0ef040e5b6 | |||
d2bbf2965d | |||
bf32cf3265 | |||
5f0192ee48 | |||
91e1ded3bf | |||
c70f6e3122 | |||
56260cd23f | |||
fdbb9803b5 | |||
83abc20300 | |||
88cf0b2cdc | |||
16950dbc54 | |||
43bf9e6c21 | |||
2698d9d23a | |||
6eb0583eeb | |||
49f140e9bc | |||
9ddc10431a | |||
cad1c9eae6 | |||
a6708594bb | |||
14027e2fc6 | |||
cf519f48e7 | |||
eb884f7ef7 | |||
9902a11621 | |||
abbec501f7 | |||
67629ce0b7 | |||
5f024eb1f7 | |||
db99225c65 | |||
6717f2a68d | |||
56a7e1e2f0 | |||
e434b0233a |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2021.3.4
|
current_version = 2021.4.6
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||||
@ -37,6 +37,8 @@ values =
|
|||||||
|
|
||||||
[bumpversion:file:web/src/constants.ts]
|
[bumpversion:file:web/src/constants.ts]
|
||||||
|
|
||||||
|
[bumpversion:file:web/nginx.conf]
|
||||||
|
|
||||||
[bumpversion:file:website/docs/outposts/manual-deploy-docker-compose.md]
|
[bumpversion:file:website/docs/outposts/manual-deploy-docker-compose.md]
|
||||||
|
|
||||||
[bumpversion:file:website/docs/outposts/manual-deploy-kubernetes.md]
|
[bumpversion:file:website/docs/outposts/manual-deploy-kubernetes.md]
|
||||||
|
3
.github/codecov.yml
vendored
Normal file
3
.github/codecov.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
coverage:
|
||||||
|
precision: 2
|
||||||
|
round: up
|
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
|||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
run: docker build
|
run: docker build
|
||||||
--no-cache
|
--no-cache
|
||||||
-t beryju/authentik:2021.3.4
|
-t beryju/authentik:2021.4.6
|
||||||
-t beryju/authentik:latest
|
-t beryju/authentik:latest
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/authentik:2021.3.4
|
run: docker push beryju/authentik:2021.4.6
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/authentik:latest
|
run: docker push beryju/authentik:latest
|
||||||
build-proxy:
|
build-proxy:
|
||||||
@ -48,11 +48,11 @@ jobs:
|
|||||||
cd outpost/
|
cd outpost/
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t beryju/authentik-proxy:2021.3.4 \
|
-t beryju/authentik-proxy:2021.4.6 \
|
||||||
-t beryju/authentik-proxy:latest \
|
-t beryju/authentik-proxy:latest \
|
||||||
-f proxy.Dockerfile .
|
-f proxy.Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/authentik-proxy:2021.3.4
|
run: docker push beryju/authentik-proxy:2021.4.6
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/authentik-proxy:latest
|
run: docker push beryju/authentik-proxy:latest
|
||||||
build-static:
|
build-static:
|
||||||
@ -61,7 +61,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: prepare ts api client
|
- name: prepare ts api client
|
||||||
run: |
|
run: |
|
||||||
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/src/api --additional-properties=typescriptThreePlus=true
|
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
||||||
- name: Docker Login Registry
|
- name: Docker Login Registry
|
||||||
env:
|
env:
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
@ -72,11 +72,11 @@ jobs:
|
|||||||
cd web/
|
cd web/
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t beryju/authentik-static:2021.3.4 \
|
-t beryju/authentik-static:2021.4.6 \
|
||||||
-t beryju/authentik-static:latest \
|
-t beryju/authentik-static:latest \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/authentik-static:2021.3.4
|
run: docker push beryju/authentik-static:2021.4.6
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/authentik-static:latest
|
run: docker push beryju/authentik-static:latest
|
||||||
test-release:
|
test-release:
|
||||||
@ -110,5 +110,5 @@ jobs:
|
|||||||
SENTRY_PROJECT: authentik
|
SENTRY_PROJECT: authentik
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
tagName: 2021.3.4
|
tagName: 2021.4.6
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
strictness: medium
|
|
||||||
test-warnings: true
|
|
||||||
doc-warnings: false
|
|
||||||
|
|
||||||
ignore-paths:
|
|
||||||
- migrations
|
|
||||||
- docs
|
|
||||||
- node_modules
|
|
||||||
|
|
||||||
uses:
|
|
||||||
- django
|
|
||||||
- celery
|
|
29
.pylintrc
29
.pylintrc
@ -1,29 +0,0 @@
|
|||||||
[MASTER]
|
|
||||||
|
|
||||||
disable =
|
|
||||||
arguments-differ,
|
|
||||||
no-self-use,
|
|
||||||
fixme,
|
|
||||||
locally-disabled,
|
|
||||||
too-many-ancestors,
|
|
||||||
too-few-public-methods,
|
|
||||||
import-outside-toplevel,
|
|
||||||
bad-continuation,
|
|
||||||
signature-differs,
|
|
||||||
similarities,
|
|
||||||
cyclic-import,
|
|
||||||
protected-access,
|
|
||||||
unsubscriptable-object # remove when pylint is upgraded to 2.6
|
|
||||||
|
|
||||||
load-plugins=pylint_django,pylint.extensions.bad_builtin
|
|
||||||
|
|
||||||
extension-pkg-whitelist=lxml,xmlsec
|
|
||||||
|
|
||||||
# Allow constants to be shorter than normal (and lowercase, for settings.py)
|
|
||||||
const-rgx=[a-zA-Z0-9_]{1,40}$
|
|
||||||
|
|
||||||
ignored-modules=django-otp
|
|
||||||
generated-members=xmlsec.constants.*,xmlsec.tree.*,xmlsec.template.*
|
|
||||||
ignore=migrations
|
|
||||||
max-attributes=12
|
|
||||||
max-branches=20
|
|
@ -40,7 +40,7 @@ RUN apt-get update && \
|
|||||||
chown authentik:authentik /backups
|
chown authentik:authentik /backups
|
||||||
|
|
||||||
COPY ./authentik/ /authentik
|
COPY ./authentik/ /authentik
|
||||||
COPY ./pytest.ini /
|
COPY ./pyproject.toml /
|
||||||
COPY ./xml /xml
|
COPY ./xml /xml
|
||||||
COPY ./manage.py /
|
COPY ./manage.py /
|
||||||
COPY ./lifecycle/ /lifecycle
|
COPY ./lifecycle/ /lifecycle
|
||||||
|
5
Makefile
5
Makefile
@ -6,7 +6,7 @@ test-integration:
|
|||||||
coverage run manage.py test -v 3 tests/integration
|
coverage run manage.py test -v 3 tests/integration
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
coverage run manage.py test -v 3 tests/e2e
|
coverage run manage.py test --failfast -v 3 tests/e2e
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
coverage run manage.py test -v 3 authentik
|
coverage run manage.py test -v 3 authentik
|
||||||
@ -14,14 +14,13 @@ coverage:
|
|||||||
coverage report
|
coverage report
|
||||||
|
|
||||||
lint-fix:
|
lint-fix:
|
||||||
isort -rc authentik tests lifecycle
|
isort authentik tests lifecycle
|
||||||
black authentik tests lifecycle
|
black authentik tests lifecycle
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
pyright authentik tests lifecycle
|
pyright authentik tests lifecycle
|
||||||
bandit -r authentik tests lifecycle -x node_modules
|
bandit -r authentik tests lifecycle -x node_modules
|
||||||
pylint authentik tests lifecycle
|
pylint authentik tests lifecycle
|
||||||
prospector
|
|
||||||
|
|
||||||
gen: coverage
|
gen: coverage
|
||||||
./manage.py generate_swagger -o swagger.yaml -f yaml
|
./manage.py generate_swagger -o swagger.yaml -f yaml
|
||||||
|
15
Pipfile
15
Pipfile
@ -11,7 +11,6 @@ channels-redis = "*"
|
|||||||
dacite = "*"
|
dacite = "*"
|
||||||
defusedxml = "*"
|
defusedxml = "*"
|
||||||
django = "*"
|
django = "*"
|
||||||
django-cors-middleware = "*"
|
|
||||||
django-dbbackup = "*"
|
django-dbbackup = "*"
|
||||||
django-filter = "*"
|
django-filter = "*"
|
||||||
django-guardian = "*"
|
django-guardian = "*"
|
||||||
@ -23,13 +22,13 @@ django-storages = "*"
|
|||||||
djangorestframework = "*"
|
djangorestframework = "*"
|
||||||
djangorestframework-guardian = "*"
|
djangorestframework-guardian = "*"
|
||||||
docker = "*"
|
docker = "*"
|
||||||
drf_yasg2 = "*"
|
drf_yasg = "*"
|
||||||
facebook-sdk = "*"
|
facebook-sdk = "*"
|
||||||
geoip2 = "*"
|
geoip2 = "*"
|
||||||
gunicorn = "*"
|
gunicorn = "*"
|
||||||
kubernetes = "*"
|
kubernetes = "*"
|
||||||
ldap3 = "*"
|
ldap3 = "*"
|
||||||
lxml = "*"
|
lxml = ">=4.6.3"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
psycopg2-binary = "*"
|
psycopg2-binary = "*"
|
||||||
pycryptodome = "*"
|
pycryptodome = "*"
|
||||||
@ -40,25 +39,23 @@ sentry-sdk = "*"
|
|||||||
service_identity = "*"
|
service_identity = "*"
|
||||||
structlog = "*"
|
structlog = "*"
|
||||||
swagger-spec-validator = "*"
|
swagger-spec-validator = "*"
|
||||||
|
twisted = "==20.3.0"
|
||||||
urllib3 = {extras = ["secure"],version = "*"}
|
urllib3 = {extras = ["secure"],version = "*"}
|
||||||
uvicorn = {extras = ["standard"],version = "*"}
|
uvicorn = {extras = ["standard"],version = "*"}
|
||||||
webauthn = "*"
|
webauthn = "*"
|
||||||
xmlsec = "*"
|
xmlsec = "*"
|
||||||
twisted = "==20.3.0"
|
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.9"
|
python_version = "3.9"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
autopep8 = "*"
|
|
||||||
bandit = "*"
|
bandit = "*"
|
||||||
black = "==20.8b1"
|
black = "==20.8b1"
|
||||||
bumpversion = "*"
|
bump2version = "*"
|
||||||
colorama = "*"
|
colorama = "*"
|
||||||
coverage = "*"
|
coverage = "*"
|
||||||
pylint = "<=2.6.0"
|
pylint = "*"
|
||||||
pylint-django = "*"
|
pylint-django = "*"
|
||||||
selenium = "*"
|
|
||||||
prospector = "*"
|
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
pytest-django = "*"
|
pytest-django = "*"
|
||||||
|
selenium = "*"
|
||||||
|
768
Pipfile.lock
generated
768
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
16
README.md
16
README.md
@ -4,13 +4,13 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[](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=6)
|
||||||
[](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=1)
|
[](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6)
|
||||||
[](https://codecov.io/gh/BeryJu/authentik)
|
[](https://codecov.io/gh/goauthentik/authentik)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## What is authentik?
|
## What is authentik?
|
||||||
|
|
||||||
@ -26,12 +26,12 @@ For bigger setups, there is a Helm Chart in the `helm/` directory. This is docum
|
|||||||
|
|
||||||
Light | Dark
|
Light | Dark
|
||||||
--- | ---
|
--- | ---
|
||||||
 | 
|
 | 
|
||||||
 | 
|
 | 
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See [Development Documentation](https://goauthentik.io/docs/development/local-dev-environment)
|
See [Development Documentation](https://goauthentik.io/developer-docs/)
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
@ -4,9 +4,8 @@
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ---------- | ------------------ |
|
| ---------- | ------------------ |
|
||||||
| 2021.1.x | :white_check_mark: |
|
|
||||||
| 2021.2.x | :white_check_mark: |
|
|
||||||
| 2021.3.x | :white_check_mark: |
|
| 2021.3.x | :white_check_mark: |
|
||||||
|
| 2021.4.x | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
"""authentik"""
|
"""authentik"""
|
||||||
__version__ = "2021.3.4"
|
__version__ = "2021.4.6"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
31
authentik/admin/api/meta.py
Normal file
31
authentik/admin/api/meta.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""Meta API"""
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from rest_framework.fields import CharField
|
||||||
|
from rest_framework.permissions import IsAdminUser
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
from authentik.lib.utils.reflection import get_apps
|
||||||
|
|
||||||
|
|
||||||
|
class AppSerializer(PassiveSerializer):
|
||||||
|
"""Serialize Application info"""
|
||||||
|
|
||||||
|
name = CharField()
|
||||||
|
label = CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class AppsViewSet(ViewSet):
|
||||||
|
"""Read-only view set list all installed apps"""
|
||||||
|
|
||||||
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
|
@swagger_auto_schema(responses={200: AppSerializer(many=True)})
|
||||||
|
def list(self, request: Request) -> Response:
|
||||||
|
"""List current messages and pass into Serializer"""
|
||||||
|
data = []
|
||||||
|
for app in sorted(get_apps(), key=lambda app: app.name):
|
||||||
|
data.append({"name": app.name, "label": app.verbose_name})
|
||||||
|
return Response(AppSerializer(data, many=True).data)
|
@ -3,18 +3,18 @@ import time
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.db.models import Count, ExpressionWrapper, F, Model
|
from django.db.models import Count, ExpressionWrapper, F
|
||||||
from django.db.models.fields import DurationField
|
from django.db.models.fields import DurationField
|
||||||
from django.db.models.functions import ExtractHour
|
from django.db.models.functions import ExtractHour
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method
|
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
|
||||||
from rest_framework.fields import IntegerField, SerializerMethodField
|
from rest_framework.fields import IntegerField, SerializerMethodField
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Serializer
|
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
|
||||||
@ -45,20 +45,14 @@ def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
class CoordinateSerializer(Serializer):
|
class CoordinateSerializer(PassiveSerializer):
|
||||||
"""Coordinates for diagrams"""
|
"""Coordinates for diagrams"""
|
||||||
|
|
||||||
x_cord = IntegerField(read_only=True)
|
x_cord = IntegerField(read_only=True)
|
||||||
y_cord = IntegerField(read_only=True)
|
y_cord = IntegerField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class LoginMetricsSerializer(PassiveSerializer):
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class LoginMetricsSerializer(Serializer):
|
|
||||||
"""Login Metrics per 1h"""
|
"""Login Metrics per 1h"""
|
||||||
|
|
||||||
logins_per_1h = SerializerMethodField()
|
logins_per_1h = SerializerMethodField()
|
||||||
@ -74,12 +68,6 @@ class LoginMetricsSerializer(Serializer):
|
|||||||
"""Get failed logins per hour for the last 24 hours"""
|
"""Get failed logins per hour for the last 24 hours"""
|
||||||
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
|
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class AdministrationMetricsViewSet(ViewSet):
|
class AdministrationMetricsViewSet(ViewSet):
|
||||||
"""Login Metrics per 1h"""
|
"""Login Metrics per 1h"""
|
||||||
|
@ -2,22 +2,21 @@
|
|||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db.models import Model
|
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
|
from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Serializer
|
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
|
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
|
||||||
|
|
||||||
|
|
||||||
class TaskSerializer(Serializer):
|
class TaskSerializer(PassiveSerializer):
|
||||||
"""Serialize TaskInfo and TaskResult"""
|
"""Serialize TaskInfo and TaskResult"""
|
||||||
|
|
||||||
task_name = CharField()
|
task_name = CharField()
|
||||||
@ -30,23 +29,36 @@ class TaskSerializer(Serializer):
|
|||||||
)
|
)
|
||||||
messages = ListField(source="result.messages")
|
messages = ListField(source="result.messages")
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class TaskViewSet(ViewSet):
|
class TaskViewSet(ViewSet):
|
||||||
"""Read-only view set that returns all background tasks"""
|
"""Read-only view set that returns all background tasks"""
|
||||||
|
|
||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
responses={200: TaskSerializer(many=False), 404: "Task not found"}
|
||||||
|
)
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
def retrieve(self, request: Request, pk=None) -> Response:
|
||||||
|
"""Get a single system task"""
|
||||||
|
task = TaskInfo.by_name(pk)
|
||||||
|
if not task:
|
||||||
|
raise Http404
|
||||||
|
return Response(TaskSerializer(task, many=False).data)
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: TaskSerializer(many=True)})
|
@swagger_auto_schema(responses={200: TaskSerializer(many=True)})
|
||||||
def list(self, request: Request) -> Response:
|
def list(self, request: Request) -> Response:
|
||||||
"""List current messages and pass into Serializer"""
|
"""List system tasks"""
|
||||||
return Response(TaskSerializer(TaskInfo.all().values(), many=True).data)
|
tasks = sorted(TaskInfo.all().values(), key=lambda task: task.task_name)
|
||||||
|
return Response(TaskSerializer(tasks, many=True).data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
responses={
|
||||||
|
204: "Task retried successfully",
|
||||||
|
404: "Task not found",
|
||||||
|
500: "Failed to retry task",
|
||||||
|
}
|
||||||
|
)
|
||||||
@action(detail=True, methods=["post"])
|
@action(detail=True, methods=["post"])
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
def retry(self, request: Request, pk=None) -> Response:
|
def retry(self, request: Request, pk=None) -> Response:
|
||||||
@ -65,12 +77,8 @@ class TaskViewSet(ViewSet):
|
|||||||
% {"name": task.task_name}
|
% {"name": task.task_name}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return Response(
|
return Response(status=204)
|
||||||
{
|
|
||||||
"successful": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
# if we get an import error, the module path has probably changed
|
# if we get an import error, the module path has probably changed
|
||||||
task.delete()
|
task.delete()
|
||||||
return Response({"successful": False})
|
return Response(status=500)
|
||||||
|
@ -2,22 +2,21 @@
|
|||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Model
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
|
||||||
from packaging.version import parse
|
from packaging.version import parse
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
from rest_framework.mixins import ListModelMixin
|
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.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Serializer
|
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
|
||||||
|
|
||||||
class VersionSerializer(Serializer):
|
class VersionSerializer(PassiveSerializer):
|
||||||
"""Get running and latest version."""
|
"""Get running and latest version."""
|
||||||
|
|
||||||
version_current = SerializerMethodField()
|
version_current = SerializerMethodField()
|
||||||
@ -47,17 +46,13 @@ class VersionSerializer(Serializer):
|
|||||||
self.get_version_latest(instance)
|
self.get_version_latest(instance)
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class VersionViewSet(ListModelMixin, GenericViewSet):
|
class VersionViewSet(ListModelMixin, GenericViewSet):
|
||||||
"""Get running and latest version."""
|
"""Get running and latest version."""
|
||||||
|
|
||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAuthenticated]
|
||||||
|
pagination_class = None
|
||||||
|
filter_backends = []
|
||||||
|
|
||||||
def get_queryset(self): # pragma: no cover
|
def get_queryset(self): # pragma: no cover
|
||||||
return None
|
return None
|
||||||
|
@ -7,5 +7,4 @@ class AuthentikAdminConfig(AppConfig):
|
|||||||
|
|
||||||
name = "authentik.admin"
|
name = "authentik.admin"
|
||||||
label = "authentik_admin"
|
label = "authentik_admin"
|
||||||
mountpoint = "administration/"
|
|
||||||
verbose_name = "authentik Admin"
|
verbose_name = "authentik Admin"
|
||||||
|
@ -1,107 +0,0 @@
|
|||||||
"""Additional fields"""
|
|
||||||
import yaml
|
|
||||||
from django import forms
|
|
||||||
from django.utils.datastructures import MultiValueDict
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class ArrayFieldSelectMultiple(forms.SelectMultiple):
|
|
||||||
"""This is a Form Widget for use with a Postgres ArrayField. It implements
|
|
||||||
a multi-select interface that can be given a set of `choices`.
|
|
||||||
You can provide a `delimiter` keyword argument to specify the delimeter used.
|
|
||||||
|
|
||||||
https://gist.github.com/stephane/00e73c0002de52b1c601"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
# Accept a `delimiter` argument, and grab it (defaulting to a comma)
|
|
||||||
self.delimiter = kwargs.pop("delimiter", ",")
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def value_from_datadict(self, data, files, name):
|
|
||||||
if isinstance(data, MultiValueDict):
|
|
||||||
# Normally, we'd want a list here, which is what we get from the
|
|
||||||
# SelectMultiple superclass, but the SimpleArrayField expects to
|
|
||||||
# get a delimited string, so we're doing a little extra work.
|
|
||||||
return self.delimiter.join(data.getlist(name))
|
|
||||||
|
|
||||||
return data.get(name)
|
|
||||||
|
|
||||||
def get_context(self, name, value, attrs):
|
|
||||||
return super().get_context(name, value.split(self.delimiter), attrs)
|
|
||||||
|
|
||||||
|
|
||||||
class CodeMirrorWidget(forms.Textarea):
|
|
||||||
"""Custom Textarea-based Widget that triggers a CodeMirror editor"""
|
|
||||||
|
|
||||||
# CodeMirror mode to enable
|
|
||||||
mode: str
|
|
||||||
|
|
||||||
template_name = "fields/codemirror.html"
|
|
||||||
|
|
||||||
def __init__(self, *args, mode="yaml", **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.mode = mode
|
|
||||||
|
|
||||||
def render(self, *args, **kwargs):
|
|
||||||
attrs = kwargs.setdefault("attrs", {})
|
|
||||||
attrs["mode"] = self.mode
|
|
||||||
return super().render(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidYAMLInput(str):
|
|
||||||
"""Invalid YAML String type"""
|
|
||||||
|
|
||||||
|
|
||||||
class YAMLString(str):
|
|
||||||
"""YAML String type"""
|
|
||||||
|
|
||||||
|
|
||||||
class YAMLField(forms.JSONField):
|
|
||||||
"""Django's JSON Field converted to YAML"""
|
|
||||||
|
|
||||||
default_error_messages = {
|
|
||||||
"invalid": _("'%(value)s' value must be valid YAML."),
|
|
||||||
}
|
|
||||||
widget = forms.Textarea
|
|
||||||
|
|
||||||
def to_python(self, value):
|
|
||||||
if self.disabled:
|
|
||||||
return value
|
|
||||||
if value in self.empty_values:
|
|
||||||
return None
|
|
||||||
if isinstance(value, (list, dict, int, float, YAMLString)):
|
|
||||||
return value
|
|
||||||
try:
|
|
||||||
converted = yaml.safe_load(value)
|
|
||||||
except yaml.YAMLError:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
self.error_messages["invalid"],
|
|
||||||
code="invalid",
|
|
||||||
params={"value": value},
|
|
||||||
)
|
|
||||||
if isinstance(converted, str):
|
|
||||||
return YAMLString(converted)
|
|
||||||
if converted is None:
|
|
||||||
return {}
|
|
||||||
return converted
|
|
||||||
|
|
||||||
def bound_data(self, data, initial):
|
|
||||||
if self.disabled:
|
|
||||||
return initial
|
|
||||||
try:
|
|
||||||
return yaml.safe_load(data)
|
|
||||||
except yaml.YAMLError:
|
|
||||||
return InvalidYAMLInput(data)
|
|
||||||
|
|
||||||
def prepare_value(self, value):
|
|
||||||
if isinstance(value, InvalidYAMLInput):
|
|
||||||
return value
|
|
||||||
return yaml.dump(value, explicit_start=True, default_flow_style=False)
|
|
||||||
|
|
||||||
def has_changed(self, initial, data):
|
|
||||||
if super().has_changed(initial, data):
|
|
||||||
return True
|
|
||||||
# For purposes of seeing whether something has changed, True isn't the
|
|
||||||
# same as 1 and the order of keys doesn't matter.
|
|
||||||
data = self.to_python(data)
|
|
||||||
return yaml.dump(initial, sort_keys=True) != yaml.dump(data, sort_keys=True)
|
|
@ -1,18 +0,0 @@
|
|||||||
"""Forms for modals on overview page"""
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyCacheClearForm(forms.Form):
|
|
||||||
"""Form to clear Policy cache"""
|
|
||||||
|
|
||||||
title = "Clear Policy cache"
|
|
||||||
body = """Are you sure you want to clear the policy cache?
|
|
||||||
This will cause all policies to be re-evaluated on their next usage."""
|
|
||||||
|
|
||||||
|
|
||||||
class FlowCacheClearForm(forms.Form):
|
|
||||||
"""Form to clear Flow cache"""
|
|
||||||
|
|
||||||
title = "Clear Flow cache"
|
|
||||||
body = """Are you sure you want to clear the flow cache?
|
|
||||||
This will cause all flows to be re-evaluated on their next usage."""
|
|
@ -1,12 +0,0 @@
|
|||||||
"""authentik administration forms"""
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from authentik.admin.fields import CodeMirrorWidget, YAMLField
|
|
||||||
from authentik.core.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyTestForm(forms.Form):
|
|
||||||
"""Form to test policies against user"""
|
|
||||||
|
|
||||||
user = forms.ModelChoiceField(queryset=User.objects.all())
|
|
||||||
context = YAMLField(widget=CodeMirrorWidget(), required=False, initial=dict)
|
|
@ -1,22 +0,0 @@
|
|||||||
"""authentik administrative user forms"""
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from authentik.admin.fields import CodeMirrorWidget, YAMLField
|
|
||||||
from authentik.core.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserForm(forms.ModelForm):
|
|
||||||
"""Update User Details"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = User
|
|
||||||
fields = ["username", "name", "email", "is_active", "attributes"]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput,
|
|
||||||
"attributes": CodeMirrorWidget,
|
|
||||||
}
|
|
||||||
field_classes = {
|
|
||||||
"attributes": YAMLField,
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
"""authentik admin mixins"""
|
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
|
||||||
|
|
||||||
|
|
||||||
class AdminRequiredMixin(UserPassesTestMixin):
|
|
||||||
"""Make sure user is administrator"""
|
|
||||||
|
|
||||||
def test_func(self):
|
|
||||||
return self.request.user.is_superuser
|
|
@ -4,7 +4,7 @@ from celery.schedules import crontab
|
|||||||
CELERY_BEAT_SCHEDULE = {
|
CELERY_BEAT_SCHEDULE = {
|
||||||
"admin_latest_version": {
|
"admin_latest_version": {
|
||||||
"task": "authentik.admin.tasks.update_latest_version",
|
"task": "authentik.admin.tasks.update_latest_version",
|
||||||
"schedule": crontab(minute=0), # Run every hour
|
"schedule": crontab(minute="*/60"), # Run every hour
|
||||||
"options": {"queue": "authentik_scheduled"},
|
"options": {"queue": "authentik_scheduled"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,9 @@ URL_FINDER = URLValidator.regex.pattern[1:]
|
|||||||
def update_latest_version(self: MonitoredTask):
|
def update_latest_version(self: MonitoredTask):
|
||||||
"""Update latest version info"""
|
"""Update latest version info"""
|
||||||
try:
|
try:
|
||||||
response = get("https://api.github.com/repos/beryju/authentik/releases/latest")
|
response = get(
|
||||||
|
"https://api.github.com/repos/goauthentik/authentik/releases/latest"
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
tag_name = data.get("tag_name")
|
tag_name = data.get("tag_name")
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% endblock %}
|
|
@ -1,14 +0,0 @@
|
|||||||
{% extends base_template|default:"generic/form.html" %}
|
|
||||||
|
|
||||||
{% load authentik_utils %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block above_form %}
|
|
||||||
<h1>
|
|
||||||
{% trans 'Generate Certificate-Key Pair' %}
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block action %}
|
|
||||||
{% trans 'Generate Certificate-Key Pair' %}
|
|
||||||
{% endblock %}
|
|
@ -1,13 +0,0 @@
|
|||||||
{% extends base_template|default:"generic/form.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block above_form %}
|
|
||||||
<h1>
|
|
||||||
{% trans 'Import Flow' %}
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block action %}
|
|
||||||
{% trans 'Import Flow' %}
|
|
||||||
{% endblock %}
|
|
@ -1,46 +0,0 @@
|
|||||||
{% extends 'generic/form.html' %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block above_form %}
|
|
||||||
<h1>{% blocktrans with policy=policy %}Test {{ policy }}{% endblocktrans %}</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block beneath_form %}
|
|
||||||
{% if result %}
|
|
||||||
<div class="pf-c-form__group ">
|
|
||||||
<div class="pf-c-form__group-label">
|
|
||||||
<label class="pf-c-form__label" for="context-1">
|
|
||||||
<span class="pf-c-form__label-text">{% trans 'Passing' %}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-form__group-label">
|
|
||||||
<div class="c-form__horizontal-group">
|
|
||||||
<span class="pf-c-form__label-text">{{ result.passing|yesno:"Yes,No" }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-form__group ">
|
|
||||||
<div class="pf-c-form__group-label">
|
|
||||||
<label class="pf-c-form__label" for="context-1">
|
|
||||||
<span class="pf-c-form__label-text">{% trans 'Messages' %}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-form__group-label">
|
|
||||||
<div class="c-form__horizontal-group">
|
|
||||||
<ul>
|
|
||||||
{% for m in result.messages %}
|
|
||||||
<li><span class="pf-c-form__label-text">{{ m }}</span></li>
|
|
||||||
{% empty %}
|
|
||||||
<li><span class="pf-c-form__label-text">-</span></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block action %}
|
|
||||||
{% trans 'Test' %}
|
|
||||||
{% endblock %}
|
|
@ -1,42 +0,0 @@
|
|||||||
{% extends "administration/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load authentik_utils %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="pf-c-page__main-section pf-m-light">
|
|
||||||
<div class="pf-c-content">
|
|
||||||
{% block above_form %}
|
|
||||||
<h1>
|
|
||||||
{% blocktrans with object_type=object|verbose_name %}
|
|
||||||
Disable {{ object_type }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="pf-c-page__main-section">
|
|
||||||
<div class="pf-l-stack">
|
|
||||||
<div class="pf-l-stack__item">
|
|
||||||
<div class="pf-c-card">
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
<form action="" method="post" class="pf-c-form">
|
|
||||||
{% csrf_token %}
|
|
||||||
<p>
|
|
||||||
{% blocktrans with object_type=object|verbose_name name=object %}
|
|
||||||
Are you sure you want to disable {{ object_type }} "{{ object }}"?
|
|
||||||
{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
|
||||||
<div class="pf-c-form__actions">
|
|
||||||
<input class="pf-c-button pf-m-danger" type="submit" value="{% trans 'Disable' %}" />
|
|
||||||
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Back" %}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
@ -1 +0,0 @@
|
|||||||
<ak-codemirror mode="{{ widget.attrs.mode }}"><textarea class="pf-c-form-control" name="{{ widget.name }}">{% if widget.value %}{{ widget.value }}{% endif %}</textarea></ak-codemirror>
|
|
@ -1,18 +0,0 @@
|
|||||||
{% extends base_template|default:"generic/form.html" %}
|
|
||||||
|
|
||||||
{% load authentik_utils %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block above_form %}
|
|
||||||
<h1>
|
|
||||||
{% blocktrans with type=form|form_verbose_name %}
|
|
||||||
Create {{ type }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block action %}
|
|
||||||
{% blocktrans with type=form|form_verbose_name %}
|
|
||||||
Create {{ type }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
{% endblock %}
|
|
@ -1,40 +0,0 @@
|
|||||||
{% extends container_template|default:"administration/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load authentik_utils %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="pf-c-page__main-section pf-m-light">
|
|
||||||
<div class="pf-c-content">
|
|
||||||
{% block above_form %}
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="pf-c-page__main-section">
|
|
||||||
<div class="pf-l-stack">
|
|
||||||
<div class="pf-l-stack__item">
|
|
||||||
<div class="pf-c-card">
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
<form id="main-form" action="" method="post" class="pf-c-form pf-m-horizontal" enctype="multipart/form-data">
|
|
||||||
{% include 'partials/form_horizontal.html' with form=form %}
|
|
||||||
{% block beneath_form %}
|
|
||||||
{% endblock %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<footer class="pf-c-modal-box__footer">
|
|
||||||
<ak-spinner-button form="main-form">
|
|
||||||
{% block action %}{% endblock %}
|
|
||||||
</ak-spinner-button>
|
|
||||||
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Cancel" %}</a>
|
|
||||||
</footer>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
{{ block.super }}
|
|
||||||
{{ form.media.js }}
|
|
||||||
{% endblock %}
|
|
@ -1,20 +0,0 @@
|
|||||||
{% extends base_template|default:"generic/form.html" %}
|
|
||||||
|
|
||||||
{% load authentik_utils %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block above_form %}
|
|
||||||
<h1>
|
|
||||||
{% trans form.title %}
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block beneath_form %}
|
|
||||||
<p>
|
|
||||||
{% trans form.body %}
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block action %}
|
|
||||||
{% trans 'Confirm' %}
|
|
||||||
{% endblock %}
|
|
@ -1,18 +0,0 @@
|
|||||||
{% extends base_template|default:"generic/form.html" %}
|
|
||||||
|
|
||||||
{% load authentik_utils %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block above_form %}
|
|
||||||
<h1>
|
|
||||||
{% blocktrans with type=form|form_verbose_name|title inst=form.instance %}
|
|
||||||
Update {{ inst }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block action %}
|
|
||||||
{% blocktrans with type=form|form_verbose_name %}
|
|
||||||
Update {{ type }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
{% endblock %}
|
|
@ -27,7 +27,7 @@ class TestAdminAPI(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
body = loads(response.content)
|
body = loads(response.content)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
any([task["task_name"] == "clean_expired_models" for task in body])
|
any(task["task_name"] == "clean_expired_models" for task in body)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tasks_retry(self):
|
def test_tasks_retry(self):
|
||||||
@ -39,9 +39,7 @@ class TestAdminAPI(TestCase):
|
|||||||
kwargs={"pk": "clean_expired_models"},
|
kwargs={"pk": "clean_expired_models"},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 204)
|
||||||
body = loads(response.content)
|
|
||||||
self.assertTrue(body["successful"])
|
|
||||||
|
|
||||||
def test_tasks_retry_404(self):
|
def test_tasks_retry_404(self):
|
||||||
"""Test Task API (retry, 404)"""
|
"""Test Task API (retry, 404)"""
|
||||||
@ -71,3 +69,8 @@ class TestAdminAPI(TestCase):
|
|||||||
"""Test metrics API"""
|
"""Test metrics API"""
|
||||||
response = self.client.get(reverse("authentik_api:admin_metrics-list"))
|
response = self.client.get(reverse("authentik_api:admin_metrics-list"))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_apps(self):
|
||||||
|
"""Test apps API"""
|
||||||
|
response = self.client.get(reverse("authentik_api:apps-list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
"""admin tests"""
|
|
||||||
from importlib import import_module
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from django.forms import ModelForm
|
|
||||||
from django.test import Client, TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.urls.exceptions import NoReverseMatch
|
|
||||||
|
|
||||||
from authentik.admin.urls import urlpatterns
|
|
||||||
from authentik.core.models import Group, User
|
|
||||||
from authentik.lib.utils.reflection import get_apps
|
|
||||||
|
|
||||||
|
|
||||||
class TestAdmin(TestCase):
|
|
||||||
"""Generic admin tests"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.user = User.objects.create_user(username="test")
|
|
||||||
self.user.ak_groups.add(Group.objects.filter(is_superuser=True).first())
|
|
||||||
self.user.save()
|
|
||||||
self.client = Client()
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
|
|
||||||
def generic_view_tester(view_name: str) -> Callable:
|
|
||||||
"""This is used instead of subTest for better visibility"""
|
|
||||||
|
|
||||||
def tester(self: TestAdmin):
|
|
||||||
try:
|
|
||||||
full_url = reverse(f"authentik_admin:{view_name}")
|
|
||||||
response = self.client.get(full_url)
|
|
||||||
self.assertTrue(response.status_code < 500)
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return tester
|
|
||||||
|
|
||||||
|
|
||||||
for url in urlpatterns:
|
|
||||||
method_name = url.name.replace("-", "_")
|
|
||||||
setattr(TestAdmin, f"test_view_{method_name}", generic_view_tester(url.name))
|
|
||||||
|
|
||||||
|
|
||||||
def generic_form_tester(form: ModelForm) -> Callable:
|
|
||||||
"""Test a form"""
|
|
||||||
|
|
||||||
def tester(self: TestAdmin):
|
|
||||||
form_inst = form()
|
|
||||||
self.assertFalse(form_inst.is_valid())
|
|
||||||
|
|
||||||
return tester
|
|
||||||
|
|
||||||
|
|
||||||
# Load the forms module from every app, so we have all forms loaded
|
|
||||||
for app in get_apps():
|
|
||||||
module = app.__module__.replace(".apps", ".forms")
|
|
||||||
try:
|
|
||||||
import_module(module)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for form_class in ModelForm.__subclasses__():
|
|
||||||
setattr(
|
|
||||||
TestAdmin, f"test_form_{form_class.__name__}", generic_form_tester(form_class)
|
|
||||||
)
|
|
@ -1,43 +0,0 @@
|
|||||||
"""admin tests"""
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.test.client import RequestFactory
|
|
||||||
|
|
||||||
from authentik.admin.views.policies_bindings import PolicyBindingCreateView
|
|
||||||
from authentik.core.models import Application
|
|
||||||
from authentik.policies.forms import PolicyBindingForm
|
|
||||||
|
|
||||||
|
|
||||||
class TestPolicyBindingView(TestCase):
|
|
||||||
"""Generic admin tests"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
|
|
||||||
def test_without_get_param(self):
|
|
||||||
"""Test PolicyBindingCreateView without get params"""
|
|
||||||
request = self.factory.get("/")
|
|
||||||
view = PolicyBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {})
|
|
||||||
|
|
||||||
def test_with_params_invalid(self):
|
|
||||||
"""Test PolicyBindingCreateView with invalid get params"""
|
|
||||||
request = self.factory.get("/", {"target": uuid4()})
|
|
||||||
view = PolicyBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {})
|
|
||||||
|
|
||||||
def test_with_params(self):
|
|
||||||
"""Test PolicyBindingCreateView with get params"""
|
|
||||||
target = Application.objects.create(name="test")
|
|
||||||
request = self.factory.get("/", {"target": target.pk.hex})
|
|
||||||
view = PolicyBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
|
|
||||||
|
|
||||||
self.assertTrue(
|
|
||||||
isinstance(
|
|
||||||
PolicyBindingForm(initial={"target": "foo"}).fields["target"].widget,
|
|
||||||
forms.HiddenInput,
|
|
||||||
)
|
|
||||||
)
|
|
@ -1,43 +0,0 @@
|
|||||||
"""admin tests"""
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.test.client import RequestFactory
|
|
||||||
|
|
||||||
from authentik.admin.views.stages_bindings import StageBindingCreateView
|
|
||||||
from authentik.flows.forms import FlowStageBindingForm
|
|
||||||
from authentik.flows.models import Flow
|
|
||||||
|
|
||||||
|
|
||||||
class TestStageBindingView(TestCase):
|
|
||||||
"""Generic admin tests"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
|
|
||||||
def test_without_get_param(self):
|
|
||||||
"""Test StageBindingCreateView without get params"""
|
|
||||||
request = self.factory.get("/")
|
|
||||||
view = StageBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {})
|
|
||||||
|
|
||||||
def test_with_params_invalid(self):
|
|
||||||
"""Test StageBindingCreateView with invalid get params"""
|
|
||||||
request = self.factory.get("/", {"target": uuid4()})
|
|
||||||
view = StageBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {})
|
|
||||||
|
|
||||||
def test_with_params(self):
|
|
||||||
"""Test StageBindingCreateView with get params"""
|
|
||||||
target = Flow.objects.create(name="test", slug="test")
|
|
||||||
request = self.factory.get("/", {"target": target.pk.hex})
|
|
||||||
view = StageBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
|
|
||||||
|
|
||||||
self.assertTrue(
|
|
||||||
isinstance(
|
|
||||||
FlowStageBindingForm(initial={"target": "foo"}).fields["target"].widget,
|
|
||||||
forms.HiddenInput,
|
|
||||||
)
|
|
||||||
)
|
|
@ -1,344 +0,0 @@
|
|||||||
"""authentik URL Configuration"""
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from authentik.admin.views import (
|
|
||||||
applications,
|
|
||||||
certificate_key_pair,
|
|
||||||
events_notifications_rules,
|
|
||||||
events_notifications_transports,
|
|
||||||
flows,
|
|
||||||
groups,
|
|
||||||
outposts,
|
|
||||||
outposts_service_connections,
|
|
||||||
overview,
|
|
||||||
policies,
|
|
||||||
policies_bindings,
|
|
||||||
property_mappings,
|
|
||||||
providers,
|
|
||||||
sources,
|
|
||||||
stages,
|
|
||||||
stages_bindings,
|
|
||||||
stages_invitations,
|
|
||||||
stages_prompts,
|
|
||||||
tokens,
|
|
||||||
users,
|
|
||||||
)
|
|
||||||
from authentik.providers.saml.views.metadata import MetadataImportView
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path(
|
|
||||||
"overview/cache/flow/",
|
|
||||||
overview.FlowCacheClearView.as_view(),
|
|
||||||
name="overview-clear-flow-cache",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"overview/cache/policy/",
|
|
||||||
overview.PolicyCacheClearView.as_view(),
|
|
||||||
name="overview-clear-policy-cache",
|
|
||||||
),
|
|
||||||
# Applications
|
|
||||||
path(
|
|
||||||
"applications/create/",
|
|
||||||
applications.ApplicationCreateView.as_view(),
|
|
||||||
name="application-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"applications/<uuid:pk>/update/",
|
|
||||||
applications.ApplicationUpdateView.as_view(),
|
|
||||||
name="application-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"applications/<uuid:pk>/delete/",
|
|
||||||
applications.ApplicationDeleteView.as_view(),
|
|
||||||
name="application-delete",
|
|
||||||
),
|
|
||||||
# Tokens
|
|
||||||
path(
|
|
||||||
"tokens/<uuid:pk>/delete/",
|
|
||||||
tokens.TokenDeleteView.as_view(),
|
|
||||||
name="token-delete",
|
|
||||||
),
|
|
||||||
# Sources
|
|
||||||
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
|
|
||||||
path(
|
|
||||||
"sources/<uuid:pk>/update/",
|
|
||||||
sources.SourceUpdateView.as_view(),
|
|
||||||
name="source-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"sources/<uuid:pk>/delete/",
|
|
||||||
sources.SourceDeleteView.as_view(),
|
|
||||||
name="source-delete",
|
|
||||||
),
|
|
||||||
# Policies
|
|
||||||
path("policies/create/", policies.PolicyCreateView.as_view(), name="policy-create"),
|
|
||||||
path(
|
|
||||||
"policies/<uuid:pk>/update/",
|
|
||||||
policies.PolicyUpdateView.as_view(),
|
|
||||||
name="policy-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"policies/<uuid:pk>/delete/",
|
|
||||||
policies.PolicyDeleteView.as_view(),
|
|
||||||
name="policy-delete",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"policies/<uuid:pk>/test/",
|
|
||||||
policies.PolicyTestView.as_view(),
|
|
||||||
name="policy-test",
|
|
||||||
),
|
|
||||||
# Policy bindings
|
|
||||||
path(
|
|
||||||
"policies/bindings/create/",
|
|
||||||
policies_bindings.PolicyBindingCreateView.as_view(),
|
|
||||||
name="policy-binding-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"policies/bindings/<uuid:pk>/update/",
|
|
||||||
policies_bindings.PolicyBindingUpdateView.as_view(),
|
|
||||||
name="policy-binding-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"policies/bindings/<uuid:pk>/delete/",
|
|
||||||
policies_bindings.PolicyBindingDeleteView.as_view(),
|
|
||||||
name="policy-binding-delete",
|
|
||||||
),
|
|
||||||
# Providers
|
|
||||||
path(
|
|
||||||
"providers/create/",
|
|
||||||
providers.ProviderCreateView.as_view(),
|
|
||||||
name="provider-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"providers/create/saml/from-metadata/",
|
|
||||||
MetadataImportView.as_view(),
|
|
||||||
name="provider-saml-from-metadata",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"providers/<int:pk>/update/",
|
|
||||||
providers.ProviderUpdateView.as_view(),
|
|
||||||
name="provider-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"providers/<int:pk>/delete/",
|
|
||||||
providers.ProviderDeleteView.as_view(),
|
|
||||||
name="provider-delete",
|
|
||||||
),
|
|
||||||
# Stages
|
|
||||||
path("stages/create/", stages.StageCreateView.as_view(), name="stage-create"),
|
|
||||||
path(
|
|
||||||
"stages/<uuid:pk>/update/",
|
|
||||||
stages.StageUpdateView.as_view(),
|
|
||||||
name="stage-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"stages/<uuid:pk>/delete/",
|
|
||||||
stages.StageDeleteView.as_view(),
|
|
||||||
name="stage-delete",
|
|
||||||
),
|
|
||||||
# Stage bindings
|
|
||||||
path(
|
|
||||||
"stages/bindings/create/",
|
|
||||||
stages_bindings.StageBindingCreateView.as_view(),
|
|
||||||
name="stage-binding-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"stages/bindings/<uuid:pk>/update/",
|
|
||||||
stages_bindings.StageBindingUpdateView.as_view(),
|
|
||||||
name="stage-binding-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"stages/bindings/<uuid:pk>/delete/",
|
|
||||||
stages_bindings.StageBindingDeleteView.as_view(),
|
|
||||||
name="stage-binding-delete",
|
|
||||||
),
|
|
||||||
# Stage Prompts
|
|
||||||
path(
|
|
||||||
"stages_prompts/create/",
|
|
||||||
stages_prompts.PromptCreateView.as_view(),
|
|
||||||
name="stage-prompt-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"stages_prompts/<uuid:pk>/update/",
|
|
||||||
stages_prompts.PromptUpdateView.as_view(),
|
|
||||||
name="stage-prompt-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"stages_prompts/<uuid:pk>/delete/",
|
|
||||||
stages_prompts.PromptDeleteView.as_view(),
|
|
||||||
name="stage-prompt-delete",
|
|
||||||
),
|
|
||||||
# Stage Invitations
|
|
||||||
path(
|
|
||||||
"stages/invitations/create/",
|
|
||||||
stages_invitations.InvitationCreateView.as_view(),
|
|
||||||
name="stage-invitation-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"stages/invitations/<uuid:pk>/delete/",
|
|
||||||
stages_invitations.InvitationDeleteView.as_view(),
|
|
||||||
name="stage-invitation-delete",
|
|
||||||
),
|
|
||||||
# Flows
|
|
||||||
path(
|
|
||||||
"flows/create/",
|
|
||||||
flows.FlowCreateView.as_view(),
|
|
||||||
name="flow-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"flows/import/",
|
|
||||||
flows.FlowImportView.as_view(),
|
|
||||||
name="flow-import",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"flows/<uuid:pk>/update/",
|
|
||||||
flows.FlowUpdateView.as_view(),
|
|
||||||
name="flow-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"flows/<uuid:pk>/execute/",
|
|
||||||
flows.FlowDebugExecuteView.as_view(),
|
|
||||||
name="flow-execute",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"flows/<uuid:pk>/export/",
|
|
||||||
flows.FlowExportView.as_view(),
|
|
||||||
name="flow-export",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"flows/<uuid:pk>/delete/",
|
|
||||||
flows.FlowDeleteView.as_view(),
|
|
||||||
name="flow-delete",
|
|
||||||
),
|
|
||||||
# Property Mappings
|
|
||||||
path(
|
|
||||||
"property-mappings/create/",
|
|
||||||
property_mappings.PropertyMappingCreateView.as_view(),
|
|
||||||
name="property-mapping-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"property-mappings/<uuid:pk>/update/",
|
|
||||||
property_mappings.PropertyMappingUpdateView.as_view(),
|
|
||||||
name="property-mapping-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"property-mappings/<uuid:pk>/delete/",
|
|
||||||
property_mappings.PropertyMappingDeleteView.as_view(),
|
|
||||||
name="property-mapping-delete",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"property-mappings/<uuid:pk>/test/",
|
|
||||||
property_mappings.PropertyMappingTestView.as_view(),
|
|
||||||
name="property-mapping-test",
|
|
||||||
),
|
|
||||||
# Users
|
|
||||||
path("users/create/", users.UserCreateView.as_view(), name="user-create"),
|
|
||||||
path("users/<int:pk>/update/", users.UserUpdateView.as_view(), name="user-update"),
|
|
||||||
path("users/<int:pk>/delete/", users.UserDeleteView.as_view(), name="user-delete"),
|
|
||||||
path(
|
|
||||||
"users/<int:pk>/disable/", users.UserDisableView.as_view(), name="user-disable"
|
|
||||||
),
|
|
||||||
path("users/<int:pk>/enable/", users.UserEnableView.as_view(), name="user-enable"),
|
|
||||||
path(
|
|
||||||
"users/<int:pk>/reset/",
|
|
||||||
users.UserPasswordResetView.as_view(),
|
|
||||||
name="user-password-reset",
|
|
||||||
),
|
|
||||||
# Groups
|
|
||||||
path("groups/create/", groups.GroupCreateView.as_view(), name="group-create"),
|
|
||||||
path(
|
|
||||||
"groups/<uuid:pk>/update/",
|
|
||||||
groups.GroupUpdateView.as_view(),
|
|
||||||
name="group-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"groups/<uuid:pk>/delete/",
|
|
||||||
groups.GroupDeleteView.as_view(),
|
|
||||||
name="group-delete",
|
|
||||||
),
|
|
||||||
# Certificate-Key Pairs
|
|
||||||
path(
|
|
||||||
"crypto/certificates/create/",
|
|
||||||
certificate_key_pair.CertificateKeyPairCreateView.as_view(),
|
|
||||||
name="certificatekeypair-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"crypto/certificates/generate/",
|
|
||||||
certificate_key_pair.CertificateKeyPairGenerateView.as_view(),
|
|
||||||
name="certificatekeypair-generate",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"crypto/certificates/<uuid:pk>/update/",
|
|
||||||
certificate_key_pair.CertificateKeyPairUpdateView.as_view(),
|
|
||||||
name="certificatekeypair-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"crypto/certificates/<uuid:pk>/delete/",
|
|
||||||
certificate_key_pair.CertificateKeyPairDeleteView.as_view(),
|
|
||||||
name="certificatekeypair-delete",
|
|
||||||
),
|
|
||||||
# Outposts
|
|
||||||
path(
|
|
||||||
"outposts/create/",
|
|
||||||
outposts.OutpostCreateView.as_view(),
|
|
||||||
name="outpost-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"outposts/<uuid:pk>/update/",
|
|
||||||
outposts.OutpostUpdateView.as_view(),
|
|
||||||
name="outpost-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"outposts/<uuid:pk>/delete/",
|
|
||||||
outposts.OutpostDeleteView.as_view(),
|
|
||||||
name="outpost-delete",
|
|
||||||
),
|
|
||||||
# Outpost Service Connections
|
|
||||||
path(
|
|
||||||
"outpost_service_connections/create/",
|
|
||||||
outposts_service_connections.OutpostServiceConnectionCreateView.as_view(),
|
|
||||||
name="outpost-service-connection-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"outpost_service_connections/<uuid:pk>/update/",
|
|
||||||
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
|
|
||||||
name="outpost-service-connection-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"outpost_service_connections/<uuid:pk>/delete/",
|
|
||||||
outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(),
|
|
||||||
name="outpost-service-connection-delete",
|
|
||||||
),
|
|
||||||
# Event Notification Transpots
|
|
||||||
path(
|
|
||||||
"events/transports/create/",
|
|
||||||
events_notifications_transports.NotificationTransportCreateView.as_view(),
|
|
||||||
name="notification-transport-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/transports/<uuid:pk>/update/",
|
|
||||||
events_notifications_transports.NotificationTransportUpdateView.as_view(),
|
|
||||||
name="notification-transport-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/transports/<uuid:pk>/delete/",
|
|
||||||
events_notifications_transports.NotificationTransportDeleteView.as_view(),
|
|
||||||
name="notification-transport-delete",
|
|
||||||
),
|
|
||||||
# Event Notification Rules
|
|
||||||
path(
|
|
||||||
"events/rules/create/",
|
|
||||||
events_notifications_rules.NotificationRuleCreateView.as_view(),
|
|
||||||
name="notification-rule-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/rules/<uuid:pk>/update/",
|
|
||||||
events_notifications_rules.NotificationRuleUpdateView.as_view(),
|
|
||||||
name="notification-rule-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/rules/<uuid:pk>/delete/",
|
|
||||||
events_notifications_rules.NotificationRuleDeleteView.as_view(),
|
|
||||||
name="notification-rule-delete",
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,80 +0,0 @@
|
|||||||
"""authentik Application administration"""
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
from guardian.shortcuts import get_objects_for_user
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.core.forms.applications import ApplicationForm
|
|
||||||
from authentik.core.models import Application
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Application"""
|
|
||||||
|
|
||||||
model = Application
|
|
||||||
form_class = ApplicationForm
|
|
||||||
permission_required = "authentik_core.add_application"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Application")
|
|
||||||
|
|
||||||
def get_initial(self) -> dict[str, Any]:
|
|
||||||
if "provider" in self.request.GET:
|
|
||||||
try:
|
|
||||||
initial_provider_pk = int(self.request.GET["provider"])
|
|
||||||
except ValueError:
|
|
||||||
return super().get_initial()
|
|
||||||
providers = (
|
|
||||||
get_objects_for_user(self.request.user, "authentik_core.view_provider")
|
|
||||||
.filter(pk=initial_provider_pk)
|
|
||||||
.select_subclasses()
|
|
||||||
)
|
|
||||||
if not providers.exists():
|
|
||||||
return {}
|
|
||||||
return {"provider": providers.first()}
|
|
||||||
return super().get_initial()
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update application"""
|
|
||||||
|
|
||||||
model = Application
|
|
||||||
form_class = ApplicationForm
|
|
||||||
permission_required = "authentik_core.change_application"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Application")
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete application"""
|
|
||||||
|
|
||||||
model = Application
|
|
||||||
permission_required = "authentik_core.delete_application"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_message = _("Successfully deleted Application")
|
|
@ -1,95 +0,0 @@
|
|||||||
"""authentik CertificateKeyPair administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http.response import HttpResponse
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from django.views.generic.edit import FormView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.crypto.builder import CertificateBuilder
|
|
||||||
from authentik.crypto.forms import (
|
|
||||||
CertificateKeyPairForm,
|
|
||||||
CertificateKeyPairGenerateForm,
|
|
||||||
)
|
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new CertificateKeyPair"""
|
|
||||||
|
|
||||||
model = CertificateKeyPair
|
|
||||||
form_class = CertificateKeyPairForm
|
|
||||||
permission_required = "authentik_crypto.add_certificatekeypair"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created Certificate-Key Pair")
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairGenerateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
FormView,
|
|
||||||
):
|
|
||||||
"""Generate new CertificateKeyPair"""
|
|
||||||
|
|
||||||
model = CertificateKeyPair
|
|
||||||
form_class = CertificateKeyPairGenerateForm
|
|
||||||
permission_required = "authentik_crypto.add_certificatekeypair"
|
|
||||||
|
|
||||||
template_name = "administration/certificatekeypair/generate.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully generated Certificate-Key Pair")
|
|
||||||
|
|
||||||
def form_valid(self, form: CertificateKeyPairGenerateForm) -> HttpResponse:
|
|
||||||
builder = CertificateBuilder()
|
|
||||||
builder.common_name = form.data["common_name"]
|
|
||||||
builder.build(
|
|
||||||
subject_alt_names=form.data.get("subject_alt_name", "").split(","),
|
|
||||||
validity_days=int(form.data["validity_days"]),
|
|
||||||
)
|
|
||||||
builder.save()
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update certificatekeypair"""
|
|
||||||
|
|
||||||
model = CertificateKeyPair
|
|
||||||
form_class = CertificateKeyPairForm
|
|
||||||
permission_required = "authentik_crypto.change_certificatekeypair"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated Certificate-Key Pair")
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete certificatekeypair"""
|
|
||||||
|
|
||||||
model = CertificateKeyPair
|
|
||||||
permission_required = "authentik_crypto.delete_certificatekeypair"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Certificate-Key Pair")
|
|
@ -1,61 +0,0 @@
|
|||||||
"""authentik NotificationRule administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.events.forms import NotificationRuleForm
|
|
||||||
from authentik.events.models import NotificationRule
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new NotificationRule"""
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
form_class = NotificationRuleForm
|
|
||||||
permission_required = "authentik_events.add_NotificationRule"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Notification Rule")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update application"""
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
form_class = NotificationRuleForm
|
|
||||||
permission_required = "authentik_events.change_NotificationRule"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Notification Rule")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete application"""
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
permission_required = "authentik_events.delete_NotificationRule"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_message = _("Successfully deleted Notification Rule")
|
|
@ -1,58 +0,0 @@
|
|||||||
"""authentik NotificationTransport administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.events.forms import NotificationTransportForm
|
|
||||||
from authentik.events.models import NotificationTransport
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new NotificationTransport"""
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
form_class = NotificationTransportForm
|
|
||||||
permission_required = "authentik_events.add_notificationtransport"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Notification Transport")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update application"""
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
form_class = NotificationTransportForm
|
|
||||||
permission_required = "authentik_events.change_notificationtransport"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Notification Transport")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete application"""
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
permission_required = "authentik_events.delete_notificationtransport"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_message = _("Successfully deleted Notification Transport")
|
|
@ -1,138 +0,0 @@
|
|||||||
"""authentik Flow administration"""
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import DetailView, FormView, UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
|
||||||
from authentik.flows.forms import FlowForm, FlowImportForm
|
|
||||||
from authentik.flows.models import Flow
|
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
|
||||||
from authentik.flows.transfer.common import DataclassEncoder
|
|
||||||
from authentik.flows.transfer.exporter import FlowExporter
|
|
||||||
from authentik.flows.transfer.importer import FlowImporter
|
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN, FlowPlanner
|
|
||||||
from authentik.lib.utils.urls import redirect_with_qs
|
|
||||||
from authentik.lib.views import CreateAssignPermView, bad_request_message
|
|
||||||
|
|
||||||
|
|
||||||
class FlowCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Flow"""
|
|
||||||
|
|
||||||
model = Flow
|
|
||||||
form_class = FlowForm
|
|
||||||
permission_required = "authentik_flows.add_flow"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created Flow")
|
|
||||||
|
|
||||||
|
|
||||||
class FlowUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update flow"""
|
|
||||||
|
|
||||||
model = Flow
|
|
||||||
form_class = FlowForm
|
|
||||||
permission_required = "authentik_flows.change_flow"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated Flow")
|
|
||||||
|
|
||||||
|
|
||||||
class FlowDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
|
||||||
"""Delete flow"""
|
|
||||||
|
|
||||||
model = Flow
|
|
||||||
permission_required = "authentik_flows.delete_flow"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Flow")
|
|
||||||
|
|
||||||
|
|
||||||
class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
||||||
"""Debug exectue flow, setting the current user as pending user"""
|
|
||||||
|
|
||||||
model = Flow
|
|
||||||
permission_required = "authentik_flows.view_flow"
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def get(self, request: HttpRequest, pk: str) -> HttpResponse:
|
|
||||||
"""Debug exectue flow, setting the current user as pending user"""
|
|
||||||
flow: Flow = self.get_object()
|
|
||||||
planner = FlowPlanner(flow)
|
|
||||||
planner.use_cache = False
|
|
||||||
try:
|
|
||||||
plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user})
|
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
|
||||||
except FlowNonApplicableException as exc:
|
|
||||||
return bad_request_message(
|
|
||||||
request,
|
|
||||||
_(
|
|
||||||
"Flow not applicable to current user/request: %(messages)s"
|
|
||||||
% {"messages": str(exc)}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return redirect_with_qs(
|
|
||||||
"authentik_flows:flow-executor-shell",
|
|
||||||
self.request.GET,
|
|
||||||
flow_slug=flow.slug,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FlowImportView(LoginRequiredMixin, FormView):
|
|
||||||
"""Import flow from JSON Export; only allowed for superusers
|
|
||||||
as these flows can contain python code"""
|
|
||||||
|
|
||||||
form_class = FlowImportForm
|
|
||||||
template_name = "administration/flow/import.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
if not request.user.is_superuser:
|
|
||||||
return self.handle_no_permission()
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def form_valid(self, form: FlowImportForm) -> HttpResponse:
|
|
||||||
importer = FlowImporter(form.cleaned_data["flow"].read().decode())
|
|
||||||
successful = importer.apply()
|
|
||||||
if not successful:
|
|
||||||
messages.error(self.request, _("Failed to import flow."))
|
|
||||||
else:
|
|
||||||
messages.success(self.request, _("Successfully imported flow."))
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class FlowExportView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
||||||
"""Export Flow"""
|
|
||||||
|
|
||||||
model = Flow
|
|
||||||
permission_required = "authentik_flows.export_flow"
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def get(self, request: HttpRequest, pk: str) -> HttpResponse:
|
|
||||||
"""Debug exectue flow, setting the current user as pending user"""
|
|
||||||
flow: Flow = self.get_object()
|
|
||||||
exporter = FlowExporter(flow)
|
|
||||||
response = JsonResponse(exporter.export(), encoder=DataclassEncoder, safe=False)
|
|
||||||
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.akflow"'
|
|
||||||
return response
|
|
@ -1,60 +0,0 @@
|
|||||||
"""authentik Group administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.core.forms.groups import GroupForm
|
|
||||||
from authentik.core.models import Group
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class GroupCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Group"""
|
|
||||||
|
|
||||||
model = Group
|
|
||||||
form_class = GroupForm
|
|
||||||
permission_required = "authentik_core.add_group"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created Group")
|
|
||||||
|
|
||||||
|
|
||||||
class GroupUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update group"""
|
|
||||||
|
|
||||||
model = Group
|
|
||||||
form_class = GroupForm
|
|
||||||
permission_required = "authentik_core.change_group"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated Group")
|
|
||||||
|
|
||||||
|
|
||||||
class GroupDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
|
||||||
"""Delete group"""
|
|
||||||
|
|
||||||
model = Group
|
|
||||||
permission_required = "authentik_flows.delete_group"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Group")
|
|
@ -1,66 +0,0 @@
|
|||||||
"""authentik Outpost administration"""
|
|
||||||
from dataclasses import asdict
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
from authentik.outposts.forms import OutpostForm
|
|
||||||
from authentik.outposts.models import Outpost, OutpostConfig
|
|
||||||
|
|
||||||
|
|
||||||
class OutpostCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Outpost"""
|
|
||||||
|
|
||||||
model = Outpost
|
|
||||||
form_class = OutpostForm
|
|
||||||
permission_required = "authentik_outposts.add_outpost"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Outpost")
|
|
||||||
|
|
||||||
def get_initial(self) -> dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"_config": asdict(
|
|
||||||
OutpostConfig(authentik_host=self.request.build_absolute_uri("/"))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class OutpostUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update outpost"""
|
|
||||||
|
|
||||||
model = Outpost
|
|
||||||
form_class = OutpostForm
|
|
||||||
permission_required = "authentik_outposts.change_outpost"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Outpost")
|
|
||||||
|
|
||||||
|
|
||||||
class OutpostDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
|
||||||
"""Delete outpost"""
|
|
||||||
|
|
||||||
model = Outpost
|
|
||||||
permission_required = "authentik_outposts.delete_outpost"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_message = _("Successfully deleted Outpost")
|
|
@ -1,61 +0,0 @@
|
|||||||
"""authentik OutpostServiceConnection administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import (
|
|
||||||
DeleteMessageView,
|
|
||||||
InheritanceCreateView,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
)
|
|
||||||
from authentik.outposts.models import OutpostServiceConnection
|
|
||||||
|
|
||||||
|
|
||||||
class OutpostServiceConnectionCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
InheritanceCreateView,
|
|
||||||
):
|
|
||||||
"""Create new OutpostServiceConnection"""
|
|
||||||
|
|
||||||
model = OutpostServiceConnection
|
|
||||||
permission_required = "authentik_outposts.add_outpostserviceconnection"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created Outpost Service Connection")
|
|
||||||
|
|
||||||
|
|
||||||
class OutpostServiceConnectionUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
):
|
|
||||||
"""Update outpostserviceconnection"""
|
|
||||||
|
|
||||||
model = OutpostServiceConnection
|
|
||||||
permission_required = "authentik_outposts.change_outpostserviceconnection"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated Outpost Service Connection")
|
|
||||||
|
|
||||||
|
|
||||||
class OutpostServiceConnectionDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete outpostserviceconnection"""
|
|
||||||
|
|
||||||
model = OutpostServiceConnection
|
|
||||||
permission_required = "authentik_outposts.delete_outpostserviceconnection"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Outpost Service Connection")
|
|
@ -1,47 +0,0 @@
|
|||||||
"""authentik administration overview"""
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.http.request import HttpRequest
|
|
||||||
from django.http.response import HttpResponse
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import FormView
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm
|
|
||||||
from authentik.admin.mixins import AdminRequiredMixin
|
|
||||||
from authentik.core.api.applications import user_app_cache_key
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
|
||||||
"""View to clear Policy cache"""
|
|
||||||
|
|
||||||
form_class = PolicyCacheClearForm
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/form_non_model.html"
|
|
||||||
success_message = _("Successfully cleared Policy cache")
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
keys = cache.keys("policy_*")
|
|
||||||
cache.delete_many(keys)
|
|
||||||
LOGGER.debug("Cleared Policy cache", keys=len(keys))
|
|
||||||
# Also delete user application cache
|
|
||||||
keys = cache.keys(user_app_cache_key("*"))
|
|
||||||
cache.delete_many(keys)
|
|
||||||
return super().post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FlowCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
|
||||||
"""View to clear Flow cache"""
|
|
||||||
|
|
||||||
form_class = FlowCacheClearForm
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/form_non_model.html"
|
|
||||||
success_message = _("Successfully cleared Flow cache")
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
keys = cache.keys("flow_*")
|
|
||||||
cache.delete_many(keys)
|
|
||||||
LOGGER.debug("Cleared flow cache", keys=len(keys))
|
|
||||||
return super().post(request, *args, **kwargs)
|
|
@ -1,104 +0,0 @@
|
|||||||
"""authentik Policy administration"""
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import FormView
|
|
||||||
from django.views.generic.detail import DetailView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.forms.policies import PolicyTestForm
|
|
||||||
from authentik.admin.views.utils import (
|
|
||||||
DeleteMessageView,
|
|
||||||
InheritanceCreateView,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
)
|
|
||||||
from authentik.policies.models import Policy, PolicyBinding
|
|
||||||
from authentik.policies.process import PolicyProcess, PolicyRequest
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
InheritanceCreateView,
|
|
||||||
):
|
|
||||||
"""Create new Policy"""
|
|
||||||
|
|
||||||
model = Policy
|
|
||||||
permission_required = "authentik_policies.add_policy"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created Policy")
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
):
|
|
||||||
"""Update policy"""
|
|
||||||
|
|
||||||
model = Policy
|
|
||||||
permission_required = "authentik_policies.change_policy"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated Policy")
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
|
||||||
"""Delete policy"""
|
|
||||||
|
|
||||||
model = Policy
|
|
||||||
permission_required = "authentik_policies.delete_policy"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Policy")
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, FormView):
|
|
||||||
"""View to test policy(s)"""
|
|
||||||
|
|
||||||
model = Policy
|
|
||||||
form_class = PolicyTestForm
|
|
||||||
permission_required = "authentik_policies.view_policy"
|
|
||||||
template_name = "administration/policy/test.html"
|
|
||||||
object = None
|
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> Policy:
|
|
||||||
return (
|
|
||||||
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
kwargs["policy"] = self.get_object()
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
def post(self, *args, **kwargs) -> HttpResponse:
|
|
||||||
self.object = self.get_object()
|
|
||||||
return super().post(*args, **kwargs)
|
|
||||||
|
|
||||||
def form_valid(self, form: PolicyTestForm) -> HttpResponse:
|
|
||||||
policy = self.get_object()
|
|
||||||
user = form.cleaned_data.get("user")
|
|
||||||
|
|
||||||
p_request = PolicyRequest(user)
|
|
||||||
p_request.debug = True
|
|
||||||
p_request.set_http_request(self.request)
|
|
||||||
p_request.context = form.cleaned_data.get("context", {})
|
|
||||||
|
|
||||||
proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None)
|
|
||||||
result = proc.execute()
|
|
||||||
context = self.get_context_data(form=form)
|
|
||||||
context["result"] = result
|
|
||||||
return self.render_to_response(context)
|
|
@ -1,81 +0,0 @@
|
|||||||
"""authentik PolicyBinding administration"""
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.db.models import Max
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
from authentik.policies.forms import PolicyBindingForm
|
|
||||||
from authentik.policies.models import PolicyBinding, PolicyBindingModel
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new PolicyBinding"""
|
|
||||||
|
|
||||||
model = PolicyBinding
|
|
||||||
permission_required = "authentik_policies.add_policybinding"
|
|
||||||
form_class = PolicyBindingForm
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created PolicyBinding")
|
|
||||||
|
|
||||||
def get_initial(self) -> dict[str, Any]:
|
|
||||||
if "target" in self.request.GET:
|
|
||||||
initial_target_pk = self.request.GET["target"]
|
|
||||||
targets = PolicyBindingModel.objects.filter(
|
|
||||||
pk=initial_target_pk
|
|
||||||
).select_subclasses()
|
|
||||||
if not targets.exists():
|
|
||||||
return {}
|
|
||||||
max_order = PolicyBinding.objects.filter(target=targets.first()).aggregate(
|
|
||||||
Max("order")
|
|
||||||
)["order__max"]
|
|
||||||
if not isinstance(max_order, int):
|
|
||||||
max_order = -1
|
|
||||||
return {"target": targets.first(), "order": max_order + 1}
|
|
||||||
return super().get_initial()
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update policybinding"""
|
|
||||||
|
|
||||||
model = PolicyBinding
|
|
||||||
permission_required = "authentik_policies.change_policybinding"
|
|
||||||
form_class = PolicyBindingForm
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated PolicyBinding")
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete policybinding"""
|
|
||||||
|
|
||||||
model = PolicyBinding
|
|
||||||
permission_required = "authentik_policies.delete_policybinding"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted PolicyBinding")
|
|
@ -1,105 +0,0 @@
|
|||||||
"""authentik PropertyMapping administration"""
|
|
||||||
from json import dumps
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import FormView
|
|
||||||
from django.views.generic.detail import DetailView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.forms.policies import PolicyTestForm
|
|
||||||
from authentik.admin.views.utils import (
|
|
||||||
DeleteMessageView,
|
|
||||||
InheritanceCreateView,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
)
|
|
||||||
from authentik.core.models import PropertyMapping
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
InheritanceCreateView,
|
|
||||||
):
|
|
||||||
"""Create new PropertyMapping"""
|
|
||||||
|
|
||||||
model = PropertyMapping
|
|
||||||
permission_required = "authentik_core.add_propertymapping"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Property Mapping")
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
):
|
|
||||||
"""Update property_mapping"""
|
|
||||||
|
|
||||||
model = PropertyMapping
|
|
||||||
permission_required = "authentik_core.change_propertymapping"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Property Mapping")
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete property_mapping"""
|
|
||||||
|
|
||||||
model = PropertyMapping
|
|
||||||
permission_required = "authentik_core.delete_propertymapping"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_message = _("Successfully deleted Property Mapping")
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingTestView(
|
|
||||||
LoginRequiredMixin, DetailView, PermissionRequiredMixin, FormView
|
|
||||||
):
|
|
||||||
"""View to test property mappings"""
|
|
||||||
|
|
||||||
model = PropertyMapping
|
|
||||||
form_class = PolicyTestForm
|
|
||||||
permission_required = "authentik_core.view_propertymapping"
|
|
||||||
template_name = "administration/property_mapping/test.html"
|
|
||||||
object = None
|
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> PropertyMapping:
|
|
||||||
return (
|
|
||||||
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
|
|
||||||
.select_subclasses()
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
kwargs["property_mapping"] = self.get_object()
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
def post(self, *args, **kwargs) -> HttpResponse:
|
|
||||||
self.object = self.get_object()
|
|
||||||
return super().post(*args, **kwargs)
|
|
||||||
|
|
||||||
def form_valid(self, form: PolicyTestForm) -> HttpResponse:
|
|
||||||
mapping = self.get_object()
|
|
||||||
user = form.cleaned_data.get("user")
|
|
||||||
|
|
||||||
context = self.get_context_data(form=form)
|
|
||||||
try:
|
|
||||||
result = mapping.evaluate(
|
|
||||||
user, self.request, **form.cleaned_data.get("context", {})
|
|
||||||
)
|
|
||||||
context["result"] = dumps(result, indent=4)
|
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
|
||||||
context["result"] = str(exc)
|
|
||||||
return self.render_to_response(context)
|
|
@ -1,57 +0,0 @@
|
|||||||
"""authentik Provider administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import (
|
|
||||||
DeleteMessageView,
|
|
||||||
InheritanceCreateView,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
)
|
|
||||||
from authentik.core.models import Provider
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
InheritanceCreateView,
|
|
||||||
):
|
|
||||||
"""Create new Provider"""
|
|
||||||
|
|
||||||
model = Provider
|
|
||||||
permission_required = "authentik_core.add_provider"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Provider")
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
):
|
|
||||||
"""Update provider"""
|
|
||||||
|
|
||||||
model = Provider
|
|
||||||
permission_required = "authentik_core.change_provider"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Provider")
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete provider"""
|
|
||||||
|
|
||||||
model = Provider
|
|
||||||
permission_required = "authentik_core.delete_provider"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_message = _("Successfully deleted Provider")
|
|
@ -1,58 +0,0 @@
|
|||||||
"""authentik Source administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import (
|
|
||||||
DeleteMessageView,
|
|
||||||
InheritanceCreateView,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
)
|
|
||||||
from authentik.core.models import Source
|
|
||||||
|
|
||||||
|
|
||||||
class SourceCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
InheritanceCreateView,
|
|
||||||
):
|
|
||||||
"""Create new Source"""
|
|
||||||
|
|
||||||
model = Source
|
|
||||||
permission_required = "authentik_core.add_source"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Source")
|
|
||||||
|
|
||||||
|
|
||||||
class SourceUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
):
|
|
||||||
"""Update source"""
|
|
||||||
|
|
||||||
model = Source
|
|
||||||
permission_required = "authentik_core.change_source"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Source")
|
|
||||||
|
|
||||||
|
|
||||||
class SourceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
|
||||||
"""Delete source"""
|
|
||||||
|
|
||||||
model = Source
|
|
||||||
permission_required = "authentik_core.delete_source"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_message = _("Successfully deleted Source")
|
|
@ -1,57 +0,0 @@
|
|||||||
"""authentik Stage administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import (
|
|
||||||
DeleteMessageView,
|
|
||||||
InheritanceCreateView,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
)
|
|
||||||
from authentik.flows.models import Stage
|
|
||||||
|
|
||||||
|
|
||||||
class StageCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
InheritanceCreateView,
|
|
||||||
):
|
|
||||||
"""Create new Stage"""
|
|
||||||
|
|
||||||
model = Stage
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
permission_required = "authentik_flows.add_stage"
|
|
||||||
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created Stage")
|
|
||||||
|
|
||||||
|
|
||||||
class StageUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
InheritanceUpdateView,
|
|
||||||
):
|
|
||||||
"""Update stage"""
|
|
||||||
|
|
||||||
model = Stage
|
|
||||||
permission_required = "authentik_flows.update_application"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated Stage")
|
|
||||||
|
|
||||||
|
|
||||||
class StageDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
|
||||||
"""Delete stage"""
|
|
||||||
|
|
||||||
model = Stage
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
permission_required = "authentik_flows.delete_stage"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Stage")
|
|
@ -1,79 +0,0 @@
|
|||||||
"""authentik StageBinding administration"""
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.db.models import Max
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.flows.forms import FlowStageBindingForm
|
|
||||||
from authentik.flows.models import Flow, FlowStageBinding
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class StageBindingCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new StageBinding"""
|
|
||||||
|
|
||||||
model = FlowStageBinding
|
|
||||||
permission_required = "authentik_flows.add_flowstagebinding"
|
|
||||||
form_class = FlowStageBindingForm
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created StageBinding")
|
|
||||||
|
|
||||||
def get_initial(self) -> dict[str, Any]:
|
|
||||||
if "target" in self.request.GET:
|
|
||||||
initial_target_pk = self.request.GET["target"]
|
|
||||||
targets = Flow.objects.filter(pk=initial_target_pk).select_subclasses()
|
|
||||||
if not targets.exists():
|
|
||||||
return {}
|
|
||||||
max_order = FlowStageBinding.objects.filter(
|
|
||||||
target=targets.first()
|
|
||||||
).aggregate(Max("order"))["order__max"]
|
|
||||||
if not isinstance(max_order, int):
|
|
||||||
max_order = -1
|
|
||||||
return {"target": targets.first(), "order": max_order + 1}
|
|
||||||
return super().get_initial()
|
|
||||||
|
|
||||||
|
|
||||||
class StageBindingUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update FlowStageBinding"""
|
|
||||||
|
|
||||||
model = FlowStageBinding
|
|
||||||
permission_required = "authentik_flows.change_flowstagebinding"
|
|
||||||
form_class = FlowStageBindingForm
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated StageBinding")
|
|
||||||
|
|
||||||
|
|
||||||
class StageBindingDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete FlowStageBinding"""
|
|
||||||
|
|
||||||
model = FlowStageBinding
|
|
||||||
permission_required = "authentik_flows.delete_flowstagebinding"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted FlowStageBinding")
|
|
@ -1,51 +0,0 @@
|
|||||||
"""authentik Invitation administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
from authentik.stages.invitation.forms import InvitationForm
|
|
||||||
from authentik.stages.invitation.models import Invitation
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Invitation"""
|
|
||||||
|
|
||||||
model = Invitation
|
|
||||||
form_class = InvitationForm
|
|
||||||
permission_required = "authentik_stages_invitation.add_invitation"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created Invitation")
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
obj = form.save(commit=False)
|
|
||||||
obj.created_by = self.request.user
|
|
||||||
obj.save()
|
|
||||||
return HttpResponseRedirect(self.success_url)
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete invitation"""
|
|
||||||
|
|
||||||
model = Invitation
|
|
||||||
permission_required = "authentik_stages_invitation.delete_invitation"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Invitation")
|
|
@ -1,60 +0,0 @@
|
|||||||
"""authentik Prompt administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
from authentik.stages.prompt.forms import PromptAdminForm
|
|
||||||
from authentik.stages.prompt.models import Prompt
|
|
||||||
|
|
||||||
|
|
||||||
class PromptCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Prompt"""
|
|
||||||
|
|
||||||
model = Prompt
|
|
||||||
form_class = PromptAdminForm
|
|
||||||
permission_required = "authentik_stages_prompt.add_prompt"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created Prompt")
|
|
||||||
|
|
||||||
|
|
||||||
class PromptUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update prompt"""
|
|
||||||
|
|
||||||
model = Prompt
|
|
||||||
form_class = PromptAdminForm
|
|
||||||
permission_required = "authentik_stages_prompt.change_prompt"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated Prompt")
|
|
||||||
|
|
||||||
|
|
||||||
class PromptDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
|
||||||
"""Delete prompt"""
|
|
||||||
|
|
||||||
model = Prompt
|
|
||||||
permission_required = "authentik_stages_prompt.delete_prompt"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Prompt")
|
|
@ -1,19 +0,0 @@
|
|||||||
"""authentik Token administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.core.models import Token
|
|
||||||
|
|
||||||
|
|
||||||
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
|
||||||
"""Delete token"""
|
|
||||||
|
|
||||||
model = Token
|
|
||||||
permission_required = "authentik_core.delete_token"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Token")
|
|
@ -1,131 +0,0 @@
|
|||||||
"""authentik User administration"""
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.http.response import HttpResponseRedirect
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.http import urlencode
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import DetailView, UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.forms.users import UserForm
|
|
||||||
from authentik.admin.views.utils import DeleteMessageView
|
|
||||||
from authentik.core.models import Token, User
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class UserCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create user"""
|
|
||||||
|
|
||||||
model = User
|
|
||||||
form_class = UserForm
|
|
||||||
permission_required = "authentik_core.add_user"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created User")
|
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update user"""
|
|
||||||
|
|
||||||
model = User
|
|
||||||
form_class = UserForm
|
|
||||||
permission_required = "authentik_core.change_user"
|
|
||||||
|
|
||||||
# By default the object's name is user which is used by other checks
|
|
||||||
context_object_name = "object"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated User")
|
|
||||||
|
|
||||||
|
|
||||||
class UserDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
|
||||||
"""Delete user"""
|
|
||||||
|
|
||||||
model = User
|
|
||||||
permission_required = "authentik_core.delete_user"
|
|
||||||
|
|
||||||
# By default the object's name is user which is used by other checks
|
|
||||||
context_object_name = "object"
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted User")
|
|
||||||
|
|
||||||
|
|
||||||
class UserDisableView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
|
||||||
"""Disable user"""
|
|
||||||
|
|
||||||
object: User
|
|
||||||
|
|
||||||
model = User
|
|
||||||
permission_required = "authentik_core.update_user"
|
|
||||||
|
|
||||||
# By default the object's name is user which is used by other checks
|
|
||||||
context_object_name = "object"
|
|
||||||
template_name = "administration/user/disable.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully disabled User")
|
|
||||||
|
|
||||||
def delete(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
self.object: User = self.get_object()
|
|
||||||
success_url = self.get_success_url()
|
|
||||||
self.object.is_active = False
|
|
||||||
self.object.save()
|
|
||||||
return HttpResponseRedirect(success_url)
|
|
||||||
|
|
||||||
|
|
||||||
class UserEnableView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
||||||
"""Enable user"""
|
|
||||||
|
|
||||||
object: User
|
|
||||||
|
|
||||||
model = User
|
|
||||||
permission_required = "authentik_core.update_user"
|
|
||||||
|
|
||||||
# By default the object's name is user which is used by other checks
|
|
||||||
context_object_name = "object"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully enabled User")
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs):
|
|
||||||
self.object: User = self.get_object()
|
|
||||||
self.object.is_active = True
|
|
||||||
self.object.save()
|
|
||||||
return HttpResponseRedirect(self.success_url)
|
|
||||||
|
|
||||||
|
|
||||||
class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
||||||
"""Get Password reset link for user"""
|
|
||||||
|
|
||||||
model = User
|
|
||||||
permission_required = "authentik_core.reset_user_password"
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
"""Create token for user and return link"""
|
|
||||||
super().get(request, *args, **kwargs)
|
|
||||||
token, __ = Token.objects.get_or_create(
|
|
||||||
identifier="password-reset-temp", user=self.object
|
|
||||||
)
|
|
||||||
querystring = urlencode({"token": token.key})
|
|
||||||
link = request.build_absolute_uri(
|
|
||||||
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
|
|
||||||
)
|
|
||||||
messages.success(request, _("Password reset link: %(link)s" % {"link": link}))
|
|
||||||
return redirect("/")
|
|
@ -1,63 +0,0 @@
|
|||||||
"""authentik admin util views"""
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http import Http404
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.views.generic import DeleteView, UpdateView
|
|
||||||
|
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteMessageView(SuccessMessageMixin, DeleteView):
|
|
||||||
"""DeleteView which shows `self.success_message` on successful deletion"""
|
|
||||||
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
|
|
||||||
def delete(self, request, *args, **kwargs):
|
|
||||||
messages.success(self.request, self.success_message)
|
|
||||||
return super().delete(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class InheritanceCreateView(CreateAssignPermView):
|
|
||||||
"""CreateView for objects using InheritanceManager"""
|
|
||||||
|
|
||||||
def get_form_class(self):
|
|
||||||
provider_type = self.request.GET.get("type")
|
|
||||||
try:
|
|
||||||
model = next(
|
|
||||||
x for x in all_subclasses(self.model) if x.__name__ == provider_type
|
|
||||||
)
|
|
||||||
except StopIteration as exc:
|
|
||||||
raise Http404 from exc
|
|
||||||
return model().form
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
kwargs = super().get_context_data(**kwargs)
|
|
||||||
form_cls = self.get_form_class()
|
|
||||||
if hasattr(form_cls, "template_name"):
|
|
||||||
kwargs["base_template"] = form_cls.template_name
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
class InheritanceUpdateView(UpdateView):
|
|
||||||
"""UpdateView for objects using InheritanceManager"""
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
kwargs = super().get_context_data(**kwargs)
|
|
||||||
form_cls = self.get_form_class()
|
|
||||||
if hasattr(form_cls, "template_name"):
|
|
||||||
kwargs["base_template"] = form_cls.template_name
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def get_form_class(self):
|
|
||||||
return self.get_object().form
|
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
|
||||||
return (
|
|
||||||
self.model.objects.filter(pk=self.kwargs.get("pk"))
|
|
||||||
.select_subclasses()
|
|
||||||
.first()
|
|
||||||
)
|
|
@ -1,9 +1,10 @@
|
|||||||
"""API Authentication"""
|
"""API Authentication"""
|
||||||
from base64 import b64decode
|
from base64 import b64decode, b64encode
|
||||||
from binascii import Error
|
from binascii import Error
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||||
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
@ -12,47 +13,53 @@ from authentik.core.models import Token, TokenIntents, User
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-return-statements
|
||||||
def token_from_header(raw_header: bytes) -> Optional[Token]:
|
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()
|
auth_credentials = raw_header.decode()
|
||||||
# Accept headers with Type format and without
|
if auth_credentials == "":
|
||||||
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):
|
|
||||||
return None
|
return None
|
||||||
# Accept credentials with username and without
|
# Legacy, accept basic auth thats fully encoded (2021.3 outposts)
|
||||||
if ":" in auth_credentials:
|
if " " not in auth_credentials:
|
||||||
_, password = auth_credentials.split(":")
|
try:
|
||||||
else:
|
plain = b64decode(auth_credentials.encode()).decode()
|
||||||
password = auth_credentials
|
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
|
if password == "": # nosec
|
||||||
return None
|
raise AuthenticationFailed("Malformed header")
|
||||||
tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
|
tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
|
||||||
if not tokens.exists():
|
if not tokens.exists():
|
||||||
LOGGER.debug("Token not found")
|
raise AuthenticationFailed("Token invalid/expired")
|
||||||
return None
|
|
||||||
return tokens.first()
|
return tokens.first()
|
||||||
|
|
||||||
|
|
||||||
class AuthentikTokenAuthentication(BaseAuthentication):
|
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]:
|
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)
|
auth = get_authorization_header(request)
|
||||||
|
|
||||||
token = token_from_header(auth)
|
token = token_from_header(auth)
|
||||||
|
# None is only returned when the header isn't set.
|
||||||
if not token:
|
if not token:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return (token.user, None)
|
return (token.user, None)
|
||||||
|
|
||||||
def authenticate_header(self, request: Request) -> str:
|
|
||||||
return 'Basic realm="authentik"'
|
|
||||||
|
32
authentik/api/decorators.py
Normal file
32
authentik/api/decorators.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""API Decorators"""
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
|
||||||
|
def permission_required(
|
||||||
|
perm: Optional[str] = None, other_perms: Optional[list[str]] = None
|
||||||
|
):
|
||||||
|
"""Check permissions for a single custom action"""
|
||||||
|
|
||||||
|
def wrapper_outter(func: Callable):
|
||||||
|
"""Check permissions for a single custom action"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response:
|
||||||
|
if perm:
|
||||||
|
obj = self.get_object()
|
||||||
|
if not request.user.has_perm(perm, obj):
|
||||||
|
return self.permission_denied(request)
|
||||||
|
if other_perms:
|
||||||
|
for other_perm in other_perms:
|
||||||
|
if not request.user.has_perm(other_perm):
|
||||||
|
return self.permission_denied(request)
|
||||||
|
return func(self, request, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return wrapper_outter
|
@ -1,8 +1,8 @@
|
|||||||
"""Swagger Pagination Schema class"""
|
"""Swagger Pagination Schema class"""
|
||||||
from typing import OrderedDict
|
from typing import OrderedDict
|
||||||
|
|
||||||
from drf_yasg2 import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg2.inspectors import PaginatorInspector
|
from drf_yasg.inspectors import PaginatorInspector
|
||||||
|
|
||||||
|
|
||||||
class PaginationInspector(PaginatorInspector):
|
class PaginationInspector(PaginatorInspector):
|
||||||
|
102
authentik/api/schema.py
Normal file
102
authentik/api/schema.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.inspectors.view import SwaggerAutoSchema
|
||||||
|
from drf_yasg.utils import force_real_str, is_list_view
|
||||||
|
from rest_framework import exceptions, status
|
||||||
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorResponseAutoSchema(SwaggerAutoSchema):
|
||||||
|
"""Inspector which includes an error schema"""
|
||||||
|
|
||||||
|
def get_generic_error_schema(self):
|
||||||
|
"""Get a generic error schema"""
|
||||||
|
return openapi.Schema(
|
||||||
|
"Generic API Error",
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={
|
||||||
|
"detail": openapi.Schema(
|
||||||
|
type=openapi.TYPE_STRING, description="Error details"
|
||||||
|
),
|
||||||
|
"code": openapi.Schema(
|
||||||
|
type=openapi.TYPE_STRING, description="Error code"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
required=["detail"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_validation_error_schema(self):
|
||||||
|
"""Get a generic validation error schema"""
|
||||||
|
return openapi.Schema(
|
||||||
|
"Validation Error",
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={
|
||||||
|
api_settings.NON_FIELD_ERRORS_KEY: openapi.Schema(
|
||||||
|
description="List of validation errors not related to any field",
|
||||||
|
type=openapi.TYPE_ARRAY,
|
||||||
|
items=openapi.Schema(type=openapi.TYPE_STRING),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
additional_properties=openapi.Schema(
|
||||||
|
description=(
|
||||||
|
"A list of error messages for each "
|
||||||
|
"field that triggered a validation error"
|
||||||
|
),
|
||||||
|
type=openapi.TYPE_ARRAY,
|
||||||
|
items=openapi.Schema(type=openapi.TYPE_STRING),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_response_serializers(self):
|
||||||
|
responses = super().get_response_serializers()
|
||||||
|
definitions = self.components.with_scope(
|
||||||
|
openapi.SCHEMA_DEFINITIONS
|
||||||
|
) # type: openapi.ReferenceResolver
|
||||||
|
|
||||||
|
definitions.setdefault("GenericError", self.get_generic_error_schema)
|
||||||
|
definitions.setdefault("ValidationError", self.get_validation_error_schema)
|
||||||
|
definitions.setdefault("APIException", self.get_generic_error_schema)
|
||||||
|
|
||||||
|
if self.get_request_serializer() or self.get_query_serializer():
|
||||||
|
responses.setdefault(
|
||||||
|
exceptions.ValidationError.status_code,
|
||||||
|
openapi.Response(
|
||||||
|
description=force_real_str(
|
||||||
|
exceptions.ValidationError.default_detail
|
||||||
|
),
|
||||||
|
schema=openapi.SchemaRef(definitions, "ValidationError"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
security = self.get_security()
|
||||||
|
if security is None or len(security) > 0:
|
||||||
|
# Note: 401 error codes are coerced into 403 see
|
||||||
|
# rest_framework/views.py:433:handle_exception
|
||||||
|
# This is b/c the API uses token auth which doesn't have WWW-Authenticate header
|
||||||
|
responses.setdefault(
|
||||||
|
status.HTTP_403_FORBIDDEN,
|
||||||
|
openapi.Response(
|
||||||
|
description="Authentication credentials were invalid, absent or insufficient.",
|
||||||
|
schema=openapi.SchemaRef(definitions, "GenericError"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not is_list_view(self.path, self.method, self.view):
|
||||||
|
responses.setdefault(
|
||||||
|
exceptions.PermissionDenied.status_code,
|
||||||
|
openapi.Response(
|
||||||
|
description="Permission denied.",
|
||||||
|
schema=openapi.SchemaRef(definitions, "APIException"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
responses.setdefault(
|
||||||
|
exceptions.NotFound.status_code,
|
||||||
|
openapi.Response(
|
||||||
|
description=(
|
||||||
|
"Object does not exist or caller "
|
||||||
|
"has insufficient permissions to access it."
|
||||||
|
),
|
||||||
|
schema=openapi.SchemaRef(definitions, "APIException"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return responses
|
49
authentik/api/templates/api/swagger.html
Normal file
49
authentik/api/templates/api/swagger.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
authentik API Browser
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script type="module" src="{% static 'dist/rapidoc-min.js' %}"></script>
|
||||||
|
{% 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"
|
||||||
|
theme="dark"
|
||||||
|
render-style="view"
|
||||||
|
primary-color="#fd4b2d"
|
||||||
|
allow-spec-url-load="false"
|
||||||
|
allow-spec-file-load="false">
|
||||||
|
<div slot="logo">
|
||||||
|
<img src="{% static 'dist/assets/icons/icon.png' %}" style="width:50px; height:50px" />
|
||||||
|
</div>
|
||||||
|
</rapi-doc>
|
||||||
|
{% endblock %}
|
@ -1,31 +0,0 @@
|
|||||||
{% extends "rest_framework/base.html" %}
|
|
||||||
|
|
||||||
{% block title %}{% if name %}{{ name }} – {% endif %}authentik{% endblock %}
|
|
||||||
|
|
||||||
{% block branding %}
|
|
||||||
<span class='navbar-brand'>
|
|
||||||
authentik
|
|
||||||
</span>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block style %}
|
|
||||||
{{ block.super }}
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background-color: #18191a;
|
|
||||||
color: #fafafa;
|
|
||||||
}
|
|
||||||
.prettyprint {
|
|
||||||
background-color: #1c1e21;
|
|
||||||
color: #fafafa;
|
|
||||||
border: 1px solid #2b2e33;
|
|
||||||
}
|
|
||||||
.pln {
|
|
||||||
color: #fafafa;
|
|
||||||
}
|
|
||||||
.well {
|
|
||||||
background-color: #1c1e21;
|
|
||||||
border: 1px solid #2b2e33;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
@ -3,6 +3,7 @@ from base64 import b64encode
|
|||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
|
||||||
from authentik.api.auth import token_from_header
|
from authentik.api.auth import token_from_header
|
||||||
from authentik.core.models import Token, TokenIntents
|
from authentik.core.models import Token, TokenIntents
|
||||||
@ -11,7 +12,7 @@ from authentik.core.models import Token, TokenIntents
|
|||||||
class TestAPIAuth(TestCase):
|
class TestAPIAuth(TestCase):
|
||||||
"""Test API Authentication"""
|
"""Test API Authentication"""
|
||||||
|
|
||||||
def test_valid(self):
|
def test_valid_basic(self):
|
||||||
"""Test valid token"""
|
"""Test valid token"""
|
||||||
token = Token.objects.create(
|
token = Token.objects.create(
|
||||||
intent=TokenIntents.INTENT_API, user=get_anonymous_user()
|
intent=TokenIntents.INTENT_API, user=get_anonymous_user()
|
||||||
@ -19,19 +20,30 @@ class TestAPIAuth(TestCase):
|
|||||||
auth = b64encode(f":{token.key}".encode()).decode()
|
auth = b64encode(f":{token.key}".encode()).decode()
|
||||||
self.assertEqual(token_from_header(f"Basic {auth}".encode()), token)
|
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):
|
def test_invalid_type(self):
|
||||||
"""Test invalid type"""
|
"""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):
|
def test_invalid_decode(self):
|
||||||
"""Test invalid bas64"""
|
"""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):
|
def test_invalid_empty_password(self):
|
||||||
"""Test invalid with empty password"""
|
"""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):
|
def test_invalid_no_token(self):
|
||||||
"""Test invalid with no token"""
|
"""Test invalid with no token"""
|
||||||
auth = b64encode(":abc".encode()).decode()
|
with self.assertRaises(AuthenticationFailed):
|
||||||
self.assertIsNone(token_from_header(f"Basic :{auth}".encode()))
|
auth = b64encode(":abc".encode()).decode()
|
||||||
|
self.assertIsNone(token_from_header(f"Basic :{auth}".encode()))
|
24
authentik/api/tests/test_swagger.py
Normal file
24
authentik/api/tests/test_swagger.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""Swagger generation tests"""
|
||||||
|
from json import loads
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from yaml import safe_load
|
||||||
|
|
||||||
|
|
||||||
|
class TestSwaggerGeneration(APITestCase):
|
||||||
|
"""Generic admin tests"""
|
||||||
|
|
||||||
|
def test_yaml(self):
|
||||||
|
"""Test YAML generation"""
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:schema-json", kwargs={"format": ".yaml"}),
|
||||||
|
)
|
||||||
|
self.assertTrue(safe_load(response.content.decode()))
|
||||||
|
|
||||||
|
def test_json(self):
|
||||||
|
"""Test JSON generation"""
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:schema-json", kwargs={"format": ".json"}),
|
||||||
|
)
|
||||||
|
self.assertTrue(loads(response.content.decode()))
|
@ -1,32 +1,33 @@
|
|||||||
"""core Configs API"""
|
"""core Configs API"""
|
||||||
from django.db.models import Model
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from rest_framework.fields import BooleanField, CharField, ListField
|
||||||
from rest_framework.fields import BooleanField, CharField
|
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Serializer
|
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
class ConfigSerializer(Serializer):
|
class FooterLinkSerializer(PassiveSerializer):
|
||||||
|
"""Links returned in Config API"""
|
||||||
|
|
||||||
|
href = CharField(read_only=True)
|
||||||
|
name = CharField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigSerializer(PassiveSerializer):
|
||||||
"""Serialize authentik Config into DRF Object"""
|
"""Serialize authentik Config into DRF Object"""
|
||||||
|
|
||||||
branding_logo = CharField(read_only=True)
|
branding_logo = CharField(read_only=True)
|
||||||
branding_title = CharField(read_only=True)
|
branding_title = CharField(read_only=True)
|
||||||
|
ui_footer_links = ListField(child=FooterLinkSerializer(), read_only=True)
|
||||||
|
|
||||||
error_reporting_enabled = BooleanField(read_only=True)
|
error_reporting_enabled = BooleanField(read_only=True)
|
||||||
error_reporting_environment = CharField(read_only=True)
|
error_reporting_environment = CharField(read_only=True)
|
||||||
error_reporting_send_pii = BooleanField(read_only=True)
|
error_reporting_send_pii = BooleanField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigsViewSet(ViewSet):
|
class ConfigsViewSet(ViewSet):
|
||||||
"""Read-only view set that returns the current session's Configs"""
|
"""Read-only view set that returns the current session's Configs"""
|
||||||
@ -43,6 +44,7 @@ class ConfigsViewSet(ViewSet):
|
|||||||
"error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
|
"error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
|
||||||
"error_reporting_environment": CONFIG.y("error_reporting.environment"),
|
"error_reporting_environment": CONFIG.y("error_reporting.environment"),
|
||||||
"error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"),
|
"error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"),
|
||||||
|
"ui_footer_links": CONFIG.y("authentik.footer_links"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return Response(config.data)
|
return Response(config.data)
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
"""api v2 urls"""
|
"""api v2 urls"""
|
||||||
from django.conf import settings
|
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
from drf_yasg2 import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg2.views import get_schema_view
|
from drf_yasg.views import get_schema_view
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
|
from authentik.admin.api.meta import AppsViewSet
|
||||||
from authentik.admin.api.metrics import AdministrationMetricsViewSet
|
from authentik.admin.api.metrics import AdministrationMetricsViewSet
|
||||||
from authentik.admin.api.tasks import TaskViewSet
|
from authentik.admin.api.tasks import TaskViewSet
|
||||||
from authentik.admin.api.version import VersionViewSet
|
from authentik.admin.api.version import VersionViewSet
|
||||||
from authentik.admin.api.workers import WorkerViewSet
|
from authentik.admin.api.workers import WorkerViewSet
|
||||||
from authentik.api.v2.config import ConfigsViewSet
|
from authentik.api.v2.config import ConfigsViewSet
|
||||||
|
from authentik.api.views import SwaggerView
|
||||||
from authentik.core.api.applications import ApplicationViewSet
|
from authentik.core.api.applications import ApplicationViewSet
|
||||||
from authentik.core.api.groups import GroupViewSet
|
from authentik.core.api.groups import GroupViewSet
|
||||||
from authentik.core.api.propertymappings import PropertyMappingViewSet
|
from authentik.core.api.propertymappings import PropertyMappingViewSet
|
||||||
@ -33,12 +34,12 @@ from authentik.outposts.api.outpost_service_connections import (
|
|||||||
ServiceConnectionViewSet,
|
ServiceConnectionViewSet,
|
||||||
)
|
)
|
||||||
from authentik.outposts.api.outposts import OutpostViewSet
|
from authentik.outposts.api.outposts import OutpostViewSet
|
||||||
from authentik.policies.api import PolicyBindingViewSet, PolicyViewSet
|
from authentik.policies.api.bindings import PolicyBindingViewSet
|
||||||
|
from authentik.policies.api.policies import PolicyViewSet
|
||||||
from authentik.policies.dummy.api import DummyPolicyViewSet
|
from authentik.policies.dummy.api import DummyPolicyViewSet
|
||||||
from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet
|
from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet
|
||||||
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
|
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||||
from authentik.policies.expression.api import ExpressionPolicyViewSet
|
from authentik.policies.expression.api import ExpressionPolicyViewSet
|
||||||
from authentik.policies.group_membership.api import GroupMembershipPolicyViewSet
|
|
||||||
from authentik.policies.hibp.api import HaveIBeenPwendPolicyViewSet
|
from authentik.policies.hibp.api import HaveIBeenPwendPolicyViewSet
|
||||||
from authentik.policies.password.api import PasswordPolicyViewSet
|
from authentik.policies.password.api import PasswordPolicyViewSet
|
||||||
from authentik.policies.reputation.api import (
|
from authentik.policies.reputation.api import (
|
||||||
@ -46,14 +47,22 @@ from authentik.policies.reputation.api import (
|
|||||||
ReputationPolicyViewSet,
|
ReputationPolicyViewSet,
|
||||||
UserReputationViewSet,
|
UserReputationViewSet,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.api import OAuth2ProviderViewSet, ScopeMappingViewSet
|
from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet
|
||||||
|
from authentik.providers.oauth2.api.scope import ScopeMappingViewSet
|
||||||
|
from authentik.providers.oauth2.api.tokens import (
|
||||||
|
AuthorizationCodeViewSet,
|
||||||
|
RefreshTokenViewSet,
|
||||||
|
)
|
||||||
from authentik.providers.proxy.api import (
|
from authentik.providers.proxy.api import (
|
||||||
ProxyOutpostConfigViewSet,
|
ProxyOutpostConfigViewSet,
|
||||||
ProxyProviderViewSet,
|
ProxyProviderViewSet,
|
||||||
)
|
)
|
||||||
from authentik.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
|
from authentik.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
|
||||||
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
||||||
from authentik.sources.oauth.api import OAuthSourceViewSet
|
from authentik.sources.oauth.api.source import OAuthSourceViewSet
|
||||||
|
from authentik.sources.oauth.api.source_connection import (
|
||||||
|
UserOAuthSourceConnectionViewSet,
|
||||||
|
)
|
||||||
from authentik.sources.saml.api import SAMLSourceViewSet
|
from authentik.sources.saml.api import SAMLSourceViewSet
|
||||||
from authentik.stages.authenticator_static.api import (
|
from authentik.stages.authenticator_static.api import (
|
||||||
AuthenticatorStaticStageViewSet,
|
AuthenticatorStaticStageViewSet,
|
||||||
@ -74,7 +83,7 @@ from authentik.stages.authenticator_webauthn.api import (
|
|||||||
WebAuthnDeviceViewSet,
|
WebAuthnDeviceViewSet,
|
||||||
)
|
)
|
||||||
from authentik.stages.captcha.api import CaptchaStageViewSet
|
from authentik.stages.captcha.api import CaptchaStageViewSet
|
||||||
from authentik.stages.consent.api import ConsentStageViewSet
|
from authentik.stages.consent.api import ConsentStageViewSet, UserConsentViewSet
|
||||||
from authentik.stages.deny.api import DenyStageViewSet
|
from authentik.stages.deny.api import DenyStageViewSet
|
||||||
from authentik.stages.dummy.api import DummyStageViewSet
|
from authentik.stages.dummy.api import DummyStageViewSet
|
||||||
from authentik.stages.email.api import EmailStageViewSet
|
from authentik.stages.email.api import EmailStageViewSet
|
||||||
@ -95,13 +104,16 @@ router.register("admin/version", VersionViewSet, basename="admin_version")
|
|||||||
router.register("admin/workers", WorkerViewSet, basename="admin_workers")
|
router.register("admin/workers", WorkerViewSet, basename="admin_workers")
|
||||||
router.register("admin/metrics", AdministrationMetricsViewSet, basename="admin_metrics")
|
router.register("admin/metrics", AdministrationMetricsViewSet, basename="admin_metrics")
|
||||||
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
|
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
|
||||||
|
router.register("admin/apps", AppsViewSet, basename="apps")
|
||||||
|
|
||||||
router.register("core/applications", ApplicationViewSet)
|
router.register("core/applications", ApplicationViewSet)
|
||||||
router.register("core/groups", GroupViewSet)
|
router.register("core/groups", GroupViewSet)
|
||||||
router.register("core/users", UserViewSet)
|
router.register("core/users", UserViewSet)
|
||||||
|
router.register("core/user_consent", UserConsentViewSet)
|
||||||
router.register("core/tokens", TokenViewSet)
|
router.register("core/tokens", TokenViewSet)
|
||||||
|
|
||||||
router.register("outposts/outposts", OutpostViewSet)
|
router.register("outposts/outposts", OutpostViewSet)
|
||||||
|
router.register("outposts/instances", OutpostViewSet)
|
||||||
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
|
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
|
||||||
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
|
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
|
||||||
router.register(
|
router.register(
|
||||||
@ -120,6 +132,7 @@ router.register("events/transports", NotificationTransportViewSet)
|
|||||||
router.register("events/rules", NotificationRuleViewSet)
|
router.register("events/rules", NotificationRuleViewSet)
|
||||||
|
|
||||||
router.register("sources/all", SourceViewSet)
|
router.register("sources/all", SourceViewSet)
|
||||||
|
router.register("sources/oauth_user_connections", UserOAuthSourceConnectionViewSet)
|
||||||
router.register("sources/ldap", LDAPSourceViewSet)
|
router.register("sources/ldap", LDAPSourceViewSet)
|
||||||
router.register("sources/saml", SAMLSourceViewSet)
|
router.register("sources/saml", SAMLSourceViewSet)
|
||||||
router.register("sources/oauth", OAuthSourceViewSet)
|
router.register("sources/oauth", OAuthSourceViewSet)
|
||||||
@ -128,7 +141,6 @@ router.register("policies/all", PolicyViewSet)
|
|||||||
router.register("policies/bindings", PolicyBindingViewSet)
|
router.register("policies/bindings", PolicyBindingViewSet)
|
||||||
router.register("policies/expression", ExpressionPolicyViewSet)
|
router.register("policies/expression", ExpressionPolicyViewSet)
|
||||||
router.register("policies/event_matcher", EventMatcherPolicyViewSet)
|
router.register("policies/event_matcher", EventMatcherPolicyViewSet)
|
||||||
router.register("policies/group_membership", GroupMembershipPolicyViewSet)
|
|
||||||
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
|
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
|
||||||
router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)
|
router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)
|
||||||
router.register("policies/password", PasswordPolicyViewSet)
|
router.register("policies/password", PasswordPolicyViewSet)
|
||||||
@ -141,6 +153,9 @@ router.register("providers/proxy", ProxyProviderViewSet)
|
|||||||
router.register("providers/oauth2", OAuth2ProviderViewSet)
|
router.register("providers/oauth2", OAuth2ProviderViewSet)
|
||||||
router.register("providers/saml", SAMLProviderViewSet)
|
router.register("providers/saml", SAMLProviderViewSet)
|
||||||
|
|
||||||
|
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
|
||||||
|
router.register("oauth2/refresh_tokens", RefreshTokenViewSet)
|
||||||
|
|
||||||
router.register("propertymappings/all", PropertyMappingViewSet)
|
router.register("propertymappings/all", PropertyMappingViewSet)
|
||||||
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
||||||
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||||
@ -178,32 +193,30 @@ router.register("policies/dummy", DummyPolicyViewSet)
|
|||||||
|
|
||||||
info = openapi.Info(
|
info = openapi.Info(
|
||||||
title="authentik API",
|
title="authentik API",
|
||||||
default_version="v2",
|
default_version="v2beta",
|
||||||
contact=openapi.Contact(email="hello@beryju.org"),
|
contact=openapi.Contact(email="hello@beryju.org"),
|
||||||
license=openapi.License(
|
license=openapi.License(
|
||||||
name="GNU GPLv3", url="https://github.com/BeryJu/authentik/blob/master/LICENSE"
|
name="GNU GPLv3",
|
||||||
|
url="https://github.com/goauthentik/authentik/blob/master/LICENSE",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
SchemaView = get_schema_view(info, public=True, permission_classes=(AllowAny,))
|
SchemaView = get_schema_view(info, public=True, permission_classes=(AllowAny,))
|
||||||
|
|
||||||
urlpatterns = router.urls + [
|
urlpatterns = (
|
||||||
path(
|
[
|
||||||
"flows/executor/<slug:flow_slug>/",
|
path("", SwaggerView.as_view(), name="swagger"),
|
||||||
FlowExecutorView.as_view(),
|
]
|
||||||
name="flow-executor",
|
+ router.urls
|
||||||
),
|
+ [
|
||||||
re_path(
|
|
||||||
r"^swagger(?P<format>\.json|\.yaml)$",
|
|
||||||
SchemaView.without_ui(cache_timeout=0),
|
|
||||||
name="schema-json",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
|
||||||
urlpatterns = urlpatterns + [
|
|
||||||
path(
|
path(
|
||||||
"swagger/",
|
"flows/executor/<slug:flow_slug>/",
|
||||||
SchemaView.with_ui("swagger", cache_timeout=0),
|
FlowExecutorView.as_view(),
|
||||||
name="schema-swagger-ui",
|
name="flow-executor",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^swagger(?P<format>\.json|\.yaml)$",
|
||||||
|
SchemaView.without_ui(cache_timeout=0),
|
||||||
|
name="schema-json",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
22
authentik/api/views.py
Normal file
22
authentik/api/views.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""General API Views"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
|
|
||||||
|
class SwaggerView(TemplateView):
|
||||||
|
"""Show swagger view based on rapi-doc"""
|
||||||
|
|
||||||
|
template_name = "api/swagger.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
path = self.request.build_absolute_uri(
|
||||||
|
reverse(
|
||||||
|
"authentik_api:schema-json",
|
||||||
|
kwargs={
|
||||||
|
"format": ".json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return super().get_context_data(path=path, **kwargs)
|
@ -1,20 +0,0 @@
|
|||||||
"""authentik core admin"""
|
|
||||||
|
|
||||||
from django.apps import AppConfig, apps
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.admin.sites import AlreadyRegistered
|
|
||||||
from guardian.admin import GuardedModelAdmin
|
|
||||||
|
|
||||||
|
|
||||||
def admin_autoregister(app: AppConfig):
|
|
||||||
"""Automatically register all models from app"""
|
|
||||||
for model in app.get_models():
|
|
||||||
try:
|
|
||||||
admin.site.register(model, GuardedModelAdmin)
|
|
||||||
except AlreadyRegistered:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
for _app in apps.get_app_configs():
|
|
||||||
if _app.label.startswith("authentik_"):
|
|
||||||
admin_autoregister(_app)
|
|
@ -1,12 +1,14 @@
|
|||||||
"""Application API Views"""
|
"""Application API Views"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.http.response import Http404
|
from django.http.response import HttpResponseBadRequest
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg import openapi
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from drf_yasg.utils import no_body, swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
@ -15,6 +17,7 @@ from rest_framework_guardian.filters import ObjectPermissionsFilter
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||||
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import EventAction
|
||||||
@ -32,11 +35,11 @@ class ApplicationSerializer(ModelSerializer):
|
|||||||
"""Application Serializer"""
|
"""Application Serializer"""
|
||||||
|
|
||||||
launch_url = SerializerMethodField()
|
launch_url = SerializerMethodField()
|
||||||
provider = ProviderSerializer(source="get_provider", required=False)
|
provider_obj = ProviderSerializer(source="get_provider", required=False)
|
||||||
|
|
||||||
def get_launch_url(self, instance: Application) -> str:
|
def get_launch_url(self, instance: Application) -> Optional[str]:
|
||||||
"""Get generated launch URL"""
|
"""Get generated launch URL"""
|
||||||
return instance.get_launch_url() or ""
|
return instance.get_launch_url()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -46,12 +49,13 @@ class ApplicationSerializer(ModelSerializer):
|
|||||||
"name",
|
"name",
|
||||||
"slug",
|
"slug",
|
||||||
"provider",
|
"provider",
|
||||||
|
"provider_obj",
|
||||||
"launch_url",
|
"launch_url",
|
||||||
"meta_launch_url",
|
"meta_launch_url",
|
||||||
"meta_icon",
|
"meta_icon",
|
||||||
"meta_description",
|
"meta_description",
|
||||||
"meta_publisher",
|
"meta_publisher",
|
||||||
"policies",
|
"policy_engine_mode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -87,6 +91,15 @@ class ApplicationViewSet(ModelViewSet):
|
|||||||
applications.append(application)
|
applications.append(application)
|
||||||
return applications
|
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:
|
def list(self, request: Request) -> Response:
|
||||||
"""Custom list method that checks Policy based access instead of guardian"""
|
"""Custom list method that checks Policy based access instead of guardian"""
|
||||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||||
@ -94,6 +107,13 @@ class ApplicationViewSet(ModelViewSet):
|
|||||||
|
|
||||||
should_cache = request.GET.get("search", "") == ""
|
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 = []
|
allowed_applications = []
|
||||||
if not should_cache:
|
if not should_cache:
|
||||||
allowed_applications = self._get_allowed_applications(queryset)
|
allowed_applications = self._get_allowed_applications(queryset)
|
||||||
@ -110,16 +130,46 @@ class ApplicationViewSet(ModelViewSet):
|
|||||||
serializer = self.get_serializer(allowed_applications, many=True)
|
serializer = self.get_serializer(allowed_applications, many=True)
|
||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@permission_required("authentik_core.change_application")
|
||||||
|
@swagger_auto_schema(
|
||||||
|
request_body=no_body,
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(
|
||||||
|
name="file",
|
||||||
|
in_=openapi.IN_FORM,
|
||||||
|
type=openapi.TYPE_FILE,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={200: "Success", 400: "Bad request"},
|
||||||
|
)
|
||||||
|
@action(
|
||||||
|
detail=True,
|
||||||
|
pagination_class=None,
|
||||||
|
filter_backends=[],
|
||||||
|
methods=["POST"],
|
||||||
|
parser_classes=(MultiPartParser,),
|
||||||
|
)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def set_icon(self, request: Request, slug: str):
|
||||||
|
"""Set application icon"""
|
||||||
|
app: Application = self.get_object()
|
||||||
|
icon = request.FILES.get("file", None)
|
||||||
|
if not icon:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
app.meta_icon = icon
|
||||||
|
app.save()
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
@permission_required(
|
||||||
|
"authentik_core.view_application", ["authentik_events.view_event"]
|
||||||
|
)
|
||||||
@swagger_auto_schema(responses={200: CoordinateSerializer(many=True)})
|
@swagger_auto_schema(responses={200: CoordinateSerializer(many=True)})
|
||||||
@action(detail=True)
|
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||||
|
# pylint: disable=unused-argument
|
||||||
def metrics(self, request: Request, slug: str):
|
def metrics(self, request: Request, slug: str):
|
||||||
"""Metrics for application logins"""
|
"""Metrics for application logins"""
|
||||||
app = get_object_or_404(
|
app = self.get_object()
|
||||||
get_objects_for_user(request.user, "authentik_core.view_application"),
|
|
||||||
slug=slug,
|
|
||||||
)
|
|
||||||
if not request.user.has_perm("authentik_events.view_event"):
|
|
||||||
raise Http404
|
|
||||||
return Response(
|
return Response(
|
||||||
get_events_per_1h(
|
get_events_per_1h(
|
||||||
action=EventAction.AUTHORIZE_APPLICATION,
|
action=EventAction.AUTHORIZE_APPLICATION,
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
"""Groups API Viewset"""
|
"""Groups API Viewset"""
|
||||||
|
from rest_framework.fields import JSONField
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import is_dict
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group
|
||||||
|
|
||||||
|
|
||||||
class GroupSerializer(ModelSerializer):
|
class GroupSerializer(ModelSerializer):
|
||||||
"""Group Serializer"""
|
"""Group Serializer"""
|
||||||
|
|
||||||
|
attributes = JSONField(validators=[is_dict], required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
|
@ -1,47 +1,73 @@
|
|||||||
"""PropertyMapping API Views"""
|
"""PropertyMapping API Views"""
|
||||||
from django.urls import reverse
|
from json import dumps
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
|
||||||
|
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
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework.fields import BooleanField, CharField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
from authentik.api.decorators import permission_required
|
||||||
|
from authentik.core.api.utils import (
|
||||||
|
MetaNameSerializer,
|
||||||
|
PassiveSerializer,
|
||||||
|
TypeCreateSerializer,
|
||||||
|
)
|
||||||
|
from authentik.core.expression import PropertyMappingEvaluator
|
||||||
from authentik.core.models import PropertyMapping
|
from authentik.core.models import PropertyMapping
|
||||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
|
from authentik.managed.api import ManagedSerializer
|
||||||
|
from authentik.policies.api.exec import PolicyTestSerializer
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingSerializer(ModelSerializer, MetaNameSerializer):
|
class PropertyMappingTestResultSerializer(PassiveSerializer):
|
||||||
|
"""Result of a Property-mapping test"""
|
||||||
|
|
||||||
|
result = CharField(read_only=True)
|
||||||
|
successful = BooleanField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSerializer):
|
||||||
"""PropertyMapping Serializer"""
|
"""PropertyMapping Serializer"""
|
||||||
|
|
||||||
object_type = SerializerMethodField(method_name="get_type")
|
component = SerializerMethodField()
|
||||||
|
|
||||||
def get_type(self, obj):
|
def get_component(self, obj: PropertyMapping) -> str:
|
||||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
"""Get object's component so that we know how to edit the object"""
|
||||||
return obj._meta.object_name.lower().replace("propertymapping", "")
|
return obj.component
|
||||||
|
|
||||||
def to_representation(self, instance: PropertyMapping):
|
def validate_expression(self, expression: str) -> str:
|
||||||
# pyright: reportGeneralTypeIssues=false
|
"""Test Syntax"""
|
||||||
if instance.__class__ == PropertyMapping:
|
evaluator = PropertyMappingEvaluator()
|
||||||
return super().to_representation(instance)
|
evaluator.validate(expression)
|
||||||
return instance.serializer(instance=instance).data
|
return expression
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = PropertyMapping
|
model = PropertyMapping
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
"managed",
|
||||||
"name",
|
"name",
|
||||||
"expression",
|
"expression",
|
||||||
"object_type",
|
"component",
|
||||||
"verbose_name",
|
"verbose_name",
|
||||||
"verbose_name_plural",
|
"verbose_name_plural",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingViewSet(ReadOnlyModelViewSet):
|
class PropertyMappingViewSet(
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
GenericViewSet,
|
||||||
|
):
|
||||||
"""PropertyMapping Viewset"""
|
"""PropertyMapping Viewset"""
|
||||||
|
|
||||||
queryset = PropertyMapping.objects.none()
|
queryset = PropertyMapping.objects.none()
|
||||||
@ -56,17 +82,65 @@ class PropertyMappingViewSet(ReadOnlyModelViewSet):
|
|||||||
return PropertyMapping.objects.select_subclasses()
|
return PropertyMapping.objects.select_subclasses()
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||||
@action(detail=False)
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
def types(self, request: Request) -> Response:
|
def types(self, request: Request) -> Response:
|
||||||
"""Get all creatable property-mapping types"""
|
"""Get all creatable property-mapping types"""
|
||||||
data = []
|
data = []
|
||||||
for subclass in all_subclasses(self.queryset.model):
|
for subclass in all_subclasses(self.queryset.model):
|
||||||
|
subclass: PropertyMapping
|
||||||
data.append(
|
data.append(
|
||||||
{
|
{
|
||||||
"name": verbose_name(subclass),
|
"name": subclass._meta.verbose_name,
|
||||||
"description": subclass.__doc__,
|
"description": subclass.__doc__,
|
||||||
"link": reverse("authentik_admin:property-mapping-create")
|
# pyright: reportGeneralTypeIssues=false
|
||||||
+ f"?type={subclass.__name__}",
|
"component": subclass().component,
|
||||||
|
"model_name": subclass._meta.model_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return Response(TypeCreateSerializer(data, many=True).data)
|
return Response(TypeCreateSerializer(data, many=True).data)
|
||||||
|
|
||||||
|
@permission_required("authentik_core.view_propertymapping")
|
||||||
|
@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
|
||||||
|
def test(self, request: Request, pk: str) -> Response:
|
||||||
|
"""Test Property Mapping"""
|
||||||
|
mapping: PropertyMapping = self.get_object()
|
||||||
|
test_params = PolicyTestSerializer(data=request.data)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
if not users.exists():
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
response_data = {"successful": True, "result": ""}
|
||||||
|
try:
|
||||||
|
result = mapping.evaluate(
|
||||||
|
users.first(),
|
||||||
|
self.request,
|
||||||
|
**test_params.validated_data.get("context", {}),
|
||||||
|
)
|
||||||
|
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
|
||||||
|
response = PropertyMappingTestResultSerializer(response_data)
|
||||||
|
return Response(response.data)
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
"""Provider API Views"""
|
"""Provider API Views"""
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
|
|
||||||
|
|
||||||
@ -21,11 +20,14 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
assigned_application_slug = ReadOnlyField(source="application.slug")
|
assigned_application_slug = ReadOnlyField(source="application.slug")
|
||||||
assigned_application_name = ReadOnlyField(source="application.name")
|
assigned_application_name = ReadOnlyField(source="application.name")
|
||||||
|
|
||||||
object_type = SerializerMethodField()
|
component = SerializerMethodField()
|
||||||
|
|
||||||
def get_object_type(self, obj):
|
def get_component(self, obj: Provider): # pragma: no cover
|
||||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
"""Get object component so that we know how to edit the object"""
|
||||||
return obj._meta.object_name.lower().replace("provider", "")
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
if obj.__class__ == Provider:
|
||||||
|
return ""
|
||||||
|
return obj.component
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -33,10 +35,9 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
"name",
|
"name",
|
||||||
"application",
|
|
||||||
"authorization_flow",
|
"authorization_flow",
|
||||||
"property_mappings",
|
"property_mappings",
|
||||||
"object_type",
|
"component",
|
||||||
"assigned_application_slug",
|
"assigned_application_slug",
|
||||||
"assigned_application_name",
|
"assigned_application_name",
|
||||||
"verbose_name",
|
"verbose_name",
|
||||||
@ -44,7 +45,12 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProviderViewSet(ModelViewSet):
|
class ProviderViewSet(
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
GenericViewSet,
|
||||||
|
):
|
||||||
"""Provider Viewset"""
|
"""Provider Viewset"""
|
||||||
|
|
||||||
queryset = Provider.objects.none()
|
queryset = Provider.objects.none()
|
||||||
@ -61,24 +67,26 @@ class ProviderViewSet(ModelViewSet):
|
|||||||
return Provider.objects.select_subclasses()
|
return Provider.objects.select_subclasses()
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||||
@action(detail=False)
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
def types(self, request: Request) -> Response:
|
def types(self, request: Request) -> Response:
|
||||||
"""Get all creatable provider types"""
|
"""Get all creatable provider types"""
|
||||||
data = []
|
data = []
|
||||||
for subclass in all_subclasses(self.queryset.model):
|
for subclass in all_subclasses(self.queryset.model):
|
||||||
|
subclass: Provider
|
||||||
data.append(
|
data.append(
|
||||||
{
|
{
|
||||||
"name": verbose_name(subclass),
|
"name": subclass._meta.verbose_name,
|
||||||
"description": subclass.__doc__,
|
"description": subclass.__doc__,
|
||||||
"link": reverse("authentik_admin:provider-create")
|
"component": subclass().component,
|
||||||
+ f"?type={subclass.__name__}",
|
"model_name": subclass._meta.model_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
data.append(
|
data.append(
|
||||||
{
|
{
|
||||||
"name": _("SAML Provider from Metadata"),
|
"name": _("SAML Provider from Metadata"),
|
||||||
"description": _("Create a SAML Provider by importing its Metadata."),
|
"description": _("Create a SAML Provider by importing its Metadata."),
|
||||||
"link": reverse("authentik_admin:provider-saml-from-metadata"),
|
"component": "ak-provider-saml-import-form",
|
||||||
|
"model_name": "",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return Response(TypeCreateSerializer(data, many=True).data)
|
return Response(TypeCreateSerializer(data, many=True).data)
|
||||||
|
@ -1,26 +1,35 @@
|
|||||||
"""Source API Views"""
|
"""Source API Views"""
|
||||||
from django.urls import reverse
|
from typing import Iterable
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||||
from authentik.core.models import Source
|
from authentik.core.models import Source
|
||||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
from authentik.core.types import UserSettingSerializer
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
|
from authentik.policies.engine import PolicyEngine
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||||
"""Source Serializer"""
|
"""Source Serializer"""
|
||||||
|
|
||||||
object_type = SerializerMethodField()
|
component = SerializerMethodField()
|
||||||
|
|
||||||
def get_object_type(self, obj):
|
def get_component(self, obj: Source):
|
||||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
"""Get object component so that we know how to edit the object"""
|
||||||
return obj._meta.object_name.lower().replace("source", "")
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
if obj.__class__ == Source:
|
||||||
|
return ""
|
||||||
|
return obj.component
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -32,13 +41,19 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
"enabled",
|
"enabled",
|
||||||
"authentication_flow",
|
"authentication_flow",
|
||||||
"enrollment_flow",
|
"enrollment_flow",
|
||||||
"object_type",
|
"component",
|
||||||
"verbose_name",
|
"verbose_name",
|
||||||
"verbose_name_plural",
|
"verbose_name_plural",
|
||||||
|
"policy_engine_mode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SourceViewSet(ReadOnlyModelViewSet):
|
class SourceViewSet(
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
GenericViewSet,
|
||||||
|
):
|
||||||
"""Source Viewset"""
|
"""Source Viewset"""
|
||||||
|
|
||||||
queryset = Source.objects.none()
|
queryset = Source.objects.none()
|
||||||
@ -49,17 +64,47 @@ class SourceViewSet(ReadOnlyModelViewSet):
|
|||||||
return Source.objects.select_subclasses()
|
return Source.objects.select_subclasses()
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||||
@action(detail=False)
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
def types(self, request: Request) -> Response:
|
def types(self, request: Request) -> Response:
|
||||||
"""Get all creatable source types"""
|
"""Get all creatable source types"""
|
||||||
data = []
|
data = []
|
||||||
for subclass in all_subclasses(self.queryset.model):
|
for subclass in all_subclasses(self.queryset.model):
|
||||||
|
subclass: Source
|
||||||
|
component = ""
|
||||||
|
if subclass._meta.abstract:
|
||||||
|
component = subclass.__bases__[0]().component
|
||||||
|
else:
|
||||||
|
component = subclass().component
|
||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
data.append(
|
data.append(
|
||||||
{
|
{
|
||||||
"name": verbose_name(subclass),
|
"name": subclass._meta.verbose_name,
|
||||||
"description": subclass.__doc__,
|
"description": subclass.__doc__,
|
||||||
"link": reverse("authentik_admin:source-create")
|
"component": component,
|
||||||
+ f"?type={subclass.__name__}",
|
"model_name": subclass._meta.model_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return Response(TypeCreateSerializer(data, many=True).data)
|
return Response(TypeCreateSerializer(data, many=True).data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(responses={200: UserSettingSerializer(many=True)})
|
||||||
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
|
def user_settings(self, request: Request) -> Response:
|
||||||
|
"""Get all sources the user can configure"""
|
||||||
|
_all_sources: Iterable[Source] = Source.objects.filter(
|
||||||
|
enabled=True
|
||||||
|
).select_subclasses()
|
||||||
|
matching_sources: list[UserSettingSerializer] = []
|
||||||
|
for source in _all_sources:
|
||||||
|
user_settings = source.ui_user_settings
|
||||||
|
if not user_settings:
|
||||||
|
continue
|
||||||
|
policy_engine = PolicyEngine(source, request.user, request)
|
||||||
|
policy_engine.build()
|
||||||
|
if not policy_engine.passing:
|
||||||
|
continue
|
||||||
|
source_settings = source.ui_user_settings
|
||||||
|
source_settings.initial_data["object_uid"] = source.slug
|
||||||
|
if not source_settings.is_valid():
|
||||||
|
LOGGER.warning(source_settings.errors)
|
||||||
|
matching_sources.append(source_settings.validated_data)
|
||||||
|
return Response(matching_sources)
|
||||||
|
@ -1,29 +1,32 @@
|
|||||||
"""Tokens API Viewset"""
|
"""Tokens API Viewset"""
|
||||||
from django.db.models.base import Model
|
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer, Serializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
from authentik.core.models import Token
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
from authentik.core.models import Token, TokenIntents
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.managed.api import ManagedSerializer
|
||||||
|
|
||||||
|
|
||||||
class TokenSerializer(ModelSerializer):
|
class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||||
"""Token Serializer"""
|
"""Token Serializer"""
|
||||||
|
|
||||||
user = UserSerializer()
|
user = UserSerializer(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Token
|
model = Token
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
"managed",
|
||||||
"identifier",
|
"identifier",
|
||||||
"intent",
|
"intent",
|
||||||
"user",
|
"user",
|
||||||
@ -34,17 +37,11 @@ class TokenSerializer(ModelSerializer):
|
|||||||
depth = 2
|
depth = 2
|
||||||
|
|
||||||
|
|
||||||
class TokenViewSerializer(Serializer):
|
class TokenViewSerializer(PassiveSerializer):
|
||||||
"""Show token's current key"""
|
"""Show token's current key"""
|
||||||
|
|
||||||
key = CharField(read_only=True)
|
key = CharField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class TokenViewSet(ModelViewSet):
|
class TokenViewSet(ModelViewSet):
|
||||||
"""Token Viewset"""
|
"""Token Viewset"""
|
||||||
@ -66,8 +63,17 @@ class TokenViewSet(ModelViewSet):
|
|||||||
]
|
]
|
||||||
ordering = ["expires"]
|
ordering = ["expires"]
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: TokenViewSerializer(many=False)})
|
def perform_create(self, serializer: TokenSerializer):
|
||||||
@action(detail=True)
|
serializer.save(user=self.request.user, intent=TokenIntents.INTENT_API)
|
||||||
|
|
||||||
|
@permission_required("authentik_core.view_token_key")
|
||||||
|
@swagger_auto_schema(
|
||||||
|
responses={
|
||||||
|
200: TokenViewSerializer(many=False),
|
||||||
|
404: "Token not found or expired",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def view_key(self, request: Request, identifier: str) -> Response:
|
def view_key(self, request: Request, identifier: str) -> Response:
|
||||||
"""Return token key and log access"""
|
"""Return token key and log access"""
|
||||||
|
@ -1,14 +1,26 @@
|
|||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
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 guardian.utils import get_anonymous_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import BooleanField, ModelSerializer
|
from rest_framework.serializers import BooleanField, ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.models import User
|
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, 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):
|
class UserSerializer(ModelSerializer):
|
||||||
@ -16,6 +28,7 @@ class UserSerializer(ModelSerializer):
|
|||||||
|
|
||||||
is_superuser = BooleanField(read_only=True)
|
is_superuser = BooleanField(read_only=True)
|
||||||
avatar = CharField(read_only=True)
|
avatar = CharField(read_only=True)
|
||||||
|
attributes = JSONField(validators=[is_dict], required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -33,6 +46,44 @@ class UserSerializer(ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SessionUserSerializer(PassiveSerializer):
|
||||||
|
"""Response for the /user/me endpoint, returns the currently active user (as `user` property)
|
||||||
|
and, if this user is being impersonated, the original user in the `original` property."""
|
||||||
|
|
||||||
|
user = UserSerializer()
|
||||||
|
original = UserSerializer(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class UserMetricsSerializer(PassiveSerializer):
|
||||||
|
"""User Metrics"""
|
||||||
|
|
||||||
|
logins_per_1h = SerializerMethodField()
|
||||||
|
logins_failed_per_1h = SerializerMethodField()
|
||||||
|
authorizations_per_1h = SerializerMethodField()
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
|
||||||
|
def get_logins_per_1h(self, _):
|
||||||
|
"""Get successful logins per hour for the last 24 hours"""
|
||||||
|
user = self.context["user"]
|
||||||
|
return get_events_per_1h(action=EventAction.LOGIN, user__pk=user.pk)
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
|
||||||
|
def get_logins_failed_per_1h(self, _):
|
||||||
|
"""Get failed logins per hour for the last 24 hours"""
|
||||||
|
user = self.context["user"]
|
||||||
|
return get_events_per_1h(
|
||||||
|
action=EventAction.LOGIN_FAILED, context__username=user.username
|
||||||
|
)
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
|
||||||
|
def get_authorizations_per_1h(self, _):
|
||||||
|
"""Get failed logins per hour for the last 24 hours"""
|
||||||
|
user = self.context["user"]
|
||||||
|
return get_events_per_1h(
|
||||||
|
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ModelViewSet):
|
class UserViewSet(ModelViewSet):
|
||||||
"""User Viewset"""
|
"""User Viewset"""
|
||||||
|
|
||||||
@ -44,9 +95,52 @@ class UserViewSet(ModelViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: UserSerializer(many=False)})
|
@swagger_auto_schema(responses={200: SessionUserSerializer(many=False)})
|
||||||
@action(detail=False)
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
def me(self, request: Request) -> Response:
|
def me(self, request: Request) -> Response:
|
||||||
"""Get information about current user"""
|
"""Get information about current user"""
|
||||||
return Response(UserSerializer(request.user).data)
|
serializer = SessionUserSerializer(
|
||||||
|
data={"user": UserSerializer(request.user).data}
|
||||||
|
)
|
||||||
|
if SESSION_IMPERSONATE_USER in request._request.session:
|
||||||
|
serializer.initial_data["original"] = UserSerializer(
|
||||||
|
request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
).data
|
||||||
|
serializer.is_valid()
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
||||||
|
@swagger_auto_schema(responses={200: UserMetricsSerializer(many=False)})
|
||||||
|
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||||
|
# pylint: disable=invalid-name, unused-argument
|
||||||
|
def metrics(self, request: Request, pk: int) -> Response:
|
||||||
|
"""User metrics per 1h"""
|
||||||
|
user: User = self.get_object()
|
||||||
|
serializer = UserMetricsSerializer(True)
|
||||||
|
serializer.context["user"] = user
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@permission_required("authentik_core.reset_user_password")
|
||||||
|
@swagger_auto_schema(
|
||||||
|
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",
|
||||||
|
user=user,
|
||||||
|
intent=TokenIntents.INTENT_RECOVERY,
|
||||||
|
)
|
||||||
|
querystring = urlencode({"token": token.key})
|
||||||
|
link = request.build_absolute_uri(
|
||||||
|
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
|
||||||
|
)
|
||||||
|
return Response({"link": link})
|
||||||
|
@ -1,21 +1,38 @@
|
|||||||
"""API Utilities"""
|
"""API Utilities"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from rest_framework.fields import CharField, IntegerField
|
from rest_framework.fields import CharField, IntegerField
|
||||||
from rest_framework.serializers import Serializer, SerializerMethodField
|
from rest_framework.serializers import (
|
||||||
|
Serializer,
|
||||||
|
SerializerMethodField,
|
||||||
|
ValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MetaNameSerializer(Serializer):
|
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):
|
||||||
|
"""Base serializer class which doesn't implement create/update methods"""
|
||||||
|
|
||||||
|
def create(self, validated_data: dict) -> Model:
|
||||||
|
return Model()
|
||||||
|
|
||||||
|
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||||
|
return Model()
|
||||||
|
|
||||||
|
|
||||||
|
class MetaNameSerializer(PassiveSerializer):
|
||||||
"""Add verbose names to response"""
|
"""Add verbose names to response"""
|
||||||
|
|
||||||
verbose_name = SerializerMethodField()
|
verbose_name = SerializerMethodField()
|
||||||
verbose_name_plural = SerializerMethodField()
|
verbose_name_plural = SerializerMethodField()
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def get_verbose_name(self, obj: Model) -> str:
|
def get_verbose_name(self, obj: Model) -> str:
|
||||||
"""Return object's verbose_name"""
|
"""Return object's verbose_name"""
|
||||||
return obj._meta.verbose_name
|
return obj._meta.verbose_name
|
||||||
@ -25,27 +42,22 @@ class MetaNameSerializer(Serializer):
|
|||||||
return obj._meta.verbose_name_plural
|
return obj._meta.verbose_name_plural
|
||||||
|
|
||||||
|
|
||||||
class TypeCreateSerializer(Serializer):
|
class TypeCreateSerializer(PassiveSerializer):
|
||||||
"""Types of an object that can be created"""
|
"""Types of an object that can be created"""
|
||||||
|
|
||||||
name = CharField(required=True)
|
name = CharField(required=True)
|
||||||
description = CharField(required=True)
|
description = CharField(required=True)
|
||||||
link = CharField(required=True)
|
component = CharField(required=True)
|
||||||
|
model_name = CharField(required=True)
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class CacheSerializer(Serializer):
|
class CacheSerializer(PassiveSerializer):
|
||||||
"""Generic cache stats for an object"""
|
"""Generic cache stats for an object"""
|
||||||
|
|
||||||
count = IntegerField(read_only=True)
|
count = IntegerField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class LinkSerializer(PassiveSerializer):
|
||||||
raise NotImplementedError
|
"""Returns a single link"""
|
||||||
|
|
||||||
|
link = CharField()
|
||||||
|
@ -14,3 +14,4 @@ class AuthentikCoreConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import_module("authentik.core.signals")
|
import_module("authentik.core.signals")
|
||||||
|
import_module("authentik.core.managed")
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Channels base classes"""
|
"""Channels base classes"""
|
||||||
from channels.exceptions import DenyConnection
|
from channels.exceptions import DenyConnection
|
||||||
from channels.generic.websocket import JsonWebsocketConsumer
|
from channels.generic.websocket import JsonWebsocketConsumer
|
||||||
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.api.auth import token_from_header
|
from authentik.api.auth import token_from_header
|
||||||
@ -22,9 +23,13 @@ class AuthJsonConsumer(JsonWebsocketConsumer):
|
|||||||
|
|
||||||
raw_header = headers[b"authorization"]
|
raw_header = headers[b"authorization"]
|
||||||
|
|
||||||
token = token_from_header(raw_header)
|
try:
|
||||||
if not token:
|
token = token_from_header(raw_header)
|
||||||
LOGGER.warning("Failed to authenticate")
|
# 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()
|
raise DenyConnection()
|
||||||
|
|
||||||
self.user = token.user
|
self.user = token.user
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
"""authentik Core Application forms"""
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from authentik.core.models import Application, Provider
|
|
||||||
from authentik.lib.widgets import GroupedModelChoiceField
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationForm(forms.ModelForm):
|
|
||||||
"""Application Form"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs): # pragma: no cover
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["provider"].queryset = (
|
|
||||||
Provider.objects.all().order_by("name").select_subclasses()
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = Application
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"slug",
|
|
||||||
"provider",
|
|
||||||
"meta_launch_url",
|
|
||||||
"meta_icon",
|
|
||||||
"meta_description",
|
|
||||||
"meta_publisher",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
"meta_launch_url": forms.TextInput(),
|
|
||||||
"meta_publisher": forms.TextInput(),
|
|
||||||
"meta_icon": forms.FileInput(),
|
|
||||||
}
|
|
||||||
help_texts = {
|
|
||||||
"meta_launch_url": _(
|
|
||||||
(
|
|
||||||
"If left empty, authentik will try to extract the launch URL "
|
|
||||||
"based on the selected provider."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
field_classes = {"provider": GroupedModelChoiceField}
|
|
||||||
labels = {
|
|
||||||
"meta_launch_url": _("Launch URL"),
|
|
||||||
"meta_icon": _("Icon"),
|
|
||||||
"meta_description": _("Description"),
|
|
||||||
"meta_publisher": _("Publisher"),
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
"""authentik Core Group forms"""
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from authentik.admin.fields import CodeMirrorWidget, YAMLField
|
|
||||||
from authentik.core.models import Group, User
|
|
||||||
|
|
||||||
|
|
||||||
class GroupForm(forms.ModelForm):
|
|
||||||
"""Group Form"""
|
|
||||||
|
|
||||||
members = forms.ModelMultipleChoiceField(
|
|
||||||
User.objects.all(),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
if self.instance.pk:
|
|
||||||
self.initial["members"] = self.instance.users.values_list("pk", flat=True)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
instance = super().save(*args, **kwargs)
|
|
||||||
if instance.pk:
|
|
||||||
instance.users.clear()
|
|
||||||
instance.users.add(*self.cleaned_data["members"])
|
|
||||||
return instance
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = Group
|
|
||||||
fields = ["name", "is_superuser", "parent", "members", "attributes"]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
"attributes": CodeMirrorWidget,
|
|
||||||
}
|
|
||||||
field_classes = {
|
|
||||||
"attributes": YAMLField,
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
"""Core user token form"""
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from authentik.core.models import Token
|
|
||||||
|
|
||||||
|
|
||||||
class UserTokenForm(forms.ModelForm):
|
|
||||||
"""Token form, for tokens created by endusers"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = Token
|
|
||||||
fields = [
|
|
||||||
"identifier",
|
|
||||||
"expires",
|
|
||||||
"expiring",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"identifier": forms.TextInput(),
|
|
||||||
"description": forms.TextInput(),
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
"""authentik core user forms"""
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from authentik.core.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserDetailForm(forms.ModelForm):
|
|
||||||
"""Update User Details"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = User
|
|
||||||
fields = ["username", "name", "email"]
|
|
||||||
widgets = {"name": forms.TextInput}
|
|
16
authentik/core/managed.py
Normal file
16
authentik/core/managed.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""Core managed objects"""
|
||||||
|
from authentik.core.models import Source
|
||||||
|
from authentik.managed.manager import EnsureExists, ObjectManager
|
||||||
|
|
||||||
|
|
||||||
|
class CoreManager(ObjectManager):
|
||||||
|
"""Core managed objects"""
|
||||||
|
|
||||||
|
def reconcile(self):
|
||||||
|
return [
|
||||||
|
EnsureExists(
|
||||||
|
Source,
|
||||||
|
"goauthentik.io/sources/inbuilt",
|
||||||
|
name="authentik Built-in",
|
||||||
|
),
|
||||||
|
]
|
@ -1,6 +1,8 @@
|
|||||||
# Generated by Django 3.0.6 on 2020-05-23 16:40
|
# Generated by Django 3.0.6 on 2020-05-23 16:40
|
||||||
|
from os import environ
|
||||||
|
|
||||||
from django.apps.registry import Apps
|
from django.apps.registry import Apps
|
||||||
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
@ -14,7 +16,12 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
||||||
username="akadmin", email="root@localhost", name="authentik Default Admin"
|
username="akadmin", email="root@localhost", name="authentik Default Admin"
|
||||||
)
|
)
|
||||||
akadmin.set_password("akadmin", signal=False) # noqa # nosec
|
if "TF_BUILD" in environ or "AK_ADMIN_PASS" in environ or settings.TEST:
|
||||||
|
akadmin.set_password(
|
||||||
|
environ.get("AK_ADMIN_PASS", "akadmin"), signal=False
|
||||||
|
) # noqa # nosec
|
||||||
|
else:
|
||||||
|
akadmin.set_unusable_password()
|
||||||
akadmin.save()
|
akadmin.save()
|
||||||
|
|
||||||
|
|
||||||
|
21
authentik/core/migrations/0018_auto_20210330_1345.py
Normal file
21
authentik/core/migrations/0018_auto_20210330_1345.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-30 13:45
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0017_managed"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="token",
|
||||||
|
options={
|
||||||
|
"permissions": (("view_token_key", "View token's key"),),
|
||||||
|
"verbose_name": "Token",
|
||||||
|
"verbose_name_plural": "Tokens",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
24
authentik/core/migrations/0019_source_managed.py
Normal file
24
authentik/core/migrations/0019_source_managed.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.2 on 2021-04-09 14:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0018_auto_20210330_1345"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="source",
|
||||||
|
name="managed",
|
||||||
|
field=models.TextField(
|
||||||
|
default=None,
|
||||||
|
help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
|
||||||
|
null=True,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Managed by authentik",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -10,7 +10,6 @@ from django.contrib.auth.models import AbstractUser
|
|||||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, QuerySet
|
from django.db.models import Q, QuerySet
|
||||||
from django.forms import ModelForm
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@ -25,6 +24,7 @@ from structlog.stdlib import get_logger
|
|||||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||||
from authentik.core.signals import password_changed
|
from authentik.core.signals import password_changed
|
||||||
from authentik.core.types import UILoginButton
|
from authentik.core.types import UILoginButton
|
||||||
|
from authentik.flows.challenge import Challenge
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
||||||
@ -187,8 +187,8 @@ class Provider(SerializerModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
def component(self) -> str:
|
||||||
"""Return Form class used to edit this object"""
|
"""Return component used to edit this object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -240,7 +240,7 @@ class Application(PolicyBindingModel):
|
|||||||
verbose_name_plural = _("Applications")
|
verbose_name_plural = _("Applications")
|
||||||
|
|
||||||
|
|
||||||
class Source(SerializerModel, PolicyBindingModel):
|
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||||
|
|
||||||
name = models.TextField(help_text=_("Source's display Name."))
|
name = models.TextField(help_text=_("Source's display Name."))
|
||||||
@ -275,8 +275,8 @@ class Source(SerializerModel, PolicyBindingModel):
|
|||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
def component(self) -> str:
|
||||||
"""Return Form class used to edit this object"""
|
"""Return component used to edit this object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -286,9 +286,9 @@ class Source(SerializerModel, PolicyBindingModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ui_user_settings(self) -> Optional[str]:
|
def ui_user_settings(self) -> Optional[Challenge]:
|
||||||
"""Entrypoint to integrate with User settings. Can either return None if no
|
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||||
user settings are available, or a string with the URL to fetch."""
|
user settings are available, or a challenge."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -322,6 +322,8 @@ class ExpiringModel(models.Model):
|
|||||||
@property
|
@property
|
||||||
def is_expired(self) -> bool:
|
def is_expired(self) -> bool:
|
||||||
"""Check if token is expired yet."""
|
"""Check if token is expired yet."""
|
||||||
|
if not self.expiring:
|
||||||
|
return False
|
||||||
return now() > self.expires
|
return now() > self.expires
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -368,6 +370,7 @@ class Token(ManagedModel, ExpiringModel):
|
|||||||
models.Index(fields=["identifier"]),
|
models.Index(fields=["identifier"]),
|
||||||
models.Index(fields=["key"]),
|
models.Index(fields=["key"]),
|
||||||
]
|
]
|
||||||
|
permissions = (("view_token_key", "View token's key"),)
|
||||||
|
|
||||||
|
|
||||||
class PropertyMapping(SerializerModel, ManagedModel):
|
class PropertyMapping(SerializerModel, ManagedModel):
|
||||||
@ -380,8 +383,8 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
|||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
def component(self) -> str:
|
||||||
"""Return Form class used to edit this object"""
|
"""Return component used to edit this object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -12,12 +12,12 @@ password_changed = Signal()
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def post_save_application(sender, instance, created: bool, **_):
|
def post_save_application(sender, instance, created: bool, **_):
|
||||||
"""Clear user's application cache upon application creation"""
|
"""Clear user's application cache upon application creation"""
|
||||||
from authentik.core.models import Application
|
|
||||||
from authentik.core.api.applications import user_app_cache_key
|
from authentik.core.api.applications import user_app_cache_key
|
||||||
|
from authentik.core.models import Application
|
||||||
|
|
||||||
if sender != Application:
|
if sender != Application:
|
||||||
return
|
return
|
||||||
if not created:
|
if not created: # pragma: no cover
|
||||||
return
|
return
|
||||||
# Also delete user application cache
|
# Also delete user application cache
|
||||||
keys = cache.keys(user_app_cache_key("*"))
|
keys = cache.keys(user_app_cache_key("*"))
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""authentik core tasks"""
|
"""authentik core tasks"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from os import environ
|
||||||
|
|
||||||
from boto3.exceptions import Boto3Error
|
from boto3.exceptions import Boto3Error
|
||||||
from botocore.exceptions import BotoCoreError, ClientError
|
from botocore.exceptions import BotoCoreError, ClientError
|
||||||
@ -8,10 +9,12 @@ from dbbackup.db.exceptions import CommandConnectorError
|
|||||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||||
from django.core import management
|
from django.core import management
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import ExpiringModel
|
from authentik.core.models import ExpiringModel
|
||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -38,6 +41,20 @@ def clean_expired_models(self: MonitoredTask):
|
|||||||
def backup_database(self: MonitoredTask): # pragma: no cover
|
def backup_database(self: MonitoredTask): # pragma: no cover
|
||||||
"""Database backup"""
|
"""Database backup"""
|
||||||
self.result_timeout_hours = 25
|
self.result_timeout_hours = 25
|
||||||
|
if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup"):
|
||||||
|
LOGGER.info("Running in k8s and s3 backups are not configured, skipping")
|
||||||
|
self.set_status(
|
||||||
|
TaskResult(
|
||||||
|
TaskResultStatus.WARNING,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"Skipping backup as authentik is running in Kubernetes "
|
||||||
|
"without S3 backups configured."
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
start = datetime.now()
|
start = datetime.now()
|
||||||
out = StringIO()
|
out = StringIO()
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
{% extends 'login/base.html' %}
|
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load authentik_utils %}
|
|
||||||
|
|
||||||
{% block card_title %}
|
|
||||||
{{ title }} <span>(403)</span>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block card %}
|
|
||||||
<form>
|
|
||||||
<h3>{{ main }}</h3>
|
|
||||||
{% if no_referer %}
|
|
||||||
<p>{{ no_referer1 }}</p>
|
|
||||||
<p>{{ no_referer2 }}</p>
|
|
||||||
<p>{{ no_referer3 }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% if no_cookie %}
|
|
||||||
<p>{{ no_cookie1 }}</p>
|
|
||||||
<p>{{ no_cookie2 }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% if 'back' in request.GET %}
|
|
||||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
@ -1,6 +1,5 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load authentik_utils %}
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
@ -12,22 +11,18 @@
|
|||||||
<link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
|
<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="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-base.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 }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
|
||||||
<script src="{% url 'javascript-catalog' %}?v={{ ak_version }}"></script>
|
{% block head_before %}
|
||||||
|
{% endblock %}
|
||||||
|
<script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script>
|
||||||
|
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% if 'authentik_impersonate_user' in request.session %}
|
|
||||||
<div class="pf-c-banner pf-m-warning pf-c-alert pf-m-sticky">
|
|
||||||
<div class="pf-l-flex pf-m-justify-content-center pf-m-justify-content-space-between-on-lg pf-m-nowrap" style="height: 100%;">
|
|
||||||
<div class="pf-u-display-none pf-u-display-block-on-lg">
|
|
||||||
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
|
|
||||||
<a href="{% url 'authentik_core:impersonate-end' %}?back={{ request.get_full_path }}" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{% extends 'base/skeleton.html' %}
|
{% extends 'base/skeleton.html' %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load authentik_utils %}
|
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@ -13,7 +12,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||||
<div class="pf-c-empty-state">
|
<div class="pf-c-empty-state">
|
||||||
<div class="pf-c-empty-state__content">
|
<div class="pf-c-empty-state__content">
|
||||||
<i class="fas fa-exclamation-circle pf-c-empty-state__icon" aria-hidden="true"></i>
|
<i class="fas fa-exclamation-circle pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||||
@ -25,9 +24,6 @@
|
|||||||
<h3>{% trans message %}</h3>
|
<h3>{% trans message %}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if 'back' in request.GET %}
|
|
||||||
<a href="{% back %}" class="pf-c-button pf-m-primary pf-m-block">{% trans 'Back' %}</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="/" class="pf-c-button pf-m-primary pf-m-block">{% trans 'Go to home' %}</a>
|
<a href="/" class="pf-c-button pf-m-primary pf-m-block">{% trans 'Go to home' %}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
{% extends "login/base_full.html" %}
|
|
||||||
|
|
||||||
{% load authentik_utils %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
{{ title }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block card %}
|
|
||||||
<form method="POST" action="{{ url }}" autosubmit>
|
|
||||||
{% csrf_token %}
|
|
||||||
{% for key, value in attrs.items %}
|
|
||||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
|
||||||
{% endfor %}
|
|
||||||
<div class="pf-c-form__group pf-u-display-flex pf-u-justify-content-center">
|
|
||||||
<div class="pf-c-form__group-control">
|
|
||||||
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
|
|
||||||
<span class="pf-c-spinner__clipper"></span>
|
|
||||||
<span class="pf-c-spinner__lead-ball"></span>
|
|
||||||
<span class="pf-c-spinner__tail-ball"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
|
||||||
<div class="pf-c-form__actions">
|
|
||||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans 'Continue' %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<script>
|
|
||||||
document.querySelector("form").submit();
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,37 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
{% load authentik_utils %}
|
|
||||||
|
|
||||||
<section class="pf-c-page__main-section pf-m-light">
|
|
||||||
<div class="pf-c-content">
|
|
||||||
{% block above_form %}
|
|
||||||
<h1>
|
|
||||||
{% blocktrans with object_type=object|verbose_name %}
|
|
||||||
Delete {{ object_type }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="pf-c-page__main-section">
|
|
||||||
<div class="pf-l-stack">
|
|
||||||
<div class="pf-l-stack__item">
|
|
||||||
<div class="pf-c-card">
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
<form id="delete-form" action="" method="post" class="pf-c-form">
|
|
||||||
{% csrf_token %}
|
|
||||||
<p>
|
|
||||||
{% blocktrans with object_type=object|verbose_name name=object %}
|
|
||||||
Are you sure you want to delete {{ object_type }} "{{ object }}"?
|
|
||||||
{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
<input type="hidden" name="confirmdelete" value="yes">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<footer class="pf-c-modal-box__footer">
|
|
||||||
<input class="pf-c-button pf-m-danger" type="submit" form="delete-form" value="{% trans 'Delete' %}" />
|
|
||||||
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Back" %}</a>
|
|
||||||
</footer>
|
|
28
authentik/core/templates/if/admin.html
Normal file
28
authentik/core/templates/if/admin.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="{% static 'dist/AdminInterface.js' %}?v={{ ak_version }}" type="module"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<ak-message-container></ak-message-container>
|
||||||
|
<ak-interface-admin>
|
||||||
|
<section class="ak-static-page 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...' %}">
|
||||||
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
</span>
|
||||||
|
<h1 class="pf-c-title pf-m-lg">
|
||||||
|
{% trans "Loading..." %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</ak-interface-admin>
|
||||||
|
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user