Compare commits
1053 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
09b02e1aec | |||
451a9aaf01 | |||
eaee7cb562 | |||
a010c91a52 | |||
709194330f | |||
5914bbf173 | |||
5e9166f859 | |||
35b8ef6592 | |||
772a939f17 | |||
24971801cf | |||
43aebe8cb2 | |||
19cfc87c84 | |||
f920f183c8 | |||
97f979c81e | |||
e61411d396 | |||
c4f985f542 | |||
302dee7ab2 | |||
83c12ad483 | |||
4224fd5c6f | |||
597ce1eb42 | |||
5ef385f0bb | |||
cda4be3d47 | |||
8cdf22fc94 | |||
6efc7578ef | |||
4e2457560d | |||
2ddf122d27 | |||
a24651437a | |||
30bb7acb17 | |||
7859145138 | |||
8a8aafec81 | |||
deebdf2bcc | |||
4982c4abcb | |||
1486f90077 | |||
f4988bc45e | |||
8abc9cc031 | |||
534689895c | |||
8a0dd6be24 | |||
65d2eed82d | |||
e450e7b107 | |||
552ddda909 | |||
bafeff7306 | |||
6791436302 | |||
7eda794070 | |||
e3129c1067 | |||
ff481ba6e7 | |||
a106bad2db | |||
3a1c311d02 | |||
6465333f4f | |||
b761659227 | |||
9321c355f8 | |||
86c8e79ea1 | |||
8916b1f8ab | |||
41fcf2aba6 | |||
87e72b08a9 | |||
b2fcd42e3c | |||
fc1b47a80f | |||
af14e3502e | |||
a2faa5ceb5 | |||
63a19a1381 | |||
b472dcb7e7 | |||
6303909031 | |||
4bdc06865b | |||
2ee48cd039 | |||
893d5f452b | |||
340a9bc8ee | |||
cb3d9f83f1 | |||
4ba55aa8e9 | |||
bab6f501ec | |||
7327939684 | |||
ffb0135f06 | |||
ee0ddc3d17 | |||
5dd979d66c | |||
a9bd34f3c5 | |||
db316b59c5 | |||
6209714f87 | |||
1ed2bddba7 | |||
26b35c9b7b | |||
86a9271f75 | |||
402ed9bd20 | |||
68a0684569 | |||
bd2e453218 | |||
1f31c63e57 | |||
480410efa2 | |||
e9bfee52ed | |||
326b574d54 | |||
0a7abcf2ad | |||
9e5019881e | |||
8071750681 | |||
f2f0931904 | |||
a91204e5b9 | |||
b14c22cbff | |||
b3e40c6aed | |||
873aa4bb22 | |||
c1ea78c422 | |||
3c8bbc2621 | |||
42a9979d91 | |||
b7f94df4d9 | |||
4143d3fe28 | |||
f95c06b76f | |||
e3e9178ccc | |||
b694816e7b | |||
e046000f36 | |||
edb5caae9b | |||
02d27651f3 | |||
44cd4d847d | |||
472256794d | |||
cbb6887983 | |||
317e9ec605 | |||
ada2a16412 | |||
61f6b0f122 | |||
6a3f7e45cf | |||
2b78c4ba86 | |||
680ef641fb | |||
2b5504ff63 | |||
f8a6aa3250 | |||
6c23fc4b2b | |||
639c2f5c2e | |||
e44632f9a0 | |||
3f2ce34468 | |||
426cef998f | |||
8ddb62ed0f | |||
572f6d4ea0 | |||
8db68410c6 | |||
caa3c3de32 | |||
23b5ca761a | |||
f1b9021e3e | |||
99c62af89e | |||
8ae50814fe | |||
2e2b491ec7 | |||
ac432e78e2 | |||
83ac42ac43 | |||
4bd1cd127b | |||
2eb5a5cc76 | |||
75051687e6 | |||
7e316b5fc2 | |||
5594ad0b36 | |||
ea097afeae | |||
b77b4b5c80 | |||
f8dc7f48f2 | |||
692e75b057 | |||
02771683a6 | |||
40404ff41d | |||
fdd5211253 | |||
85a417d22e | |||
66c530ea06 | |||
347c3793fc | |||
cf78c89830 | |||
20c738c384 | |||
4f54ce6afb | |||
f0d7edb963 | |||
e42ad8db93 | |||
e917e756cc | |||
b4963bec76 | |||
0d23796989 | |||
d0ceafe79e | |||
f2023a7af2 | |||
31d597005f | |||
62dc86be7b | |||
7aa8e35f87 | |||
60b95271eb | |||
382b0e8941 | |||
3b068610b9 | |||
9a8f62f42e | |||
632e3cf7dc | |||
e7144649d5 | |||
dd8909c9b2 | |||
e6818c1f6a | |||
10c4e3c717 | |||
b8425867c8 | |||
a05da8cdbf | |||
c3aeefa653 | |||
62c840df21 | |||
45d1db8880 | |||
b34f30f1dd | |||
7a54e84eb4 | |||
917eef96fb | |||
9a393848b2 | |||
a6abeb50c6 | |||
39acb044fb | |||
7d2f622f4b | |||
a2b38caf64 | |||
1193b9fd22 | |||
e3a5ef1907 | |||
e597bb4542 | |||
c31df2b3f9 | |||
3f2637cffa | |||
3b6d9bec0a | |||
b184210610 | |||
d2010808ee | |||
f5b185dd06 | |||
ae161c1ba9 | |||
109283b189 | |||
235d283def | |||
96a86b3298 | |||
db9ea8603c | |||
8b7f698c7b | |||
813c13ce45 | |||
629a0e1a4d | |||
d1e2c018a3 | |||
1e86844823 | |||
b58875d4c7 | |||
03e0eecb1d | |||
7aa61d86e4 | |||
0e6a799e6d | |||
bc6afdf94f | |||
80364b04a9 | |||
0948e0ee1c | |||
5c54de66fc | |||
937edc73bc | |||
2c0d8d8943 | |||
059ccdd592 | |||
0ec0d3f1aa | |||
0a0eee138a | |||
3ed4c38101 | |||
de8cf65503 | |||
121b36f35f | |||
363aed2a47 | |||
ef994e0084 | |||
e1ef196283 | |||
f81ffd54f3 | |||
f9bfae9190 | |||
0d686465a4 | |||
e13b4a561f | |||
f6417f95e5 | |||
9c6bf5f4ae | |||
d2d7acb50e | |||
c7681dde32 | |||
8cf9661e08 | |||
2dbd76cf90 | |||
28d39f4d80 | |||
760428aa18 | |||
49bbac7441 | |||
0b8cfd437b | |||
b69aaf9417 | |||
758d1bdfd4 | |||
ab501ca971 | |||
9657741a3d | |||
29b7368f42 | |||
75724b6f8d | |||
7c9f821bfd | |||
5b9e6bed6c | |||
6113d7d768 | |||
0e3602d7eb | |||
2b94e9a687 | |||
6ed7d842e4 | |||
8794c840cf | |||
9c9c00755a | |||
6703c0a5d1 | |||
060f19ce06 | |||
b2d2e7cbc8 | |||
91fd792f88 | |||
2d9cd28221 | |||
aa64cf898f | |||
27d109c1fe | |||
1b4a14f3ee | |||
9835785864 | |||
d785998c5a | |||
8ba9553220 | |||
6eb132c48b | |||
b523cd064b | |||
355b832cc3 | |||
8f5af464a2 | |||
fb70769358 | |||
ad06778c34 | |||
bcb4451fb7 | |||
110d558572 | |||
e32d4f0095 | |||
0e413acd61 | |||
d3397c349f | |||
fb18a10e61 | |||
9bb0d04aeb | |||
666cf77b04 | |||
90ca1b8e5a | |||
f1e95b8816 | |||
dad8547212 | |||
a957e1fc45 | |||
39e3f02503 | |||
2b999e922c | |||
4224134a19 | |||
eda260dddd | |||
8a1dd521e1 | |||
1c5e91de1d | |||
4b1744fad0 | |||
f17b83010d | |||
12ddf9e73c | |||
0b3b300333 | |||
23f1a19765 | |||
b27e998615 | |||
2b928146a8 | |||
a94b0504b7 | |||
4fcbfa7709 | |||
986e01db20 | |||
9092d1189b | |||
605ed94ba2 | |||
4cbeeb9a0c | |||
993dee6aad | |||
c663deb659 | |||
61621e7d60 | |||
0ee9b07172 | |||
431ba6b4ef | |||
146818793e | |||
0ce663bce4 | |||
923ba4fb42 | |||
bb6eed0db1 | |||
d1bd8f333b | |||
2ac9f5426d | |||
8d1fd48003 | |||
241cb01ec6 | |||
65b4139997 | |||
1431be8c44 | |||
049fceeeee | |||
e6638afa3c | |||
465898c7d0 | |||
c363b1cfde | |||
b30ffd1318 | |||
fe0d3a64c8 | |||
ae9f1c1063 | |||
ea63d384fd | |||
c28d75754d | |||
518b691e00 | |||
cd845be45d | |||
a813d8e05e | |||
75f850f4d2 | |||
c84265c6f0 | |||
a477ea29cd | |||
f6aa85e340 | |||
0aeedb3ad8 | |||
4b29f238b5 | |||
34157db06a | |||
84b9e66a97 | |||
e831e4fb94 | |||
956922820b | |||
b0fac9c9f1 | |||
f4db09cd59 | |||
047030f901 | |||
638e8d741f | |||
425b87a6d0 | |||
e7dc763612 | |||
a80cc94da9 | |||
547dd3cb7a | |||
95739a934c | |||
d12e24017e | |||
e4a0345231 | |||
078633c2af | |||
4b8b800648 | |||
6f9ed001a1 | |||
e4095dfffe | |||
d5341c2284 | |||
357bd65028 | |||
867fb0dac0 | |||
2666aa2c73 | |||
f0e9bafa35 | |||
0d739f5c1a | |||
e08077c73a | |||
7cf8a31057 | |||
c43049a981 | |||
1a9ace6f9d | |||
b8d86bc482 | |||
f7044e41c6 | |||
fa59fec17a | |||
e29afa289e | |||
4d4193a586 | |||
59343ff441 | |||
cab564152d | |||
97b814ab33 | |||
88516ba2ca | |||
f069cfb643 | |||
4ce3c2341c | |||
77e42d60cb | |||
cacb919c6f | |||
2a3b049b01 | |||
e4a5e86c93 | |||
3a51bcd890 | |||
c28f68400d | |||
5d50fc281a | |||
9f7d1466e9 | |||
c815d24806 | |||
d1200a7e40 | |||
edd4f9ceae | |||
1cfe81887b | |||
bb5e0ebab1 | |||
dfda76d896 | |||
8fc5114ce4 | |||
e7b4363d21 | |||
53905d1a89 | |||
0ad1392632 | |||
6db1c914ee | |||
00324f922d | |||
8a24ddad28 | |||
0f85fe3c29 | |||
1f05eaa420 | |||
84e126a32c | |||
9ae69866bd | |||
56576a7f44 | |||
7f0295ba53 | |||
5553b3ff36 | |||
6f969525fe | |||
bac12246fb | |||
b53ef6e529 | |||
39c62afb93 | |||
c98bdbacc5 | |||
1e8d45dc15 | |||
202b057ce9 | |||
d5d8641b37 | |||
9dd37689e3 | |||
cc0832f487 | |||
b515bf7d2e | |||
34fbf3941b | |||
e73606b54d | |||
0a413fe21a | |||
d1b9f1e6b8 | |||
e5a6e128e4 | |||
9295d1ed0b | |||
5d479a6c8f | |||
4a773b2b4f | |||
8003d67844 | |||
58baf97e2d | |||
51783c1cbb | |||
94290c7e36 | |||
123ff7ad1f | |||
8f3e863cce | |||
3d6c459349 | |||
6a583bae49 | |||
78e5879d9a | |||
fdcac2a9ed | |||
e81715caef | |||
ab2b13938e | |||
5c97a3aef3 | |||
e6963c543d | |||
9ca15983a2 | |||
99ef94b7aa | |||
133bedafba | |||
c3faa61ed9 | |||
da74304221 | |||
ed6659a46d | |||
0abb1f94a4 | |||
c7e299e0bf | |||
8a6590bac8 | |||
ed717dcfa2 | |||
b6df42f580 | |||
2ea85bd0c4 | |||
68fa8105e1 | |||
79db0ce4c1 | |||
5e23b11764 | |||
c4e029ffe2 | |||
61b5b36192 | |||
c6cc1b1728 | |||
77dd652160 | |||
1144944adb | |||
7751be284e | |||
74382c6287 | |||
011babbbd9 | |||
3c01a1dd7b | |||
6e832be2de | |||
46017f2f86 | |||
da50eb0369 | |||
b996e3cee7 | |||
12735cc14c | |||
4d36699b78 | |||
8110d2861b | |||
1cc60f572d | |||
90151a13ae | |||
f958aa6930 | |||
13fbac30a2 | |||
4f4cdf16f1 | |||
7d75599627 | |||
924a13e832 | |||
ae83c35dfd | |||
e9102f4e28 | |||
9b8c1cbea5 | |||
6424bf98da | |||
74fb0f9e2a | |||
4380f37a77 | |||
17fccd44e6 | |||
217a8b5610 | |||
2cef220a3e | |||
5a8c66d325 | |||
8de13d3f67 | |||
5c22bedbaf | |||
8a0f993f0b | |||
abcf515a69 | |||
894f704c27 | |||
7798292aa8 | |||
3005ca17bd | |||
909461e533 | |||
df838a4023 | |||
0f86b62dd3 | |||
a40c3aeb68 | |||
4080738ded | |||
4a89be3048 | |||
e587c53e18 | |||
023b97aa69 | |||
51365dba74 | |||
0d3705685e | |||
738e4d5c74 | |||
b14b9cb0dd | |||
2a21ebf7b0 | |||
5bc1301043 | |||
e0e4bf6972 | |||
337677ad12 | |||
3712d5aee2 | |||
dd82d55725 | |||
8d766efecb | |||
9ac3b29418 | |||
5000c5b061 | |||
b362d2af03 | |||
bcd42fce13 | |||
6deddd038f | |||
3b47cb64da | |||
cf5e70c759 | |||
20bc38a54b | |||
672a4ab1f4 | |||
47dd667261 | |||
d1ac69789b | |||
08abf81c6d | |||
76bd987e6f | |||
5374352411 | |||
08eff4cc5d | |||
c87a9f9489 | |||
8f6d700aa8 | |||
c6843b026c | |||
3769c33ef0 | |||
8982afaf44 | |||
58c221e867 | |||
108d3e56e3 | |||
145b32c480 | |||
c788504bb0 | |||
34782b31e5 | |||
5a3ca13d76 | |||
5dc0f3b91b | |||
f51515f3de | |||
f978575293 | |||
cb64eed90d | |||
db1f7f0400 | |||
0d02dbf55c | |||
6da78b8c32 | |||
3a80bc8bda | |||
1aa9c0f9ca | |||
2da7a8fede | |||
89cb402f42 | |||
b617fd213f | |||
97b0f58f25 | |||
49a98bb744 | |||
f93a00d773 | |||
8de40a8a21 | |||
b9c54e97fa | |||
f1c55465f7 | |||
40c2b2860b | |||
a92bce322d | |||
af83308fd4 | |||
73d991e75a | |||
1eba3f1334 | |||
b86251255d | |||
ccab41a6ca | |||
0e051031b1 | |||
aecbe8c585 | |||
da98022704 | |||
e13f9c0b38 | |||
7941fb9d95 | |||
d2392b0881 | |||
b2044d75fb | |||
617b64b7db | |||
2bf5f2709a | |||
f03325df28 | |||
2b71e5bdfd | |||
f861737b85 | |||
6036d88392 | |||
bfc8a56a0b | |||
8d995011b8 | |||
5646141fe2 | |||
96b0bc324e | |||
335d6edd11 | |||
5d9bed130a | |||
0a1ab74707 | |||
ef24b94585 | |||
77b0438aa4 | |||
2788329880 | |||
15ab11be70 | |||
8d5460a132 | |||
5ba2c80813 | |||
06766bdb25 | |||
fdae13316c | |||
ae21886e8e | |||
f5dc81907a | |||
40f8ce3c4c | |||
c934915776 | |||
d70c8fbcc3 | |||
12b26e49ec | |||
0ac548d56e | |||
e771e1857f | |||
479e9750c7 | |||
c5e7801247 | |||
48ea15a946 | |||
e4c06f7356 | |||
4d7d866e4b | |||
72a93c0959 | |||
73733b20b6 | |||
3872314931 | |||
85c6ede448 | |||
49c2bee9d6 | |||
6b2c9d7c44 | |||
381010600f | |||
2a265f706a | |||
1b21b50b77 | |||
fa6324ab1d | |||
9e0daf2bcf | |||
0273ae16df | |||
f2f12ef0ba | |||
61d3df5f02 | |||
971de4fcb9 | |||
9c0bc78ca0 | |||
92085f1a3c | |||
6067406e96 | |||
9ccd4d69fe | |||
17ec48332d | |||
d3f5253a6b | |||
7a70726d57 | |||
be303937fb | |||
2326fc9ae2 | |||
9374b0bcf2 | |||
47e6028099 | |||
24114e8304 | |||
921d9c79a1 | |||
1119989ab7 | |||
e17594f0f7 | |||
5ae3b868d4 | |||
37ee4af5ff | |||
829aaca317 | |||
8eb4d53810 | |||
e60dfc5b3c | |||
cc403d8777 | |||
b81e2e69d1 | |||
731f5d0199 | |||
a40cb03b44 | |||
f6a85c98c9 | |||
5727f28784 | |||
6fc54ed7c6 | |||
4298900ecc | |||
f04aa09b72 | |||
3647633232 | |||
2e06786869 | |||
eba91c6b2b | |||
ba9f8a5795 | |||
02b4173d30 | |||
61fab497cf | |||
6a95de4e8a | |||
621e7f564a | |||
535f2eb27e | |||
0db4716e92 | |||
c10ce5c679 | |||
070438aabe | |||
71798b931c | |||
8663134c87 | |||
6bcbaeec2e | |||
17ce113c6b | |||
ff600cd5b1 | |||
2df4322ecf | |||
bb8e0c6f59 | |||
ca682c3ee4 | |||
f011e8a61a | |||
bfe27d5979 | |||
b8aff17d98 | |||
3b7e8e3931 | |||
03369e2338 | |||
5da7d9a573 | |||
12110e264d | |||
f5049d3d0f | |||
b616253444 | |||
41efe49d27 | |||
86d0e6ce45 | |||
89bb27b95c | |||
9333ffd04f | |||
2b155964c2 | |||
c3bd509eb8 | |||
72c0da2bdf | |||
151c62733f | |||
dbdea24290 | |||
909c4217bc | |||
922fc9b8d5 | |||
2c06eed8e7 | |||
a1b3af401d | |||
92d38f62b5 | |||
98a56c77e3 | |||
e5906a4115 | |||
20c6874bb4 | |||
222d3bd358 | |||
02c15f7c43 | |||
ab200eb855 | |||
9e8ce012e3 | |||
00dc8f8b1f | |||
ce812e14c7 | |||
8d32a53126 | |||
f9b6b1dd3f | |||
9679be39fa | |||
0225bf9c99 | |||
8040e2b6e4 | |||
56a56ffdbf | |||
afedcc0074 | |||
4d93e30147 | |||
f62786e58b | |||
f76c1a6f93 | |||
56871523e7 | |||
5f9dda2e58 | |||
0c55eea678 | |||
19a343dadb | |||
3ab9798f38 | |||
dd9dc7e596 | |||
797e31696a | |||
9a42c5815d | |||
f341479732 | |||
8eddb4b95b | |||
5c58532121 | |||
4b7399f454 | |||
27982a771c | |||
8296d0c94c | |||
9bc9568008 | |||
07d619d257 | |||
6ee7d5bf9c | |||
634375c43f | |||
10fc33f7d3 | |||
ee140014e9 | |||
2d363948b6 | |||
dcb3ef14d1 | |||
a71ef7f36c | |||
4d51ec906d | |||
cd42281383 | |||
faf706cbec | |||
16c05a7bbc | |||
2ad5995332 | |||
f73a404fd6 | |||
178e8e7e43 | |||
98907ec889 | |||
9dd9ab6da3 | |||
80c6b8f0c7 | |||
8436814874 | |||
3c16bdce45 | |||
a2bce79796 | |||
3e5b05203b | |||
57e86582d1 | |||
dd7cb45733 | |||
2b09d97522 | |||
d39dbc7287 | |||
48f96ea55f | |||
22a7c25526 | |||
cc69311ec0 | |||
15d7004e25 | |||
ddb70a283e | |||
ecfc3a6d93 | |||
5753182e03 | |||
db79244ba4 | |||
3231bcea66 | |||
5e0299ca82 | |||
42e35aace0 | |||
d96cfc8e30 | |||
36c97afc44 | |||
9c322be8d7 | |||
cf09205933 | |||
e851a7f294 | |||
e4f141c6c0 | |||
35fa93d9aa | |||
2bdc0102f9 | |||
aef9d27706 | |||
7bf587af24 | |||
ef1cf7867c | |||
da443b443c | |||
f4322e665a | |||
e22b8f5fdc | |||
a18176af56 | |||
4132fd139c | |||
b077bb8783 | |||
69665d9547 | |||
d0f056357d | |||
9ed236f7ab | |||
83f4830946 | |||
e23df99a9e | |||
b80ecd4668 | |||
66ca488ea0 | |||
d959b7a930 | |||
62ae3f1e31 | |||
619203c177 | |||
a1adf382af | |||
834bddd0da | |||
7d9251ce2f | |||
fb13a46252 | |||
dfefdbfd7c | |||
846c971674 | |||
5b7e1f97e0 | |||
dff0613b3d | |||
0a4343d61b | |||
09696207a6 | |||
8965451073 | |||
994c1c4b6a | |||
3ee5a672f1 | |||
b33ea9cc61 | |||
50a623d8ab | |||
cdbf7ae567 | |||
1307a39042 | |||
dca34cfbd3 | |||
735f7cbd69 | |||
728356d420 | |||
a9f6f1563d | |||
155c28d7cd | |||
f9a180eb1f | |||
4ae476e58d | |||
f32d35b07c | |||
9e936e4436 | |||
649abddea7 | |||
956382b682 | |||
67b88595ad | |||
b4ee693a5c | |||
57e5acaf2f | |||
050ec99c89 | |||
10fd1c8120 | |||
070745e764 | |||
cbeee27fc1 | |||
2bc4d0cedb | |||
5105a1c207 | |||
64e357ab0e | |||
6ca93525aa | |||
a2c978768c | |||
f0c7be7144 | |||
0f96e3e4b3 | |||
d42fc37a88 | |||
4ecd8f5dcf | |||
d7a194b512 | |||
753f8d38bf | |||
118a54517a | |||
8c27616d0c | |||
e444d0d640 | |||
3869965b4c | |||
097a42bb7b | |||
26f1f47cc1 | |||
471f9c6d05 | |||
d4e1b95991 | |||
67d13f19a1 | |||
1b7c19cf50 | |||
b012ae600d | |||
1838101d60 | |||
929add4e9c | |||
18edaea658 | |||
8030e45d75 | |||
d75c63d38b | |||
52889ffea1 | |||
2b730dec54 | |||
2aacb311bc | |||
40055ef01b | |||
6c603cdf80 | |||
5f4a1417b2 | |||
75608dce5c | |||
b0f7083879 | |||
e8420957b1 | |||
62bf79ce32 | |||
7a16c9cb14 | |||
d29d161ac6 | |||
aee58c8d53 | |||
c47ab4f1fc | |||
fa6df84de2 | |||
1faa403fe2 | |||
653631ac77 | |||
cde303e780 | |||
7f5feb9451 | |||
b85aeae5ef | |||
aa359a032c | |||
6491065aab | |||
79eec5a3a0 | |||
cd5e091937 | |||
7ed8952803 | |||
c1f302fb7c | |||
cb31e52d0e | |||
782764ac73 | |||
d0c56325ef | |||
73d57d6f82 | |||
2716a26887 | |||
0452537e8b | |||
d1a1bfbbc5 | |||
a69fcbca9a | |||
1ac4dacc3b | |||
b72b731320 | |||
65de4b8cad | |||
9e7e22367b | |||
9301b27e43 | |||
7b415a24ee | |||
f5761dc70d | |||
4f57dfda93 | |||
16380b3f7a | |||
b0e416e9f0 | |||
16f2603130 | |||
e742494f3d | |||
5fdca722f4 | |||
847cfed73f | |||
19247accd9 | |||
05b587ae44 | |||
a515afae0b | |||
8da00585e3 | |||
b70a72f247 | |||
11160b6e04 | |||
55259adf38 | |||
3f308ad48c | |||
ee6fd6f609 | |||
d53d0c353f | |||
1360b76d1b | |||
e22a286a6f | |||
62c0f69541 | |||
1c340ddbbd | |||
bcf7e162a4 | |||
62af5b2dd3 | |||
f44956bd61 | |||
e0859686c4 | |||
cb37e5c10e | |||
73bb778d62 | |||
b612a82e16 | |||
83991c743e | |||
09f43ca43b | |||
1c91835a26 | |||
c032914092 | |||
3634bf4629 | |||
0692663537 | |||
b5649bdcc4 | |||
418e491799 | |||
fab9a10487 | |||
9778050dda | |||
9ac808ee98 | |||
0f00b27384 | |||
ab5981836d | |||
a4418a83f8 | |||
36b23c4624 | |||
0c6237d8c4 | |||
e546453250 | |||
5b35d71bb3 | |||
cddff85e1c | |||
c65c6a62cc | |||
1bc51adcac | |||
c523b799be | |||
9d0d779f40 | |||
8a791c4eac | |||
036a4e86e2 | |||
4715e7bf04 | |||
45f99fbaf0 | |||
83150d9920 | |||
e31a3307b5 | |||
d28fcca344 | |||
c296e1214c | |||
d30dcda814 | |||
c720c9f41b | |||
62cfb76b39 | |||
d676cf6e3f | |||
39d87841d0 | |||
fcd879034c | |||
b285814e24 | |||
1c52836060 | |||
f3cc1be0f2 | |||
8dd77793a0 | |||
f6e8dbfb5e | |||
3c1ac4c7ec | |||
52bbf454e3 | |||
1252c6b07d | |||
3493d35af9 | |||
f8e4ffbc85 | |||
faca127217 | |||
f88575cec4 | |||
1a6ea72c09 | |||
b4eac771c2 | |||
84e4ec4406 | |||
c251b87f8c | |||
21a9aa229a | |||
5f6565ee27 | |||
afad55a357 | |||
f25d76fa43 | |||
53e15bfbca | |||
8bce16e6b4 | |||
e9bb8c896b | |||
de5455716d | |||
1d879400f2 | |||
5136ae17f5 | |||
10b45d954e | |||
339eaf37f2 | |||
f723fdd551 | |||
4cb8ae760a | |||
e4898f4b92 | |||
a2f3c54c2a | |||
c0a0b52fbb | |||
8359f0bfb3 | |||
ee610a906a | |||
828eeb5ebb | |||
c9c177d8f9 | |||
c19afa4f16 | |||
cfd4817bb5 | |||
94ae52b576 | |||
be479f2453 | |||
c5d066577d | |||
9ec6eaf4b8 | |||
b057120351 | |||
b8082598a1 | |||
1b5a163f46 | |||
1f2f48a7bc | |||
f9ad102915 | |||
ea4b920264 | |||
7d8390ca77 | |||
7ae551da65 | |||
51b26c2ac7 | |||
e4a5f22f9b | |||
2462d58135 | |||
44534153a0 | |||
facfea035b | |||
941bc61b31 | |||
282b364606 | |||
ad4bc4083d | |||
ebe282eb1a | |||
830c26ca25 | |||
ed3b4a3d4a | |||
975c4ddc04 | |||
7e2896298a | |||
cba9cf8361 | |||
bf12580f64 | |||
75ef4ce596 | |||
c2f3ce11b0 | |||
3c256fecc6 | |||
0285b84133 | |||
99a371a02c | |||
c7e6eb8896 | |||
674bd9e05c | |||
b79901df87 | |||
b248f450dd | |||
05db9e5c40 | |||
234a5e2b66 | |||
aea1736f70 | |||
9f4a4449f5 | |||
b6b55e2336 | |||
8f2805e05b | |||
4f3583cd7e | |||
617e90dca3 | |||
f7408626a8 | |||
4dcb15af46 | |||
89beb7a9f7 | |||
28eeb4798e | |||
79b92e764e | |||
919336a519 | |||
27e04589c1 | |||
ba44fbdac8 | |||
0e093a8917 | |||
d0bfb99859 | |||
93bdea3769 | |||
e681654af7 | |||
cab7593dca | |||
cf92f9aefc | |||
8d72b3498d | |||
42ab858c50 | |||
a1abae9ab1 | |||
8f36b49061 | |||
64b4e851ce | |||
40a62ac1e5 | |||
5df60e4d87 | |||
50ebc8522d |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2021.9.2
|
current_version = 2021.12.1-rc4
|
||||||
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>.*)
|
||||||
|
3
.github/codespell-words.txt
vendored
Normal file
3
.github/codespell-words.txt
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
keypair
|
||||||
|
keypairs
|
||||||
|
hass
|
231
.github/workflows/ci-main.yml
vendored
231
.github/workflows/ci-main.yml
vendored
@ -18,79 +18,17 @@ env:
|
|||||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-pylint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
steps:
|
fail-fast: false
|
||||||
- uses: actions/checkout@v2
|
matrix:
|
||||||
- uses: actions/setup-python@v2
|
job:
|
||||||
with:
|
- pylint
|
||||||
python-version: '3.9'
|
- black
|
||||||
# - id: cache-pipenv
|
- isort
|
||||||
# uses: actions/cache@v2.1.6
|
- bandit
|
||||||
# with:
|
- pyright
|
||||||
# path: ~/.local/share/virtualenvs
|
- pending-migrations
|
||||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
|
||||||
- name: prepare
|
|
||||||
# env:
|
|
||||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
|
||||||
run: scripts/ci_prepare.sh
|
|
||||||
- name: run pylint
|
|
||||||
run: pipenv run pylint authentik tests lifecycle
|
|
||||||
lint-black:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: '3.9'
|
|
||||||
# - id: cache-pipenv
|
|
||||||
# uses: actions/cache@v2.1.6
|
|
||||||
# with:
|
|
||||||
# path: ~/.local/share/virtualenvs
|
|
||||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
|
||||||
- name: prepare
|
|
||||||
# env:
|
|
||||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
|
||||||
run: scripts/ci_prepare.sh
|
|
||||||
- name: run black
|
|
||||||
run: pipenv run black --check authentik tests lifecycle
|
|
||||||
lint-isort:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: '3.9'
|
|
||||||
# - id: cache-pipenv
|
|
||||||
# uses: actions/cache@v2.1.6
|
|
||||||
# with:
|
|
||||||
# path: ~/.local/share/virtualenvs
|
|
||||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
|
||||||
- name: prepare
|
|
||||||
# env:
|
|
||||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
|
||||||
run: scripts/ci_prepare.sh
|
|
||||||
- name: run isort
|
|
||||||
run: pipenv run isort --check authentik tests lifecycle
|
|
||||||
lint-bandit:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: '3.9'
|
|
||||||
# - id: cache-pipenv
|
|
||||||
# uses: actions/cache@v2.1.6
|
|
||||||
# with:
|
|
||||||
# path: ~/.local/share/virtualenvs
|
|
||||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
|
||||||
- name: prepare
|
|
||||||
# env:
|
|
||||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
|
||||||
run: scripts/ci_prepare.sh
|
|
||||||
- name: run bandit
|
|
||||||
run: pipenv run bandit -r authentik tests lifecycle
|
|
||||||
lint-pyright:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@ -100,12 +38,17 @@ jobs:
|
|||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '16'
|
||||||
|
- id: cache-pipenv
|
||||||
|
uses: actions/cache@v2.1.7
|
||||||
|
with:
|
||||||
|
path: ~/.local/share/virtualenvs
|
||||||
|
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
- name: prepare
|
- name: prepare
|
||||||
run: |
|
env:
|
||||||
scripts/ci_prepare.sh
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
npm install -g pyright@1.1.136
|
run: scripts/ci_prepare.sh
|
||||||
- name: run bandit
|
- name: run pylint
|
||||||
run: pipenv run pyright e2e lifecycle
|
run: pipenv run make ci-${{ matrix.job }}
|
||||||
test-migrations:
|
test-migrations:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -113,14 +56,14 @@ jobs:
|
|||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
# - id: cache-pipenv
|
- id: cache-pipenv
|
||||||
# uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
# with:
|
with:
|
||||||
# path: ~/.local/share/virtualenvs
|
path: ~/.local/share/virtualenvs
|
||||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
- name: prepare
|
- name: prepare
|
||||||
# env:
|
env:
|
||||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
run: scripts/ci_prepare.sh
|
run: scripts/ci_prepare.sh
|
||||||
- name: run migrations
|
- name: run migrations
|
||||||
run: pipenv run python -m lifecycle.migrate
|
run: pipenv run python -m lifecycle.migrate
|
||||||
@ -133,32 +76,44 @@ jobs:
|
|||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
|
- name: prepare variables
|
||||||
|
id: ev
|
||||||
|
run: |
|
||||||
|
python ./scripts/gh_env.py
|
||||||
|
- id: cache-pipenv
|
||||||
|
uses: actions/cache@v2.1.7
|
||||||
|
with:
|
||||||
|
path: ~/.local/share/virtualenvs
|
||||||
|
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
- name: checkout stable
|
- name: checkout stable
|
||||||
run: |
|
run: |
|
||||||
# Copy current, latest config to local
|
# Copy current, latest config to local
|
||||||
cp authentik/lib/default.yml local.env.yml
|
cp authentik/lib/default.yml local.env.yml
|
||||||
|
cp -R .github ..
|
||||||
|
cp -R scripts ..
|
||||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||||
# - id: cache-pipenv
|
rm -rf .github/ scripts/
|
||||||
# uses: actions/cache@v2.1.6
|
mv ../.github ../scripts .
|
||||||
# with:
|
|
||||||
# path: ~/.local/share/virtualenvs
|
|
||||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
|
||||||
- name: prepare
|
- name: prepare
|
||||||
# env:
|
env:
|
||||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
run: scripts/ci_prepare.sh
|
run: |
|
||||||
|
scripts/ci_prepare.sh
|
||||||
|
# Sync anyways since stable will have different dependencies
|
||||||
|
pipenv sync --dev
|
||||||
- name: run migrations to stable
|
- name: run migrations to stable
|
||||||
run: pipenv run python -m lifecycle.migrate
|
run: pipenv run python -m lifecycle.migrate
|
||||||
- name: prepare variables
|
|
||||||
id: ev
|
|
||||||
run: |
|
|
||||||
python ./scripts/gh_do_set_branch.py
|
|
||||||
- name: checkout current code
|
- name: checkout current code
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
git fetch
|
git fetch
|
||||||
git checkout ${{ steps.ev.outputs.branchName }}
|
git reset --hard HEAD
|
||||||
|
git checkout $GITHUB_HEAD_REF
|
||||||
pipenv sync --dev
|
pipenv sync --dev
|
||||||
|
- name: prepare
|
||||||
|
env:
|
||||||
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
|
run: scripts/ci_prepare.sh
|
||||||
- name: migrate to latest
|
- name: migrate to latest
|
||||||
run: pipenv run python -m lifecycle.migrate
|
run: pipenv run python -m lifecycle.migrate
|
||||||
test-unittest:
|
test-unittest:
|
||||||
@ -168,14 +123,14 @@ jobs:
|
|||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
# - id: cache-pipenv
|
- id: cache-pipenv
|
||||||
# uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
# with:
|
with:
|
||||||
# path: ~/.local/share/virtualenvs
|
path: ~/.local/share/virtualenvs
|
||||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
- name: prepare
|
- name: prepare
|
||||||
# env:
|
env:
|
||||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
run: scripts/ci_prepare.sh
|
run: scripts/ci_prepare.sh
|
||||||
- uses: testspace-com/setup-testspace@v1
|
- uses: testspace-com/setup-testspace@v1
|
||||||
with:
|
with:
|
||||||
@ -197,14 +152,14 @@ jobs:
|
|||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
# - id: cache-pipenv
|
- id: cache-pipenv
|
||||||
# uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
# with:
|
with:
|
||||||
# path: ~/.local/share/virtualenvs
|
path: ~/.local/share/virtualenvs
|
||||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
- name: prepare
|
- name: prepare
|
||||||
# env:
|
env:
|
||||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
run: scripts/ci_prepare.sh
|
run: scripts/ci_prepare.sh
|
||||||
- uses: testspace-com/setup-testspace@v1
|
- uses: testspace-com/setup-testspace@v1
|
||||||
with:
|
with:
|
||||||
@ -236,19 +191,19 @@ jobs:
|
|||||||
- uses: testspace-com/setup-testspace@v1
|
- uses: testspace-com/setup-testspace@v1
|
||||||
with:
|
with:
|
||||||
domain: ${{github.repository_owner}}
|
domain: ${{github.repository_owner}}
|
||||||
# - id: cache-pipenv
|
- id: cache-pipenv
|
||||||
# uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
# with:
|
with:
|
||||||
# path: ~/.local/share/virtualenvs
|
path: ~/.local/share/virtualenvs
|
||||||
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
- name: prepare
|
- name: prepare
|
||||||
# env:
|
env:
|
||||||
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
run: |
|
run: |
|
||||||
scripts/ci_prepare.sh
|
scripts/ci_prepare.sh
|
||||||
docker-compose -f tests/e2e/ci.docker-compose.yml up -d
|
docker-compose -f tests/e2e/docker-compose.yml up -d
|
||||||
- id: cache-web
|
- id: cache-web
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
with:
|
with:
|
||||||
path: web/dist
|
path: web/dist
|
||||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }}
|
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }}
|
||||||
@ -268,21 +223,30 @@ jobs:
|
|||||||
testspace [e2e]unittest.xml --link=codecov
|
testspace [e2e]unittest.xml --link=codecov
|
||||||
- if: ${{ always() }}
|
- if: ${{ always() }}
|
||||||
uses: codecov/codecov-action@v2
|
uses: codecov/codecov-action@v2
|
||||||
build:
|
ci-core-mark:
|
||||||
needs:
|
needs:
|
||||||
- lint-pylint
|
- lint
|
||||||
- lint-black
|
|
||||||
- lint-isort
|
|
||||||
- lint-bandit
|
|
||||||
- lint-pyright
|
|
||||||
- test-migrations
|
- test-migrations
|
||||||
- test-migrations-from-stable
|
- test-migrations-from-stable
|
||||||
- test-unittest
|
- test-unittest
|
||||||
- test-integration
|
- test-integration
|
||||||
- test-e2e
|
- test-e2e
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo mark
|
||||||
|
build:
|
||||||
|
needs: ci-core-mark
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 120
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch:
|
||||||
|
- 'linux/amd64'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1.2.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
- name: prepare variables
|
- name: prepare variables
|
||||||
@ -290,20 +254,21 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||||
run: |
|
run: |
|
||||||
python ./scripts/gh_do_set_branch.py
|
python ./scripts/gh_env.py
|
||||||
- name: Login to Container Registry
|
- name: Login to Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
with:
|
with:
|
||||||
registry: beryju.org
|
registry: ghcr.io
|
||||||
username: ${{ secrets.HARBOR_USERNAME }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.HARBOR_PASSWORD }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}
|
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}
|
||||||
beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }}
|
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }}
|
||||||
build-args: |
|
build-args: |
|
||||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||||
|
platforms: ${{ matrix.arch }}
|
||||||
|
29
.github/workflows/ci-outpost.yml
vendored
29
.github/workflows/ci-outpost.yml
vendored
@ -30,17 +30,29 @@ jobs:
|
|||||||
-w /app \
|
-w /app \
|
||||||
golangci/golangci-lint:v1.39.0 \
|
golangci/golangci-lint:v1.39.0 \
|
||||||
golangci-lint run -v --timeout 200s
|
golangci-lint run -v --timeout 200s
|
||||||
build:
|
ci-outpost-mark:
|
||||||
needs:
|
needs:
|
||||||
- lint-golint
|
- lint-golint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo mark
|
||||||
|
build:
|
||||||
|
timeout-minutes: 120
|
||||||
|
needs:
|
||||||
|
- ci-outpost-mark
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
type:
|
type:
|
||||||
- proxy
|
- proxy
|
||||||
- ldap
|
- ldap
|
||||||
|
arch:
|
||||||
|
- 'linux/amd64'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1.2.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
- name: prepare variables
|
- name: prepare variables
|
||||||
@ -48,22 +60,23 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||||
run: |
|
run: |
|
||||||
python ./scripts/gh_do_set_branch.py
|
python ./scripts/gh_env.py
|
||||||
- name: Login to Container Registry
|
- name: Login to Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
with:
|
with:
|
||||||
registry: beryju.org
|
registry: ghcr.io
|
||||||
username: ${{ secrets.HARBOR_USERNAME }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.HARBOR_PASSWORD }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}
|
ghcr.io/goauthentik/dev-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchNameContainer }}
|
||||||
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}
|
ghcr.io/goauthentik/dev-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}
|
||||||
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.sha }}
|
ghcr.io/goauthentik/dev-${{ matrix.type }}:gh-${{ steps.ev.outputs.sha }}
|
||||||
file: ${{ matrix.type }}.Dockerfile
|
file: ${{ matrix.type }}.Dockerfile
|
||||||
build-args: |
|
build-args: |
|
||||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||||
|
platforms: ${{ matrix.arch }}
|
||||||
|
10
.github/workflows/ci-web.yml
vendored
10
.github/workflows/ci-web.yml
vendored
@ -61,16 +61,22 @@ jobs:
|
|||||||
npm install
|
npm install
|
||||||
- name: Generate API
|
- name: Generate API
|
||||||
run: make gen-web
|
run: make gen-web
|
||||||
- name: prettier
|
- name: lit-analyse
|
||||||
run: |
|
run: |
|
||||||
cd web
|
cd web
|
||||||
npm run lit-analyse
|
npm run lit-analyse
|
||||||
build:
|
ci-web-mark:
|
||||||
needs:
|
needs:
|
||||||
- lint-eslint
|
- lint-eslint
|
||||||
- lint-prettier
|
- lint-prettier
|
||||||
- lint-lit-analyse
|
- lint-lit-analyse
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo mark
|
||||||
|
build:
|
||||||
|
needs:
|
||||||
|
- ci-web-mark
|
||||||
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
|
22
.github/workflows/ghcr-retention.yml
vendored
Normal file
22
.github/workflows/ghcr-retention.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
name: ghcr-retention
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *' # every day at midnight
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
clean-ghcr:
|
||||||
|
name: Delete old unused container images
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Delete 'dev' containers older than a week
|
||||||
|
uses: sondrelg/container-retention-policy@v1
|
||||||
|
with:
|
||||||
|
image-names: dev-server,dev-ldap,dev-proxy
|
||||||
|
cut-off: One week ago UTC
|
||||||
|
account-type: org
|
||||||
|
org-name: goauthentik
|
||||||
|
untagged-only: false
|
||||||
|
token: ${{ secrets.GHCR_CLEANUP_TOKEN }}
|
||||||
|
skip-tags: gh-next,gh-master
|
107
.github/workflows/release-publish.yml
vendored
107
.github/workflows/release-publish.yml
vendored
@ -3,9 +3,6 @@ name: authentik-on-release
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published, created]
|
types: [published, created]
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- version-*
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Build
|
# Build
|
||||||
@ -33,14 +30,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik:2021.9.2,
|
beryju/authentik:2021.12.1-rc4,
|
||||||
beryju/authentik:latest,
|
beryju/authentik:latest,
|
||||||
ghcr.io/goauthentik/server:2021.9.2,
|
ghcr.io/goauthentik/server:2021.12.1-rc4,
|
||||||
ghcr.io/goauthentik/server:latest
|
ghcr.io/goauthentik/server:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
context: .
|
context: .
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.9.2', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc4', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik:latest
|
docker pull beryju/authentik:latest
|
||||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||||
@ -48,8 +45,14 @@ jobs:
|
|||||||
docker pull ghcr.io/goauthentik/server:latest
|
docker pull ghcr.io/goauthentik/server:latest
|
||||||
docker tag ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:stable
|
docker tag ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:stable
|
||||||
docker push ghcr.io/goauthentik/server:stable
|
docker push ghcr.io/goauthentik/server:stable
|
||||||
build-proxy:
|
build-outpost:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
type:
|
||||||
|
- proxy
|
||||||
|
- ldap
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v2
|
||||||
@ -75,97 +78,47 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-proxy:2021.9.2,
|
beryju/authentik-${{ matrix.type }}:2021.12.1-rc4,
|
||||||
beryju/authentik-proxy:latest,
|
beryju/authentik-${{ matrix.type }}:latest,
|
||||||
ghcr.io/goauthentik/proxy:2021.9.2,
|
ghcr.io/goauthentik/${{ matrix.type }}:2021.12.1-rc4,
|
||||||
ghcr.io/goauthentik/proxy:latest
|
ghcr.io/goauthentik/${{ matrix.type }}:latest
|
||||||
file: proxy.Dockerfile
|
file: ${{ matrix.type }}.Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.9.2', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc4', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik-proxy:latest
|
docker pull beryju/authentik-${{ matrix.type }}:latest
|
||||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable
|
||||||
docker push beryju/authentik-proxy:stable
|
docker push beryju/authentik-${{ matrix.type }}:stable
|
||||||
docker pull ghcr.io/goauthentik/proxy:latest
|
docker pull ghcr.io/goauthentik/${{ matrix.type }}:latest
|
||||||
docker tag ghcr.io/goauthentik/proxy:latest ghcr.io/goauthentik/proxy:stable
|
docker tag ghcr.io/goauthentik/${{ matrix.type }}:latest ghcr.io/goauthentik/${{ matrix.type }}:stable
|
||||||
docker push ghcr.io/goauthentik/proxy:stable
|
docker push ghcr.io/goauthentik/${{ matrix.type }}:stable
|
||||||
build-ldap:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: "^1.15"
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1.2.0
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Docker Login Registry
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Building Docker Image
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
push: ${{ github.event_name == 'release' }}
|
|
||||||
tags: |
|
|
||||||
beryju/authentik-ldap:2021.9.2,
|
|
||||||
beryju/authentik-ldap:latest,
|
|
||||||
ghcr.io/goauthentik/ldap:2021.9.2,
|
|
||||||
ghcr.io/goauthentik/ldap:latest
|
|
||||||
file: ldap.Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
- name: Building Docker Image (stable)
|
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.9.2', 'rc') }}
|
|
||||||
run: |
|
|
||||||
docker pull beryju/authentik-ldap:latest
|
|
||||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
|
||||||
docker push beryju/authentik-ldap:stable
|
|
||||||
docker pull ghcr.io/goauthentik/ldap:latest
|
|
||||||
docker tag ghcr.io/goauthentik/ldap:latest ghcr.io/goauthentik/ldap:stable
|
|
||||||
docker push ghcr.io/goauthentik/ldap:stable
|
|
||||||
test-release:
|
test-release:
|
||||||
needs:
|
needs:
|
||||||
- build-server
|
- build-server
|
||||||
- build-proxy
|
- build-outpost
|
||||||
- build-ldap
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Run test suite in final docker images
|
- name: Run test suite in final docker images
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y pwgen
|
echo "PG_PASS=$(openssl rand -base64 32)" >> .env
|
||||||
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
|
||||||
echo "AUTHENTIK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
|
||||||
docker-compose pull -q
|
docker-compose pull -q
|
||||||
docker-compose up --no-start
|
docker-compose up --no-start
|
||||||
docker-compose start postgresql redis
|
docker-compose start postgresql redis
|
||||||
docker-compose run -u root server test
|
docker-compose run -u root server test
|
||||||
sentry-release:
|
sentry-release:
|
||||||
if: ${{ github.event_name == 'release' }}
|
|
||||||
needs:
|
needs:
|
||||||
- test-release
|
- test-release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Setup Node.js environment
|
- name: Get static files from docker image
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: '16'
|
|
||||||
- name: Build web api client and web ui
|
|
||||||
run: |
|
run: |
|
||||||
export NODE_ENV=production
|
docker pull ghcr.io/goauthentik/server:latest
|
||||||
cd web
|
container=$(docker container create ghcr.io/goauthentik/server:latest)
|
||||||
npm i
|
docker cp ${container}:web/ .
|
||||||
npm run build
|
|
||||||
- name: Create a Sentry.io release
|
- name: Create a Sentry.io release
|
||||||
uses: getsentry/action-release@v1
|
uses: getsentry/action-release@v1
|
||||||
if: ${{ github.event_name == 'release' }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
@ -175,7 +128,7 @@ jobs:
|
|||||||
SENTRY_PROJECT: authentik
|
SENTRY_PROJECT: authentik
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
version: authentik@2021.9.2
|
version: authentik@2021.12.1-rc4
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
sourcemaps: './web/dist'
|
sourcemaps: './web/dist'
|
||||||
url_prefix: '~/static/dist'
|
url_prefix: '~/static/dist'
|
||||||
|
14
.github/workflows/release-tag.yml
vendored
14
.github/workflows/release-tag.yml
vendored
@ -13,21 +13,21 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Pre-release test
|
- name: Pre-release test
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y pwgen
|
echo "PG_PASS=$(openssl rand -base64 32)" >> .env
|
||||||
echo "AUTHENTIK_TAG=latest" >> .env
|
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
|
||||||
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
docker buildx install
|
||||||
echo "AUTHENTIK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
|
||||||
docker-compose pull -q
|
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t ghcr.io/goauthentik/server:latest \
|
-t testing:latest \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
|
echo "AUTHENTIK_IMAGE=testing" >> .env
|
||||||
|
echo "AUTHENTIK_TAG=latest" >> .env
|
||||||
docker-compose up --no-start
|
docker-compose up --no-start
|
||||||
docker-compose start postgresql redis
|
docker-compose start postgresql redis
|
||||||
docker-compose run -u root server test
|
docker-compose run -u root server test
|
||||||
- name: Extract version number
|
- name: Extract version number
|
||||||
id: get_version
|
id: get_version
|
||||||
uses: actions/github-script@v4.1
|
uses: actions/github-script@v5
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
58
.github/workflows/translation-compile.yml
vendored
Normal file
58
.github/workflows/translation-compile.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
name: authentik-backend-translate-compile
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
paths:
|
||||||
|
- '/locale/'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '/locale/'
|
||||||
|
schedule:
|
||||||
|
- cron: "0 */2 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: authentik
|
||||||
|
POSTGRES_USER: authentik
|
||||||
|
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
compile:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- id: cache-pipenv
|
||||||
|
uses: actions/cache@v2.1.7
|
||||||
|
with:
|
||||||
|
path: ~/.local/share/virtualenvs
|
||||||
|
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
|
- name: prepare
|
||||||
|
env:
|
||||||
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y gettext
|
||||||
|
scripts/ci_prepare.sh
|
||||||
|
- name: run compile
|
||||||
|
run: pipenv run ./manage.py compilemessages
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v3
|
||||||
|
id: cpr
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
branch: compile-backend-translation
|
||||||
|
commit-message: "core: compile backend translations"
|
||||||
|
title: "core: compile backend translations"
|
||||||
|
body: "core: compile backend translations"
|
||||||
|
delete-branch: true
|
||||||
|
signoff: true
|
||||||
|
- name: Enable Pull Request Automerge
|
||||||
|
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||||
|
uses: peter-evans/enable-pull-request-automerge@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||||
|
merge-method: squash
|
9
.github/workflows/web-api-publish.yml
vendored
9
.github/workflows/web-api-publish.yml
vendored
@ -30,10 +30,19 @@ jobs:
|
|||||||
npm i @goauthentik/api@$VERSION
|
npm i @goauthentik/api@$VERSION
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v3
|
uses: peter-evans/create-pull-request@v3
|
||||||
|
id: cpr
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
branch: update-web-api-client
|
branch: update-web-api-client
|
||||||
commit-message: "web: Update Web API Client version"
|
commit-message: "web: Update Web API Client version"
|
||||||
title: "web: Update Web API Client version"
|
title: "web: Update Web API Client version"
|
||||||
|
body: "web: Update Web API Client version"
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
signoff: true
|
signoff: true
|
||||||
|
- name: Enable Pull Request Automerge
|
||||||
|
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||||
|
uses: peter-evans/enable-pull-request-automerge@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||||
|
merge-method: squash
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -66,7 +66,9 @@ coverage.xml
|
|||||||
unittest.xml
|
unittest.xml
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
# Have to include binary mo files as they are annoying to compile at build time
|
||||||
|
# since a full postgres and redis instance are required
|
||||||
|
# *.mo
|
||||||
|
|
||||||
# Django stuff:
|
# Django stuff:
|
||||||
|
|
||||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -10,7 +10,8 @@
|
|||||||
"plex",
|
"plex",
|
||||||
"saml",
|
"saml",
|
||||||
"totp",
|
"totp",
|
||||||
"webauthn"
|
"webauthn",
|
||||||
|
"traefik"
|
||||||
],
|
],
|
||||||
"python.linting.pylintEnabled": true,
|
"python.linting.pylintEnabled": true,
|
||||||
"todo-tree.tree.showCountsInTree": true,
|
"todo-tree.tree.showCountsInTree": true,
|
||||||
|
@ -31,7 +31,7 @@ Basically, don't be a dickhead. This is an open-source non-profit project, that
|
|||||||
|
|
||||||
## I don't want to read this whole thing I just have a question!!!
|
## I don't want to read this whole thing I just have a question!!!
|
||||||
|
|
||||||
Either [create a question on GitHub](https://github.com/goauthentik/authentik/issues/new?assignees=&labels=question&template=question.md&title=) or join [the Discord server](https://discord.gg/jg33eMhnj6)
|
Either [create a question on GitHub](https://github.com/goauthentik/authentik/issues/new?assignees=&labels=question&template=question.md&title=) or join [the Discord server](https://goauthentik.io/discord)
|
||||||
|
|
||||||
## What should I know before I get started?
|
## What should I know before I get started?
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ This section guides you through submitting a bug report for authentik. Following
|
|||||||
|
|
||||||
Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form.
|
Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form.
|
||||||
|
|
||||||
This form will have the full stack trace of the error that ocurred and shouldn't contain any sensitive data.
|
This form will have the full stack trace of the error that occurred and shouldn't contain any sensitive data.
|
||||||
|
|
||||||
### Suggesting Enhancements
|
### Suggesting Enhancements
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ When you are creating an enhancement suggestion, please fill in [the template](h
|
|||||||
|
|
||||||
authentik can be run locally, all though depending on which part you want to work on, different pre-requisites are required.
|
authentik can be run locally, all though depending on which part you want to work on, different pre-requisites are required.
|
||||||
|
|
||||||
This is documented in the [developer docs](https://goauthentik.io/developer-docs/)
|
This is documented in the [developer docs](https://goauthentik.io/developer-docs/?utm_source=github)
|
||||||
|
|
||||||
### Pull Requests
|
### Pull Requests
|
||||||
|
|
||||||
|
50
Dockerfile
50
Dockerfile
@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Lock python dependencies
|
# Stage 1: Lock python dependencies
|
||||||
FROM python:3.9-slim-buster as locker
|
FROM docker.io/python:3.10.1-slim-bullseye as locker
|
||||||
|
|
||||||
COPY ./Pipfile /app/
|
COPY ./Pipfile /app/
|
||||||
COPY ./Pipfile.lock /app/
|
COPY ./Pipfile.lock /app/
|
||||||
@ -11,35 +11,32 @@ RUN pip install pipenv && \
|
|||||||
pipenv lock -r --dev-only > requirements-dev.txt
|
pipenv lock -r --dev-only > requirements-dev.txt
|
||||||
|
|
||||||
# Stage 2: Build website
|
# Stage 2: Build website
|
||||||
FROM node as website-builder
|
FROM --platform=${BUILDPLATFORM} docker.io/node:16 as website-builder
|
||||||
|
|
||||||
COPY ./website /static/
|
COPY ./website /work/website/
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN cd /static && npm i && npm run build-docs-only
|
RUN cd /work/website && npm i && npm run build-docs-only
|
||||||
|
|
||||||
# Stage 3: Build webui
|
# Stage 3: Build webui
|
||||||
FROM node as web-builder
|
FROM --platform=${BUILDPLATFORM} docker.io/node:16 as web-builder
|
||||||
|
|
||||||
COPY ./web /static/
|
COPY ./web /work/web/
|
||||||
|
COPY ./website /work/website/
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN cd /static && npm i && npm run build
|
RUN cd /work/web && npm i && npm run build
|
||||||
|
|
||||||
# Stage 4: Build go proxy
|
# Stage 4: Build go proxy
|
||||||
FROM golang:1.17.1 AS builder
|
FROM docker.io/golang:1.17.5-bullseye AS builder
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
COPY --from=web-builder /static/robots.txt /work/web/robots.txt
|
COPY --from=web-builder /work/web/robots.txt /work/web/robots.txt
|
||||||
COPY --from=web-builder /static/security.txt /work/web/security.txt
|
COPY --from=web-builder /work/web/security.txt /work/web/security.txt
|
||||||
COPY --from=web-builder /static/dist/ /work/web/dist/
|
|
||||||
COPY --from=web-builder /static/authentik/ /work/web/authentik/
|
|
||||||
COPY --from=website-builder /static/help/ /work/website/help/
|
|
||||||
|
|
||||||
COPY ./cmd /work/cmd
|
COPY ./cmd /work/cmd
|
||||||
COPY ./web/static.go /work/web/static.go
|
COPY ./web/static.go /work/web/static.go
|
||||||
COPY ./website/static.go /work/website/static.go
|
|
||||||
COPY ./internal /work/internal
|
COPY ./internal /work/internal
|
||||||
COPY ./go.mod /work/go.mod
|
COPY ./go.mod /work/go.mod
|
||||||
COPY ./go.sum /work/go.sum
|
COPY ./go.sum /work/go.sum
|
||||||
@ -47,7 +44,7 @@ COPY ./go.sum /work/go.sum
|
|||||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
RUN go build -o /work/authentik ./cmd/server/main.go
|
||||||
|
|
||||||
# Stage 5: Run
|
# Stage 5: Run
|
||||||
FROM python:3.9-slim-buster
|
FROM docker.io/python:3.10.1-slim-bullseye
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY --from=locker /app/requirements.txt /
|
COPY --from=locker /app/requirements.txt /
|
||||||
@ -57,19 +54,20 @@ ARG GIT_BUILD_HASH
|
|||||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends curl ca-certificates gnupg git runit && \
|
apt-get install -y --no-install-recommends \
|
||||||
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
curl ca-certificates gnupg git runit libpq-dev \
|
||||||
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
postgresql-client build-essential libxmlsec1-dev \
|
||||||
apt-get update && \
|
pkg-config libmaxminddb0 && \
|
||||||
apt-get install -y --no-install-recommends libpq-dev postgresql-client build-essential libxmlsec1-dev pkg-config libmaxminddb0 && \
|
pip install lxml==4.6.4 --no-cache-dir && \
|
||||||
|
export C_INCLUDE_PATH=/usr/local/lib/python3.10/site-packages/lxml/includes && \
|
||||||
pip install -r /requirements.txt --no-cache-dir && \
|
pip install -r /requirements.txt --no-cache-dir && \
|
||||||
apt-get remove --purge -y build-essential git && \
|
apt-get remove --purge -y build-essential git && \
|
||||||
apt-get autoremove --purge -y && \
|
apt-get autoremove --purge -y && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
||||||
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
||||||
mkdir /backups && \
|
mkdir -p /backups /certs /media && \
|
||||||
chown authentik:authentik /backups
|
chown authentik:authentik /backups /certs /media
|
||||||
|
|
||||||
COPY ./authentik/ /authentik
|
COPY ./authentik/ /authentik
|
||||||
COPY ./pyproject.toml /
|
COPY ./pyproject.toml /
|
||||||
@ -78,10 +76,16 @@ COPY ./tests /tests
|
|||||||
COPY ./manage.py /
|
COPY ./manage.py /
|
||||||
COPY ./lifecycle/ /lifecycle
|
COPY ./lifecycle/ /lifecycle
|
||||||
COPY --from=builder /work/authentik /authentik-proxy
|
COPY --from=builder /work/authentik /authentik-proxy
|
||||||
|
COPY --from=web-builder /work/web/dist/ /web/dist/
|
||||||
|
COPY --from=web-builder /work/web/authentik/ /web/authentik/
|
||||||
|
COPY --from=website-builder /work/website/help/ /website/help/
|
||||||
|
|
||||||
USER authentik
|
USER authentik
|
||||||
|
|
||||||
ENV TMPDIR /dev/shm/
|
ENV TMPDIR /dev/shm/
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
ENV prometheus_multiproc_dir /dev/shm/
|
|
||||||
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
|
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "/lifecycle/ak", "healthcheck" ]
|
||||||
|
|
||||||
ENTRYPOINT [ "/lifecycle/ak" ]
|
ENTRYPOINT [ "/lifecycle/ak" ]
|
||||||
|
72
Makefile
72
Makefile
@ -7,25 +7,37 @@ NPM_VERSION = $(shell python -m scripts.npm_version)
|
|||||||
all: lint-fix lint test gen
|
all: lint-fix lint test gen
|
||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
coverage run manage.py test -v 3 tests/integration
|
coverage run manage.py test tests/integration
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
coverage run manage.py test --failfast -v 3 tests/e2e
|
coverage run manage.py test tests/e2e
|
||||||
|
|
||||||
test:
|
test:
|
||||||
coverage run manage.py test -v 3 authentik
|
coverage run manage.py test authentik
|
||||||
coverage html
|
coverage html
|
||||||
coverage report
|
coverage report
|
||||||
|
|
||||||
lint-fix:
|
lint-fix:
|
||||||
isort authentik tests lifecycle
|
isort authentik tests lifecycle
|
||||||
black authentik tests lifecycle
|
black authentik tests lifecycle
|
||||||
|
codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \
|
||||||
|
authentik \
|
||||||
|
internal \
|
||||||
|
cmd \
|
||||||
|
web/src \
|
||||||
|
website/src \
|
||||||
|
website/docs \
|
||||||
|
website/developer-docs
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
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
|
||||||
|
|
||||||
|
i18n-extract: i18n-extract-core web-extract
|
||||||
|
|
||||||
|
i18n-extract-core:
|
||||||
|
./manage.py makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en
|
||||||
|
|
||||||
gen-build:
|
gen-build:
|
||||||
./manage.py spectacular --file schema.yml
|
./manage.py spectacular --file schema.yml
|
||||||
|
|
||||||
@ -49,18 +61,20 @@ gen-web:
|
|||||||
\cp -rfv web-api/* web/node_modules/@goauthentik/api
|
\cp -rfv web-api/* web/node_modules/@goauthentik/api
|
||||||
|
|
||||||
gen-outpost:
|
gen-outpost:
|
||||||
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml
|
||||||
|
mkdir -p templates
|
||||||
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O templates/README.mustache
|
||||||
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O templates/go.mod.mustache
|
||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
openapitools/openapi-generator-cli generate \
|
openapitools/openapi-generator-cli:v5.2.1 generate \
|
||||||
--git-host goauthentik.io \
|
|
||||||
--git-repo-id outpost \
|
|
||||||
--git-user-id api \
|
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g go \
|
-g go \
|
||||||
-o /local/api \
|
-o /local/api \
|
||||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false
|
-c /local/config.yaml
|
||||||
rm -f api/go.mod api/go.sum
|
go mod edit -replace goauthentik.io/api=./api
|
||||||
|
rm -rf config.yaml ./templates/
|
||||||
|
|
||||||
gen: gen-build gen-clean gen-web
|
gen: gen-build gen-clean gen-web
|
||||||
|
|
||||||
@ -68,4 +82,40 @@ migrate:
|
|||||||
python -m lifecycle.migrate
|
python -m lifecycle.migrate
|
||||||
|
|
||||||
run:
|
run:
|
||||||
WORKERS=1 go run -v cmd/server/main.go
|
go run -v cmd/server/main.go
|
||||||
|
|
||||||
|
web-watch:
|
||||||
|
cd web && npm run watch
|
||||||
|
|
||||||
|
web: web-lint-fix web-lint web-extract
|
||||||
|
|
||||||
|
web-lint-fix:
|
||||||
|
cd web && npm run prettier
|
||||||
|
|
||||||
|
web-lint:
|
||||||
|
cd web && npm run lint
|
||||||
|
cd web && npm run lit-analyse
|
||||||
|
|
||||||
|
web-extract:
|
||||||
|
cd web && npm run extract
|
||||||
|
|
||||||
|
# These targets are use by GitHub actions to allow usage of matrix
|
||||||
|
# which makes the YAML File a lot smaller
|
||||||
|
|
||||||
|
ci-pylint:
|
||||||
|
pylint authentik tests lifecycle
|
||||||
|
|
||||||
|
ci-black:
|
||||||
|
black --check authentik tests lifecycle
|
||||||
|
|
||||||
|
ci-isort:
|
||||||
|
isort --check authentik tests lifecycle
|
||||||
|
|
||||||
|
ci-bandit:
|
||||||
|
bandit -r authentik tests lifecycle
|
||||||
|
|
||||||
|
ci-pyright:
|
||||||
|
pyright e2e lifecycle
|
||||||
|
|
||||||
|
ci-pending-migrations:
|
||||||
|
./manage.py makemigrations --check
|
||||||
|
24
Pipfile
24
Pipfile
@ -8,7 +8,10 @@ boto3 = "*"
|
|||||||
celery = "*"
|
celery = "*"
|
||||||
channels = "*"
|
channels = "*"
|
||||||
channels-redis = "*"
|
channels-redis = "*"
|
||||||
|
codespell = "*"
|
||||||
|
colorama = "*"
|
||||||
dacite = "*"
|
dacite = "*"
|
||||||
|
deepmerge = "*"
|
||||||
defusedxml = "*"
|
defusedxml = "*"
|
||||||
django = "*"
|
django = "*"
|
||||||
django-dbbackup = { git = 'https://github.com/django-dbbackup/django-dbbackup.git', ref = '9d1909c30a3271c8c9c8450add30d6e0b996e145' }
|
django-dbbackup = { git = 'https://github.com/django-dbbackup/django-dbbackup.git', ref = '9d1909c30a3271c8c9c8450add30d6e0b996e145' }
|
||||||
@ -23,12 +26,14 @@ djangorestframework = "*"
|
|||||||
djangorestframework-guardian = "*"
|
djangorestframework-guardian = "*"
|
||||||
docker = "*"
|
docker = "*"
|
||||||
drf-spectacular = "*"
|
drf-spectacular = "*"
|
||||||
|
duo-client = "*"
|
||||||
facebook-sdk = "*"
|
facebook-sdk = "*"
|
||||||
geoip2 = "*"
|
geoip2 = "*"
|
||||||
gunicorn = "*"
|
gunicorn = "*"
|
||||||
kubernetes = "*"
|
kubernetes = "==v19.15.0"
|
||||||
ldap3 = "*"
|
ldap3 = "*"
|
||||||
lxml = ">=4.6.3"
|
# 4.7.0 and later remove `lxml-version.h` which is required by xmlsec
|
||||||
|
lxml = "==4.6.5"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
psycopg2-binary = "*"
|
psycopg2-binary = "*"
|
||||||
pycryptodome = "*"
|
pycryptodome = "*"
|
||||||
@ -40,24 +45,25 @@ service_identity = "*"
|
|||||||
structlog = "*"
|
structlog = "*"
|
||||||
swagger-spec-validator = "*"
|
swagger-spec-validator = "*"
|
||||||
twisted = "==21.7.0"
|
twisted = "==21.7.0"
|
||||||
|
ua-parser = "*"
|
||||||
urllib3 = {extras = ["secure"],version = "*"}
|
urllib3 = {extras = ["secure"],version = "*"}
|
||||||
uvicorn = {extras = ["standard"],version = "*"}
|
uvicorn = {extras = ["standard"],version = "*"}
|
||||||
webauthn = "*"
|
webauthn = "*"
|
||||||
xmlsec = "*"
|
xmlsec = "*"
|
||||||
duo-client = "*"
|
flower = "*"
|
||||||
ua-parser = "*"
|
wsproto = "*"
|
||||||
deepmerge = "*"
|
|
||||||
colorama = "*"
|
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
bandit = "*"
|
bandit = "*"
|
||||||
black = "==21.5b1"
|
black = "==21.11b1"
|
||||||
bump2version = "*"
|
bump2version = "*"
|
||||||
colorama = "*"
|
colorama = "*"
|
||||||
coverage = "*"
|
coverage = {extras = ["toml"],version = "*"}
|
||||||
pylint = "*"
|
pylint = "*"
|
||||||
pylint-django = "*"
|
pylint-django = "*"
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
pytest-django = "*"
|
pytest-django = "*"
|
||||||
selenium = "*"
|
pytest-randomly = "*"
|
||||||
requests-mock = "*"
|
requests-mock = "*"
|
||||||
|
selenium = "*"
|
||||||
|
importlib-metadata = "*"
|
||||||
|
2154
Pipfile.lock
generated
2154
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[](https://discord.gg/jg33eMhnj6)
|
[](https://goauthentik.io/discord)
|
||||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-main.yml)
|
[](https://github.com/goauthentik/authentik/actions/workflows/ci-main.yml)
|
||||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
|
[](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
|
||||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
|
[](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
|
||||||
@ -20,9 +20,9 @@ authentik is an open-source Identity Provider focused on flexibility and versati
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/)
|
For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github)
|
||||||
|
|
||||||
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/)
|
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ Light | Dark
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See [Development Documentation](https://goauthentik.io/developer-docs/)
|
See [Development Documentation](https://goauthentik.io/developer-docs/?utm_source=github)
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ---------- | ------------------ |
|
| ---------- | ------------------ |
|
||||||
| 2021.7.x | :white_check_mark: |
|
| 2021.9.x | :white_check_mark: |
|
||||||
| 2021.8.x | :white_check_mark: |
|
| 2021.10.x | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
"""authentik"""
|
"""authentik"""
|
||||||
__version__ = "2021.9.2"
|
__version__ = "2021.12.1-rc4"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
@ -84,9 +84,9 @@ class SystemSerializer(PassiveSerializer):
|
|||||||
return now()
|
return now()
|
||||||
|
|
||||||
def get_embedded_outpost_host(self, request: Request) -> str:
|
def get_embedded_outpost_host(self, request: Request) -> str:
|
||||||
"""Get the FQDN configured on the embeddded outpost"""
|
"""Get the FQDN configured on the embedded outpost"""
|
||||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||||
if not outposts.exists():
|
if not outposts.exists(): # pragma: no cover
|
||||||
return ""
|
return ""
|
||||||
return outposts.first().config.authentik_host
|
return outposts.first().config.authentik_host
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class TaskSerializer(PassiveSerializer):
|
|||||||
are pickled in cache. In that case, just delete the info"""
|
are pickled in cache. In that case, just delete the info"""
|
||||||
try:
|
try:
|
||||||
return super().to_representation(instance)
|
return super().to_representation(instance)
|
||||||
except AttributeError:
|
except AttributeError: # pragma: no cover
|
||||||
if isinstance(self.instance, list):
|
if isinstance(self.instance, list):
|
||||||
for inst in self.instance:
|
for inst in self.instance:
|
||||||
inst.delete()
|
inst.delete()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""authentik administration overview"""
|
"""authentik administration overview"""
|
||||||
|
from django.conf import settings
|
||||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||||
from prometheus_client import Gauge
|
from prometheus_client import Gauge
|
||||||
from rest_framework.fields import IntegerField
|
from rest_framework.fields import IntegerField
|
||||||
@ -21,4 +22,7 @@ class WorkerView(APIView):
|
|||||||
def get(self, request: Request) -> Response:
|
def get(self, request: Request) -> Response:
|
||||||
"""Get currently connected worker count."""
|
"""Get currently connected worker count."""
|
||||||
count = len(CELERY_APP.control.ping(timeout=0.5))
|
count = len(CELERY_APP.control.ping(timeout=0.5))
|
||||||
|
# In debug we run with `CELERY_TASK_ALWAYS_EAGER`, so tasks are ran on the main process
|
||||||
|
if settings.DEBUG: # pragma: no cover
|
||||||
|
count += 1
|
||||||
return Response({"count": count})
|
return Response({"count": count})
|
||||||
|
@ -8,3 +8,8 @@ class AuthentikAdminConfig(AppConfig):
|
|||||||
name = "authentik.admin"
|
name = "authentik.admin"
|
||||||
label = "authentik_admin"
|
label = "authentik_admin"
|
||||||
verbose_name = "authentik Admin"
|
verbose_name = "authentik Admin"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from authentik.admin.tasks import clear_update_notifications
|
||||||
|
|
||||||
|
clear_update_notifications.delay()
|
||||||
|
@ -10,8 +10,13 @@ from requests import RequestException
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction, Notification
|
||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.events.monitored_tasks import (
|
||||||
|
MonitoredTask,
|
||||||
|
TaskResult,
|
||||||
|
TaskResultStatus,
|
||||||
|
prefill_task,
|
||||||
|
)
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
@ -22,6 +27,7 @@ VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours
|
|||||||
# Chop of the first ^ because we want to search the entire string
|
# Chop of the first ^ because we want to search the entire string
|
||||||
URL_FINDER = URLValidator.regex.pattern[1:]
|
URL_FINDER = URLValidator.regex.pattern[1:]
|
||||||
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
||||||
|
LOCAL_VERSION = parse(__version__)
|
||||||
|
|
||||||
|
|
||||||
def _set_prom_info():
|
def _set_prom_info():
|
||||||
@ -35,7 +41,20 @@ def _set_prom_info():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@CELERY_APP.task()
|
||||||
|
def clear_update_notifications():
|
||||||
|
"""Clear update notifications on startup if the notification was for the version
|
||||||
|
we're running now."""
|
||||||
|
for notification in Notification.objects.filter(event__action=EventAction.UPDATE_AVAILABLE):
|
||||||
|
if "new_version" not in notification.event.context:
|
||||||
|
continue
|
||||||
|
notification_version = notification.event.context["new_version"]
|
||||||
|
if LOCAL_VERSION >= parse(notification_version):
|
||||||
|
notification.delete()
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||||
|
@prefill_task
|
||||||
def update_latest_version(self: MonitoredTask):
|
def update_latest_version(self: MonitoredTask):
|
||||||
"""Update latest version info"""
|
"""Update latest version info"""
|
||||||
if CONFIG.y_bool("disable_update_check"):
|
if CONFIG.y_bool("disable_update_check"):
|
||||||
@ -56,8 +75,7 @@ def update_latest_version(self: MonitoredTask):
|
|||||||
_set_prom_info()
|
_set_prom_info()
|
||||||
# Check if upstream version is newer than what we're running,
|
# Check if upstream version is newer than what we're running,
|
||||||
# and if no event exists yet, create one.
|
# and if no event exists yet, create one.
|
||||||
local_version = parse(__version__)
|
if LOCAL_VERSION < parse(upstream_version):
|
||||||
if local_version < parse(upstream_version):
|
|
||||||
# Event has already been created, don't create duplicate
|
# Event has already been created, don't create duplicate
|
||||||
if Event.objects.filter(
|
if Event.objects.filter(
|
||||||
action=EventAction.UPDATE_AVAILABLE,
|
action=EventAction.UPDATE_AVAILABLE,
|
||||||
|
@ -8,6 +8,7 @@ from authentik import __version__
|
|||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
from authentik.core.tasks import clean_expired_models
|
from authentik.core.tasks import clean_expired_models
|
||||||
from authentik.events.monitored_tasks import TaskResultStatus
|
from authentik.events.monitored_tasks import TaskResultStatus
|
||||||
|
from authentik.managed.tasks import managed_reconcile
|
||||||
|
|
||||||
|
|
||||||
class TestAdminAPI(TestCase):
|
class TestAdminAPI(TestCase):
|
||||||
@ -94,5 +95,7 @@ class TestAdminAPI(TestCase):
|
|||||||
|
|
||||||
def test_system(self):
|
def test_system(self):
|
||||||
"""Test system API"""
|
"""Test system API"""
|
||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
managed_reconcile() # pylint: disable=no-value-for-parameter
|
||||||
response = self.client.get(reverse("authentik_api:admin_system"))
|
response = self.client.get(reverse("authentik_api:admin_system"))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -3,8 +3,13 @@ from django.core.cache import cache
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from requests_mock import Mocker
|
from requests_mock import Mocker
|
||||||
|
|
||||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
from authentik.admin.tasks import (
|
||||||
|
VERSION_CACHE_KEY,
|
||||||
|
clear_update_notifications,
|
||||||
|
update_latest_version,
|
||||||
|
)
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
RESPONSE_VALID = {
|
RESPONSE_VALID = {
|
||||||
"$schema": "https://version.goauthentik.io/schema.json",
|
"$schema": "https://version.goauthentik.io/schema.json",
|
||||||
@ -56,3 +61,23 @@ class TestAdminTasks(TestCase):
|
|||||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
|
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
|
||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_version_disabled(self):
|
||||||
|
"""Test Update checker while its disabled"""
|
||||||
|
with CONFIG.patch("disable_update_check", True):
|
||||||
|
update_latest_version.delay().get()
|
||||||
|
self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0")
|
||||||
|
|
||||||
|
def test_clear_update_notifications(self):
|
||||||
|
"""Test clear of previous notification"""
|
||||||
|
Event.objects.create(
|
||||||
|
action=EventAction.UPDATE_AVAILABLE, context={"new_version": "99999999.9999999.9999999"}
|
||||||
|
)
|
||||||
|
Event.objects.create(action=EventAction.UPDATE_AVAILABLE, context={"new_version": "1.1.1"})
|
||||||
|
Event.objects.create(action=EventAction.UPDATE_AVAILABLE, context={})
|
||||||
|
clear_update_notifications()
|
||||||
|
self.assertFalse(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.UPDATE_AVAILABLE, context__new_version="1.1"
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
@ -9,6 +9,7 @@ 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
|
||||||
|
|
||||||
|
from authentik.core.middleware import KEY_AUTH_VIA, LOCAL
|
||||||
from authentik.core.models import Token, TokenIntents, User
|
from authentik.core.models import Token, TokenIntents, User
|
||||||
from authentik.outposts.models import Outpost
|
from authentik.outposts.models import Outpost
|
||||||
|
|
||||||
@ -44,6 +45,8 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
|
|||||||
if not user:
|
if not user:
|
||||||
raise AuthenticationFailed("Token invalid/expired")
|
raise AuthenticationFailed("Token invalid/expired")
|
||||||
return user
|
return user
|
||||||
|
if hasattr(LOCAL, "authentik"):
|
||||||
|
LOCAL.authentik[KEY_AUTH_VIA] = "api_token"
|
||||||
return tokens.first().user
|
return tokens.first().user
|
||||||
|
|
||||||
|
|
||||||
@ -57,7 +60,8 @@ def token_secret_key(value: str) -> Optional[User]:
|
|||||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||||
if not outposts:
|
if not outposts:
|
||||||
return None
|
return None
|
||||||
LOGGER.info("Authenticating via secret_key")
|
if hasattr(LOCAL, "authentik"):
|
||||||
|
LOCAL.authentik[KEY_AUTH_VIA] = "secret_key"
|
||||||
outpost = outposts.first()
|
outpost = outposts.first()
|
||||||
return outpost.user
|
return outpost.user
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from drf_spectacular.types import OpenApiTypes
|
|||||||
|
|
||||||
|
|
||||||
def build_standard_type(obj, **kwargs):
|
def build_standard_type(obj, **kwargs):
|
||||||
"""Build a basic type with optional add ons."""
|
"""Build a basic type with optional add owns."""
|
||||||
schema = build_basic_type(obj)
|
schema = build_basic_type(obj)
|
||||||
schema.update(kwargs)
|
schema.update(kwargs)
|
||||||
return schema
|
return schema
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
"""API tasks"""
|
|
||||||
|
|
||||||
from authentik.lib.utils.http import get_http_session
|
|
||||||
from authentik.root.celery import CELERY_APP
|
|
||||||
|
|
||||||
SENTRY_SESSION = get_http_session()
|
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
|
||||||
def sentry_proxy(payload: str):
|
|
||||||
"""Relay data to sentry"""
|
|
||||||
SENTRY_SESSION.post(
|
|
||||||
"https://sentry.beryju.org/api/8/envelope/",
|
|
||||||
data=payload,
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
@ -4,7 +4,7 @@ from django.urls import include, path
|
|||||||
from authentik.api.v3.urls import urlpatterns as v3_urls
|
from authentik.api.v3.urls import urlpatterns as v3_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Remove in 2022.1
|
# TODO: Remove in 2022.1
|
||||||
path("v2beta/", include(v3_urls)),
|
path("v2beta/", include(v3_urls)),
|
||||||
path("v3/", include(v3_urls)),
|
path("v3/", include(v3_urls)),
|
||||||
]
|
]
|
||||||
|
@ -5,7 +5,14 @@ from django.conf import settings
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||||
from rest_framework.fields import BooleanField, CharField, ChoiceField, IntegerField, ListField
|
from rest_framework.fields import (
|
||||||
|
BooleanField,
|
||||||
|
CharField,
|
||||||
|
ChoiceField,
|
||||||
|
FloatField,
|
||||||
|
IntegerField,
|
||||||
|
ListField,
|
||||||
|
)
|
||||||
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
|
||||||
@ -24,13 +31,19 @@ class Capabilities(models.TextChoices):
|
|||||||
CAN_BACKUP = "can_backup"
|
CAN_BACKUP = "can_backup"
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorReportingConfigSerializer(PassiveSerializer):
|
||||||
|
"""Config for error reporting"""
|
||||||
|
|
||||||
|
enabled = BooleanField(read_only=True)
|
||||||
|
environment = CharField(read_only=True)
|
||||||
|
send_pii = BooleanField(read_only=True)
|
||||||
|
traces_sample_rate = FloatField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class ConfigSerializer(PassiveSerializer):
|
class ConfigSerializer(PassiveSerializer):
|
||||||
"""Serialize authentik Config into DRF Object"""
|
"""Serialize authentik Config into DRF Object"""
|
||||||
|
|
||||||
error_reporting_enabled = BooleanField(read_only=True)
|
error_reporting = ErrorReportingConfigSerializer(required=True)
|
||||||
error_reporting_environment = CharField(read_only=True)
|
|
||||||
error_reporting_send_pii = BooleanField(read_only=True)
|
|
||||||
|
|
||||||
capabilities = ListField(child=ChoiceField(choices=Capabilities.choices))
|
capabilities = ListField(child=ChoiceField(choices=Capabilities.choices))
|
||||||
|
|
||||||
cache_timeout = IntegerField(required=True)
|
cache_timeout = IntegerField(required=True)
|
||||||
@ -63,12 +76,15 @@ class ConfigView(APIView):
|
|||||||
|
|
||||||
@extend_schema(responses={200: ConfigSerializer(many=False)})
|
@extend_schema(responses={200: ConfigSerializer(many=False)})
|
||||||
def get(self, request: Request) -> Response:
|
def get(self, request: Request) -> Response:
|
||||||
"""Retrive public configuration options"""
|
"""Retrieve public configuration options"""
|
||||||
config = ConfigSerializer(
|
config = ConfigSerializer(
|
||||||
{
|
{
|
||||||
"error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
|
"error_reporting": {
|
||||||
"error_reporting_environment": CONFIG.y("error_reporting.environment"),
|
"enabled": CONFIG.y("error_reporting.enabled"),
|
||||||
"error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"),
|
"environment": CONFIG.y("error_reporting.environment"),
|
||||||
|
"send_pii": CONFIG.y("error_reporting.send_pii"),
|
||||||
|
"traces_sample_rate": float(CONFIG.y("error_reporting.sample_rate", 0.4)),
|
||||||
|
},
|
||||||
"capabilities": self.get_capabilities(),
|
"capabilities": self.get_capabilities(),
|
||||||
"cache_timeout": int(CONFIG.y("redis.cache_timeout")),
|
"cache_timeout": int(CONFIG.y("redis.cache_timeout")),
|
||||||
"cache_timeout_flows": int(CONFIG.y("redis.cache_timeout_flows")),
|
"cache_timeout_flows": int(CONFIG.y("redis.cache_timeout_flows")),
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
"""Sentry tunnel"""
|
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.http.request import HttpRequest
|
|
||||||
from django.http.response import HttpResponse
|
|
||||||
from rest_framework.authentication import SessionAuthentication
|
|
||||||
from rest_framework.parsers import BaseParser
|
|
||||||
from rest_framework.permissions import AllowAny
|
|
||||||
from rest_framework.request import Request
|
|
||||||
from rest_framework.throttling import AnonRateThrottle
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.api.tasks import sentry_proxy
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class PlainTextParser(BaseParser):
|
|
||||||
"""Plain text parser."""
|
|
||||||
|
|
||||||
media_type = "text/plain"
|
|
||||||
|
|
||||||
def parse(self, stream, media_type=None, parser_context=None) -> str:
|
|
||||||
"""Simply return a string representing the body of the request."""
|
|
||||||
return stream.read()
|
|
||||||
|
|
||||||
|
|
||||||
class CsrfExemptSessionAuthentication(SessionAuthentication):
|
|
||||||
"""CSRF-exempt Session authentication"""
|
|
||||||
|
|
||||||
def enforce_csrf(self, request: Request):
|
|
||||||
return # To not perform the csrf check previously happening
|
|
||||||
|
|
||||||
|
|
||||||
class SentryTunnelView(APIView):
|
|
||||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
|
||||||
|
|
||||||
serializer_class = None
|
|
||||||
parser_classes = [PlainTextParser]
|
|
||||||
throttle_classes = [AnonRateThrottle]
|
|
||||||
permission_classes = [AllowAny]
|
|
||||||
authentication_classes = [CsrfExemptSessionAuthentication]
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
|
||||||
# Only allow usage of this endpoint when error reporting is enabled
|
|
||||||
if not CONFIG.y_bool("error_reporting.enabled", False):
|
|
||||||
LOGGER.debug("error reporting disabled")
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
# Body is 2 json objects separated by \n
|
|
||||||
full_body = request.body
|
|
||||||
lines = full_body.splitlines()
|
|
||||||
if len(lines) < 1:
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
header = loads(lines[0])
|
|
||||||
# Check that the DSN is what we expect
|
|
||||||
dsn = header.get("dsn", "")
|
|
||||||
if dsn != settings.SENTRY_DSN:
|
|
||||||
LOGGER.debug("Invalid dsn", have=dsn, expected=settings.SENTRY_DSN)
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
sentry_proxy.delay(full_body.decode())
|
|
||||||
return HttpResponse(status=204)
|
|
@ -11,14 +11,14 @@ from authentik.admin.api.tasks import TaskViewSet
|
|||||||
from authentik.admin.api.version import VersionView
|
from authentik.admin.api.version import VersionView
|
||||||
from authentik.admin.api.workers import WorkerView
|
from authentik.admin.api.workers import WorkerView
|
||||||
from authentik.api.v3.config import ConfigView
|
from authentik.api.v3.config import ConfigView
|
||||||
from authentik.api.v3.sentry import SentryTunnelView
|
|
||||||
from authentik.api.views import APIBrowserView
|
from authentik.api.views import APIBrowserView
|
||||||
from authentik.core.api.applications import ApplicationViewSet
|
from authentik.core.api.applications import ApplicationViewSet
|
||||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
||||||
|
from authentik.core.api.devices import DeviceViewSet
|
||||||
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
|
||||||
from authentik.core.api.providers import ProviderViewSet
|
from authentik.core.api.providers import ProviderViewSet
|
||||||
from authentik.core.api.sources import SourceViewSet
|
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
||||||
from authentik.core.api.tokens import TokenViewSet
|
from authentik.core.api.tokens import TokenViewSet
|
||||||
from authentik.core.api.users import UserViewSet
|
from authentik.core.api.users import UserViewSet
|
||||||
from authentik.crypto.api import CertificateKeyPairViewSet
|
from authentik.crypto.api import CertificateKeyPairViewSet
|
||||||
@ -30,7 +30,8 @@ from authentik.events.api.notification_transport import NotificationTransportVie
|
|||||||
from authentik.flows.api.bindings import FlowStageBindingViewSet
|
from authentik.flows.api.bindings import FlowStageBindingViewSet
|
||||||
from authentik.flows.api.flows import FlowViewSet
|
from authentik.flows.api.flows import FlowViewSet
|
||||||
from authentik.flows.api.stages import StageViewSet
|
from authentik.flows.api.stages import StageViewSet
|
||||||
from authentik.flows.views import FlowExecutorView
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
|
from authentik.flows.views.inspector import FlowInspectorView
|
||||||
from authentik.outposts.api.outposts import OutpostViewSet
|
from authentik.outposts.api.outposts import OutpostViewSet
|
||||||
from authentik.outposts.api.service_connections import (
|
from authentik.outposts.api.service_connections import (
|
||||||
DockerServiceConnectionViewSet,
|
DockerServiceConnectionViewSet,
|
||||||
@ -67,6 +68,11 @@ from authentik.stages.authenticator_duo.api import (
|
|||||||
DuoAdminDeviceViewSet,
|
DuoAdminDeviceViewSet,
|
||||||
DuoDeviceViewSet,
|
DuoDeviceViewSet,
|
||||||
)
|
)
|
||||||
|
from authentik.stages.authenticator_sms.api import (
|
||||||
|
AuthenticatorSMSStageViewSet,
|
||||||
|
SMSAdminDeviceViewSet,
|
||||||
|
SMSDeviceViewSet,
|
||||||
|
)
|
||||||
from authentik.stages.authenticator_static.api import (
|
from authentik.stages.authenticator_static.api import (
|
||||||
AuthenticatorStaticStageViewSet,
|
AuthenticatorStaticStageViewSet,
|
||||||
StaticAdminDeviceViewSet,
|
StaticAdminDeviceViewSet,
|
||||||
@ -130,6 +136,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/user_connections/all", UserSourceConnectionViewSet)
|
||||||
router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet)
|
router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet)
|
||||||
router.register("sources/user_connections/plex", PlexSourceConnectionViewSet)
|
router.register("sources/user_connections/plex", PlexSourceConnectionViewSet)
|
||||||
router.register("sources/ldap", LDAPSourceViewSet)
|
router.register("sources/ldap", LDAPSourceViewSet)
|
||||||
@ -163,7 +170,9 @@ router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
|||||||
router.register("propertymappings/scope", ScopeMappingViewSet)
|
router.register("propertymappings/scope", ScopeMappingViewSet)
|
||||||
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
|
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
|
||||||
|
|
||||||
|
router.register("authenticators/all", DeviceViewSet, basename="device")
|
||||||
router.register("authenticators/duo", DuoDeviceViewSet)
|
router.register("authenticators/duo", DuoDeviceViewSet)
|
||||||
|
router.register("authenticators/sms", SMSDeviceViewSet)
|
||||||
router.register("authenticators/static", StaticDeviceViewSet)
|
router.register("authenticators/static", StaticDeviceViewSet)
|
||||||
router.register("authenticators/totp", TOTPDeviceViewSet)
|
router.register("authenticators/totp", TOTPDeviceViewSet)
|
||||||
router.register("authenticators/webauthn", WebAuthnDeviceViewSet)
|
router.register("authenticators/webauthn", WebAuthnDeviceViewSet)
|
||||||
@ -172,6 +181,11 @@ router.register(
|
|||||||
DuoAdminDeviceViewSet,
|
DuoAdminDeviceViewSet,
|
||||||
basename="admin-duodevice",
|
basename="admin-duodevice",
|
||||||
)
|
)
|
||||||
|
router.register(
|
||||||
|
"authenticators/admin/sms",
|
||||||
|
SMSAdminDeviceViewSet,
|
||||||
|
basename="admin-smsdevice",
|
||||||
|
)
|
||||||
router.register(
|
router.register(
|
||||||
"authenticators/admin/static",
|
"authenticators/admin/static",
|
||||||
StaticAdminDeviceViewSet,
|
StaticAdminDeviceViewSet,
|
||||||
@ -186,6 +200,7 @@ router.register(
|
|||||||
|
|
||||||
router.register("stages/all", StageViewSet)
|
router.register("stages/all", StageViewSet)
|
||||||
router.register("stages/authenticator/duo", AuthenticatorDuoStageViewSet)
|
router.register("stages/authenticator/duo", AuthenticatorDuoStageViewSet)
|
||||||
|
router.register("stages/authenticator/sms", AuthenticatorSMSStageViewSet)
|
||||||
router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)
|
router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)
|
||||||
router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet)
|
router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet)
|
||||||
router.register("stages/authenticator/validate", AuthenticatorValidateStageViewSet)
|
router.register("stages/authenticator/validate", AuthenticatorValidateStageViewSet)
|
||||||
@ -228,7 +243,11 @@ urlpatterns = (
|
|||||||
FlowExecutorView.as_view(),
|
FlowExecutorView.as_view(),
|
||||||
name="flow-executor",
|
name="flow-executor",
|
||||||
),
|
),
|
||||||
path("sentry/", SentryTunnelView.as_view(), name="sentry"),
|
path(
|
||||||
|
"flows/inspector/<slug:flow_slug>/",
|
||||||
|
FlowInspectorView.as_view(),
|
||||||
|
name="flow-inspector",
|
||||||
|
),
|
||||||
path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
|
path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
36
authentik/core/api/devices.py
Normal file
36
authentik/core/api/devices.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""Authenticator Devices API Views"""
|
||||||
|
from django_otp import devices_for_user
|
||||||
|
from django_otp.models import Device
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import MetaNameSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSerializer(MetaNameSerializer):
|
||||||
|
"""Serializer for Duo authenticator devices"""
|
||||||
|
|
||||||
|
pk = IntegerField()
|
||||||
|
name = CharField()
|
||||||
|
type = SerializerMethodField()
|
||||||
|
|
||||||
|
def get_type(self, instance: Device) -> str:
|
||||||
|
"""Get type of device"""
|
||||||
|
return instance._meta.label
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceViewSet(ViewSet):
|
||||||
|
"""Viewset for authenticator devices"""
|
||||||
|
|
||||||
|
serializer_class = DeviceSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
@extend_schema(responses={200: DeviceSerializer(many=True)})
|
||||||
|
def list(self, request: Request) -> Response:
|
||||||
|
"""Get all devices for current user"""
|
||||||
|
devices = devices_for_user(request.user)
|
||||||
|
return Response(DeviceSerializer(devices, many=True).data)
|
@ -1,9 +1,11 @@
|
|||||||
"""Groups API Viewset"""
|
"""Groups API Viewset"""
|
||||||
|
from json import loads
|
||||||
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django_filters.filters import ModelMultipleChoiceFilter
|
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from rest_framework.fields import CharField, JSONField
|
from rest_framework.fields import CharField, JSONField
|
||||||
from rest_framework.serializers import ListSerializer, ModelSerializer
|
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||||
|
|
||||||
@ -42,6 +44,7 @@ class GroupSerializer(ModelSerializer):
|
|||||||
users_obj = ListSerializer(
|
users_obj = ListSerializer(
|
||||||
child=GroupMemberSerializer(), read_only=True, source="users", required=False
|
child=GroupMemberSerializer(), read_only=True, source="users", required=False
|
||||||
)
|
)
|
||||||
|
parent_name = CharField(source="parent.name", read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -51,6 +54,7 @@ class GroupSerializer(ModelSerializer):
|
|||||||
"name",
|
"name",
|
||||||
"is_superuser",
|
"is_superuser",
|
||||||
"parent",
|
"parent",
|
||||||
|
"parent_name",
|
||||||
"users",
|
"users",
|
||||||
"attributes",
|
"attributes",
|
||||||
"users_obj",
|
"users_obj",
|
||||||
@ -60,6 +64,13 @@ class GroupSerializer(ModelSerializer):
|
|||||||
class GroupFilter(FilterSet):
|
class GroupFilter(FilterSet):
|
||||||
"""Filter for groups"""
|
"""Filter for groups"""
|
||||||
|
|
||||||
|
attributes = CharFilter(
|
||||||
|
field_name="attributes",
|
||||||
|
lookup_expr="",
|
||||||
|
label="Attributes",
|
||||||
|
method="filter_attributes",
|
||||||
|
)
|
||||||
|
|
||||||
members_by_username = ModelMultipleChoiceFilter(
|
members_by_username = ModelMultipleChoiceFilter(
|
||||||
field_name="users__username",
|
field_name="users__username",
|
||||||
to_field_name="username",
|
to_field_name="username",
|
||||||
@ -70,10 +81,28 @@ class GroupFilter(FilterSet):
|
|||||||
queryset=User.objects.all(),
|
queryset=User.objects.all(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def filter_attributes(self, queryset, name, value):
|
||||||
|
"""Filter attributes by query args"""
|
||||||
|
try:
|
||||||
|
value = loads(value)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(detail="filter: failed to parse JSON")
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValidationError(detail="filter: value must be key:value mapping")
|
||||||
|
qs = {}
|
||||||
|
for key, _value in value.items():
|
||||||
|
qs[f"attributes__{key}"] = _value
|
||||||
|
try:
|
||||||
|
_ = len(queryset.filter(**qs))
|
||||||
|
return queryset.filter(**qs)
|
||||||
|
except ValueError:
|
||||||
|
return queryset
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
fields = ["name", "is_superuser", "members_by_pk", "members_by_username"]
|
fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"]
|
||||||
|
|
||||||
|
|
||||||
class GroupViewSet(UsedByMixin, ModelViewSet):
|
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
@ -56,6 +56,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
|
|||||||
"component",
|
"component",
|
||||||
"verbose_name",
|
"verbose_name",
|
||||||
"verbose_name_plural",
|
"verbose_name_plural",
|
||||||
|
"meta_model_name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
"assigned_application_name",
|
"assigned_application_name",
|
||||||
"verbose_name",
|
"verbose_name",
|
||||||
"verbose_name_plural",
|
"verbose_name_plural",
|
||||||
|
"meta_model_name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
"""Source API Views"""
|
"""Source API Views"""
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
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 GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
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, UserSourceConnection
|
||||||
from authentik.core.types import UserSettingSerializer
|
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
|
from authentik.policies.engine import PolicyEngine
|
||||||
@ -45,6 +48,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
"component",
|
"component",
|
||||||
"verbose_name",
|
"verbose_name",
|
||||||
"verbose_name_plural",
|
"verbose_name_plural",
|
||||||
|
"meta_model_name",
|
||||||
"policy_engine_mode",
|
"policy_engine_mode",
|
||||||
"user_matching_mode",
|
"user_matching_mode",
|
||||||
]
|
]
|
||||||
@ -113,3 +117,39 @@ class SourceViewSet(
|
|||||||
LOGGER.warning(source_settings.errors)
|
LOGGER.warning(source_settings.errors)
|
||||||
matching_sources.append(source_settings.validated_data)
|
matching_sources.append(source_settings.validated_data)
|
||||||
return Response(matching_sources)
|
return Response(matching_sources)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSourceConnectionSerializer(SourceSerializer):
|
||||||
|
"""OAuth Source Serializer"""
|
||||||
|
|
||||||
|
source = SourceSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserSourceConnection
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"user",
|
||||||
|
"source",
|
||||||
|
"created",
|
||||||
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
"user": {"read_only": True},
|
||||||
|
"created": {"read_only": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UserSourceConnectionViewSet(
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
GenericViewSet,
|
||||||
|
):
|
||||||
|
"""User-source connection Viewset"""
|
||||||
|
|
||||||
|
queryset = UserSourceConnection.objects.all()
|
||||||
|
serializer_class = UserSourceConnectionSerializer
|
||||||
|
permission_classes = [OwnerPermissions]
|
||||||
|
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
|
ordering = ["pk"]
|
||||||
|
@ -82,6 +82,7 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"description",
|
"description",
|
||||||
"expires",
|
"expires",
|
||||||
"expiring",
|
"expiring",
|
||||||
|
"managed",
|
||||||
]
|
]
|
||||||
ordering = ["identifier", "expires"]
|
ordering = ["identifier", "expires"]
|
||||||
permission_classes = [OwnerSuperuserPermissions]
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
|
@ -22,7 +22,7 @@ from drf_spectacular.utils import (
|
|||||||
)
|
)
|
||||||
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
from rest_framework.fields import CharField, DictField, JSONField, SerializerMethodField
|
||||||
from rest_framework.permissions import IsAuthenticated
|
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
|
||||||
@ -45,6 +45,8 @@ from authentik.core.api.used_by import UsedByMixin
|
|||||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
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.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
|
USER_ATTRIBUTE_CHANGE_EMAIL,
|
||||||
|
USER_ATTRIBUTE_CHANGE_USERNAME,
|
||||||
USER_ATTRIBUTE_SA,
|
USER_ATTRIBUTE_SA,
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||||
Group,
|
Group,
|
||||||
@ -53,6 +55,7 @@ from authentik.core.models import (
|
|||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import EventAction
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.stages.email.tasks import send_mails
|
from authentik.stages.email.tasks import send_mails
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
@ -90,6 +93,9 @@ class UserSerializer(ModelSerializer):
|
|||||||
"attributes",
|
"attributes",
|
||||||
"uid",
|
"uid",
|
||||||
]
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
"name": {"allow_blank": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserSelfSerializer(ModelSerializer):
|
class UserSelfSerializer(ModelSerializer):
|
||||||
@ -98,8 +104,45 @@ class UserSelfSerializer(ModelSerializer):
|
|||||||
|
|
||||||
is_superuser = BooleanField(read_only=True)
|
is_superuser = BooleanField(read_only=True)
|
||||||
avatar = CharField(read_only=True)
|
avatar = CharField(read_only=True)
|
||||||
groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
|
groups = SerializerMethodField()
|
||||||
uid = CharField(read_only=True)
|
uid = CharField(read_only=True)
|
||||||
|
settings = DictField(source="attributes.settings", default=dict)
|
||||||
|
|
||||||
|
@extend_schema_field(
|
||||||
|
ListSerializer(
|
||||||
|
child=inline_serializer(
|
||||||
|
"UserSelfGroups",
|
||||||
|
{"name": CharField(read_only=True), "pk": CharField(read_only=True)},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
def get_groups(self, _: User):
|
||||||
|
"""Return only the group names a user is member of"""
|
||||||
|
for group in self.instance.ak_groups.all():
|
||||||
|
yield {
|
||||||
|
"name": group.name,
|
||||||
|
"pk": group.pk,
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_email(self, email: str):
|
||||||
|
"""Check if the user is allowed to change their email"""
|
||||||
|
if self.instance.group_attributes().get(
|
||||||
|
USER_ATTRIBUTE_CHANGE_EMAIL, CONFIG.y_bool("default_user_change_email", True)
|
||||||
|
):
|
||||||
|
return email
|
||||||
|
if email != self.instance.email:
|
||||||
|
raise ValidationError("Not allowed to change email.")
|
||||||
|
return email
|
||||||
|
|
||||||
|
def validate_username(self, username: str):
|
||||||
|
"""Check if the user is allowed to change their username"""
|
||||||
|
if self.instance.group_attributes().get(
|
||||||
|
USER_ATTRIBUTE_CHANGE_USERNAME, CONFIG.y_bool("default_user_change_username", True)
|
||||||
|
):
|
||||||
|
return username
|
||||||
|
if username != self.instance.username:
|
||||||
|
raise ValidationError("Not allowed to change username.")
|
||||||
|
return username
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -114,9 +157,11 @@ class UserSelfSerializer(ModelSerializer):
|
|||||||
"email",
|
"email",
|
||||||
"avatar",
|
"avatar",
|
||||||
"uid",
|
"uid",
|
||||||
|
"settings",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"is_active": {"read_only": True},
|
"is_active": {"read_only": True},
|
||||||
|
"name": {"allow_blank": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -188,7 +233,11 @@ class UsersFilter(FilterSet):
|
|||||||
qs = {}
|
qs = {}
|
||||||
for key, _value in value.items():
|
for key, _value in value.items():
|
||||||
qs[f"attributes__{key}"] = _value
|
qs[f"attributes__{key}"] = _value
|
||||||
return queryset.filter(**qs)
|
try:
|
||||||
|
_ = len(queryset.filter(**qs))
|
||||||
|
return queryset.filter(**qs)
|
||||||
|
except ValueError:
|
||||||
|
return queryset
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
@ -208,6 +257,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""User Viewset"""
|
"""User Viewset"""
|
||||||
|
|
||||||
queryset = User.objects.none()
|
queryset = User.objects.none()
|
||||||
|
ordering = ["username"]
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
search_fields = ["username", "name", "is_active", "email"]
|
search_fields = ["username", "name", "is_active", "email"]
|
||||||
filterset_class = UsersFilter
|
filterset_class = UsersFilter
|
||||||
@ -268,7 +318,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
name=username,
|
name=username,
|
||||||
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
||||||
)
|
)
|
||||||
if create_group:
|
if create_group and self.request.user.has_perm("authentik_core.add_group"):
|
||||||
group = Group.objects.create(
|
group = Group.objects.create(
|
||||||
name=username,
|
name=username,
|
||||||
)
|
)
|
||||||
@ -288,13 +338,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
# 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"""
|
||||||
serializer = SessionUserSerializer(data={"user": UserSelfSerializer(request.user).data})
|
serializer = SessionUserSerializer(
|
||||||
|
data={"user": UserSelfSerializer(instance=request.user).data}
|
||||||
|
)
|
||||||
if SESSION_IMPERSONATE_USER in request._request.session:
|
if SESSION_IMPERSONATE_USER in request._request.session:
|
||||||
serializer.initial_data["original"] = UserSelfSerializer(
|
serializer.initial_data["original"] = UserSelfSerializer(
|
||||||
request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
instance=request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||||
).data
|
).data
|
||||||
serializer.is_valid()
|
return Response(serializer.initial_data)
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@extend_schema(request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)})
|
@extend_schema(request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)})
|
||||||
@action(
|
@action(
|
||||||
@ -308,15 +359,13 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""Allow users to change information on their own profile"""
|
"""Allow users to change information on their own profile"""
|
||||||
data = UserSelfSerializer(instance=User.objects.get(pk=request.user.pk), data=request.data)
|
data = UserSelfSerializer(instance=User.objects.get(pk=request.user.pk), data=request.data)
|
||||||
if not data.is_valid():
|
if not data.is_valid():
|
||||||
return Response(data.errors)
|
return Response(data.errors, status=400)
|
||||||
new_user = data.save()
|
new_user = data.save()
|
||||||
# If we're impersonating, we need to update that user object
|
# If we're impersonating, we need to update that user object
|
||||||
# since it caches the full object
|
# since it caches the full object
|
||||||
if SESSION_IMPERSONATE_USER in request.session:
|
if SESSION_IMPERSONATE_USER in request.session:
|
||||||
request.session[SESSION_IMPERSONATE_USER] = new_user
|
request.session[SESSION_IMPERSONATE_USER] = new_user
|
||||||
serializer = SessionUserSerializer(data={"user": UserSelfSerializer(request.user).data})
|
return Response({"user": data.data})
|
||||||
serializer.is_valid()
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
||||||
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
||||||
|
@ -41,6 +41,7 @@ class MetaNameSerializer(PassiveSerializer):
|
|||||||
|
|
||||||
verbose_name = SerializerMethodField()
|
verbose_name = SerializerMethodField()
|
||||||
verbose_name_plural = SerializerMethodField()
|
verbose_name_plural = SerializerMethodField()
|
||||||
|
meta_model_name = SerializerMethodField()
|
||||||
|
|
||||||
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"""
|
||||||
@ -50,6 +51,10 @@ class MetaNameSerializer(PassiveSerializer):
|
|||||||
"""Return object's plural verbose_name"""
|
"""Return object's plural verbose_name"""
|
||||||
return obj._meta.verbose_name_plural
|
return obj._meta.verbose_name_plural
|
||||||
|
|
||||||
|
def get_meta_model_name(self, obj: Model) -> str:
|
||||||
|
"""Return internal model name"""
|
||||||
|
return f"{obj._meta.app_label}.{obj._meta.model_name}"
|
||||||
|
|
||||||
|
|
||||||
class TypeCreateSerializer(PassiveSerializer):
|
class TypeCreateSerializer(PassiveSerializer):
|
||||||
"""Types of an object that can be created"""
|
"""Types of an object that can be created"""
|
||||||
|
@ -8,7 +8,7 @@ from django.http.request import HttpRequest
|
|||||||
from authentik.core.models import Token, TokenIntents, User
|
from authentik.core.models import Token, TokenIntents, User
|
||||||
from authentik.events.utils import cleanse_dict, sanitize_dict
|
from authentik.events.utils import cleanse_dict, sanitize_dict
|
||||||
from authentik.flows.planner import FlowPlan
|
from authentik.flows.planner import FlowPlan
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||||
|
|
||||||
|
|
||||||
@ -55,5 +55,5 @@ class TokenBackend(InbuiltBackend):
|
|||||||
if not tokens.exists():
|
if not tokens.exists():
|
||||||
return None
|
return None
|
||||||
token = tokens.first()
|
token = tokens.first()
|
||||||
self.set_method("password", request, token=token)
|
self.set_method("token", request, token=token)
|
||||||
return token.user
|
return token.user
|
||||||
|
@ -10,6 +10,8 @@ SESSION_IMPERSONATE_USER = "authentik_impersonate_user"
|
|||||||
SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
|
SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
|
||||||
LOCAL = local()
|
LOCAL = local()
|
||||||
RESPONSE_HEADER_ID = "X-authentik-id"
|
RESPONSE_HEADER_ID = "X-authentik-id"
|
||||||
|
KEY_AUTH_VIA = "auth_via"
|
||||||
|
KEY_USER = "user"
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateMiddleware:
|
class ImpersonateMiddleware:
|
||||||
@ -50,15 +52,19 @@ class RequestIDMiddleware:
|
|||||||
}
|
}
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
response[RESPONSE_HEADER_ID] = request.request_id
|
response[RESPONSE_HEADER_ID] = request.request_id
|
||||||
del LOCAL.authentik["request_id"]
|
setattr(response, "ak_context", {})
|
||||||
del LOCAL.authentik["host"]
|
response.ak_context.update(LOCAL.authentik)
|
||||||
|
response.ak_context[KEY_USER] = request.user.username
|
||||||
|
for key in list(LOCAL.authentik.keys()):
|
||||||
|
del LOCAL.authentik[key]
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def structlog_add_request_id(logger: Logger, method_name: str, event_dict):
|
def structlog_add_request_id(logger: Logger, method_name: str, event_dict: dict):
|
||||||
"""If threadlocal has authentik defined, add request_id to log"""
|
"""If threadlocal has authentik defined, add request_id to log"""
|
||||||
if hasattr(LOCAL, "authentik"):
|
if hasattr(LOCAL, "authentik"):
|
||||||
event_dict["request_id"] = LOCAL.authentik.get("request_id", "")
|
event_dict.update(LOCAL.authentik)
|
||||||
event_dict["host"] = LOCAL.authentik.get("host", "")
|
if hasattr(LOCAL, "authentik_task"):
|
||||||
|
event_dict.update(LOCAL.authentik_task)
|
||||||
return event_dict
|
return event_dict
|
||||||
|
@ -0,0 +1,221 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-10 16:16
|
||||||
|
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
import authentik.core.models
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
# We have to use a direct import here, otherwise we get an object manager error
|
||||||
|
from authentik.core.models import User
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
||||||
|
username="akadmin", email="root@localhost", name="authentik Default Admin"
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_admin_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
Group = apps.get_model("authentik_core", "Group")
|
||||||
|
User = apps.get_model("authentik_core", "User")
|
||||||
|
|
||||||
|
# Creates a default admin group
|
||||||
|
group, _ = Group.objects.using(db_alias).get_or_create(
|
||||||
|
is_superuser=True,
|
||||||
|
defaults={
|
||||||
|
"name": "authentik Admins",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
group.users.set(User.objects.filter(username="akadmin"))
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [
|
||||||
|
("authentik_core", "0002_auto_20200523_1133"),
|
||||||
|
("authentik_core", "0003_default_user"),
|
||||||
|
("authentik_core", "0004_auto_20200703_2213"),
|
||||||
|
("authentik_core", "0005_token_intent"),
|
||||||
|
("authentik_core", "0006_auto_20200709_1608"),
|
||||||
|
("authentik_core", "0007_auto_20200815_1841"),
|
||||||
|
("authentik_core", "0008_auto_20200824_1532"),
|
||||||
|
("authentik_core", "0009_group_is_superuser"),
|
||||||
|
("authentik_core", "0010_auto_20200917_1021"),
|
||||||
|
("authentik_core", "0011_provider_name_temp"),
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0001_initial"),
|
||||||
|
("authentik_flows", "0003_auto_20200523_1133"),
|
||||||
|
("auth", "0012_alter_user_first_name_max_length"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="application",
|
||||||
|
name="skip_authorization",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="source",
|
||||||
|
name="authentication_flow",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
help_text="Flow to use when authenticating existing users.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="source_authentication",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="source",
|
||||||
|
name="enrollment_flow",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
help_text="Flow to use when enrolling new users.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="source_enrollment",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="provider",
|
||||||
|
name="authorization_flow",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text="Flow used when authorizing this provider.",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="provider_authorization",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="user",
|
||||||
|
name="is_superuser",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="user",
|
||||||
|
name="is_staff",
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=create_default_user,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="is_superuser",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="is_staff",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="application",
|
||||||
|
options={"verbose_name": "Application", "verbose_name_plural": "Applications"},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="user",
|
||||||
|
options={
|
||||||
|
"permissions": (("reset_user_password", "Reset Password"),),
|
||||||
|
"verbose_name": "User",
|
||||||
|
"verbose_name_plural": "Users",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="token",
|
||||||
|
name="intent",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[("verification", "Intent Verification"), ("api", "Intent Api")],
|
||||||
|
default="verification",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="source",
|
||||||
|
name="slug",
|
||||||
|
field=models.SlugField(help_text="Internal source name, used in URLs.", unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="first_name",
|
||||||
|
field=models.CharField(blank=True, max_length=150, verbose_name="first name"),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="user",
|
||||||
|
name="groups",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="groups",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.Group",
|
||||||
|
verbose_name="groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="user",
|
||||||
|
name="is_superuser",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="user",
|
||||||
|
name="is_staff",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="pb_groups",
|
||||||
|
field=models.ManyToManyField(related_name="users", to="authentik_core.Group"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="group",
|
||||||
|
name="is_superuser",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False, help_text="Users added to this group will be superusers."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=create_default_admin_group,
|
||||||
|
),
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name="user",
|
||||||
|
managers=[
|
||||||
|
("objects", authentik.core.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="user",
|
||||||
|
options={
|
||||||
|
"permissions": (
|
||||||
|
("reset_user_password", "Reset Password"),
|
||||||
|
("impersonate", "Can impersonate other users"),
|
||||||
|
),
|
||||||
|
"verbose_name": "User",
|
||||||
|
"verbose_name_plural": "Users",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="provider",
|
||||||
|
name="name_temp",
|
||||||
|
field=models.TextField(default=""),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,118 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-12 15:36
|
||||||
|
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
import authentik.core.models
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_token_key(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
Token = apps.get_model("authentik_core", "Token")
|
||||||
|
|
||||||
|
for token in Token.objects.using(db_alias).all():
|
||||||
|
token.key = token.pk.hex
|
||||||
|
token.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [
|
||||||
|
("authentik_core", "0012_auto_20201003_1737"),
|
||||||
|
("authentik_core", "0013_auto_20201003_2132"),
|
||||||
|
("authentik_core", "0014_auto_20201018_1158"),
|
||||||
|
("authentik_core", "0015_application_icon"),
|
||||||
|
("authentik_core", "0016_auto_20201202_2234"),
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_providers_saml", "0006_remove_samlprovider_name"),
|
||||||
|
("authentik_providers_oauth2", "0006_remove_oauth2provider_name"),
|
||||||
|
("authentik_core", "0011_provider_name_temp"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="provider",
|
||||||
|
old_name="name_temp",
|
||||||
|
new_name="name",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="token",
|
||||||
|
name="identifier",
|
||||||
|
field=models.TextField(default=""),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="token",
|
||||||
|
name="intent",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("verification", "Intent Verification"),
|
||||||
|
("api", "Intent Api"),
|
||||||
|
("recovery", "Intent Recovery"),
|
||||||
|
],
|
||||||
|
default="verification",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="token",
|
||||||
|
unique_together={("identifier", "user")},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="token",
|
||||||
|
name="key",
|
||||||
|
field=models.TextField(default=authentik.core.models.default_token_key),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="token",
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="token",
|
||||||
|
name="identifier",
|
||||||
|
field=models.SlugField(max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="token",
|
||||||
|
index=models.Index(fields=["key"], name="authentik_co_key_e45007_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="token",
|
||||||
|
index=models.Index(fields=["identifier"], name="authentik_co_identif_1a34a8_idx"),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=set_default_token_key,
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="application",
|
||||||
|
name="meta_icon_url",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="application",
|
||||||
|
name="meta_icon",
|
||||||
|
field=models.FileField(blank=True, default="", upload_to="application-icons/"),
|
||||||
|
),
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name="token",
|
||||||
|
name="authentik_co_key_e45007_idx",
|
||||||
|
),
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name="token",
|
||||||
|
name="authentik_co_identif_1a34a8_idx",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="user",
|
||||||
|
old_name="pb_groups",
|
||||||
|
new_name="ak_groups",
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="token",
|
||||||
|
index=models.Index(fields=["identifier"], name="authentik_c_identif_d9d032_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="token",
|
||||||
|
index=models.Index(fields=["key"], name="authentik_c_key_f71355_idx"),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,210 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-10 16:12
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
|
import authentik.core.models
|
||||||
|
import authentik.lib.models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
session_keys = cache.keys(KEY_PREFIX + "*")
|
||||||
|
cache.delete_many(session_keys)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_duplicates(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
Token = apps.get_model("authentik_core", "token")
|
||||||
|
identifiers = (
|
||||||
|
Token.objects.using(db_alias)
|
||||||
|
.values("identifier")
|
||||||
|
.annotate(identifier_count=Count("identifier"))
|
||||||
|
.filter(identifier_count__gt=1)
|
||||||
|
)
|
||||||
|
for ident in identifiers:
|
||||||
|
Token.objects.using(db_alias).filter(identifier=ident["identifier"]).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_user_token(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
# We have to use a direct import here, otherwise we get an object manager error
|
||||||
|
from authentik.core.models import Token, TokenIntents, User
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
akadmin = User.objects.using(db_alias).filter(username="akadmin")
|
||||||
|
if not akadmin.exists():
|
||||||
|
return
|
||||||
|
if "AK_ADMIN_TOKEN" not in environ:
|
||||||
|
return
|
||||||
|
Token.objects.using(db_alias).create(
|
||||||
|
identifier="authentik-boostrap-token",
|
||||||
|
user=akadmin.first(),
|
||||||
|
intent=TokenIntents.INTENT_API,
|
||||||
|
expiring=False,
|
||||||
|
key=environ["AK_ADMIN_TOKEN"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [
|
||||||
|
("authentik_core", "0018_auto_20210330_1345"),
|
||||||
|
("authentik_core", "0019_source_managed"),
|
||||||
|
("authentik_core", "0020_source_user_matching_mode"),
|
||||||
|
("authentik_core", "0021_alter_application_slug"),
|
||||||
|
("authentik_core", "0022_authenticatedsession"),
|
||||||
|
("authentik_core", "0023_alter_application_meta_launch_url"),
|
||||||
|
("authentik_core", "0024_alter_token_identifier"),
|
||||||
|
("authentik_core", "0025_alter_application_meta_icon"),
|
||||||
|
("authentik_core", "0026_alter_application_meta_icon"),
|
||||||
|
("authentik_core", "0027_bootstrap_token"),
|
||||||
|
("authentik_core", "0028_alter_token_intent"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="source",
|
||||||
|
name="user_matching_mode",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("identifier", "Use the source-specific identifier"),
|
||||||
|
(
|
||||||
|
"email_link",
|
||||||
|
"Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"email_deny",
|
||||||
|
"Use the user's email address, but deny enrollment when the email address already exists.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"username_link",
|
||||||
|
"Link to a user with identical username. Can have security implications when a username is used with another source.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"username_deny",
|
||||||
|
"Use the user's username, but deny enrollment when the username already exists.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
default="identifier",
|
||||||
|
help_text="How the source determines if an existing user should be authenticated or a new user enrolled.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="application",
|
||||||
|
name="slug",
|
||||||
|
field=models.SlugField(
|
||||||
|
help_text="Internal application name, used in URLs.", unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="AuthenticatedSession",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"expires",
|
||||||
|
models.DateTimeField(default=authentik.core.models.default_token_duration),
|
||||||
|
),
|
||||||
|
("expiring", models.BooleanField(default=True)),
|
||||||
|
("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
("session_key", models.CharField(max_length=40)),
|
||||||
|
("last_ip", models.TextField()),
|
||||||
|
("last_user_agent", models.TextField(blank=True)),
|
||||||
|
("last_used", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=migrate_sessions,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="application",
|
||||||
|
name="meta_launch_url",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True, default="", validators=[authentik.lib.models.DomainlessURLValidator()]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=fix_duplicates,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="token",
|
||||||
|
name="identifier",
|
||||||
|
field=models.SlugField(max_length=255, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="application",
|
||||||
|
name="meta_icon",
|
||||||
|
field=models.FileField(default=None, null=True, upload_to="application-icons/"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="application",
|
||||||
|
name="meta_icon",
|
||||||
|
field=models.FileField(
|
||||||
|
default=None, max_length=500, null=True, upload_to="application-icons/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="authenticatedsession",
|
||||||
|
options={
|
||||||
|
"verbose_name": "Authenticated Session",
|
||||||
|
"verbose_name_plural": "Authenticated Sessions",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=create_default_user_token,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="token",
|
||||||
|
name="intent",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("verification", "Intent Verification"),
|
||||||
|
("api", "Intent Api"),
|
||||||
|
("recovery", "Intent Recovery"),
|
||||||
|
("app_password", "Intent App Password"),
|
||||||
|
],
|
||||||
|
default="verification",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"username_link",
|
"username_link",
|
||||||
"Link to a user with identical username address. Can have security implications when a username is used with another source.",
|
"Link to a user with identical username. Can have security implications when a username is used with another source.",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"username_deny",
|
"username_deny",
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
# Generated by Django 3.2.3 on 2021-06-02 21:51
|
# Generated by Django 3.2.3 on 2021-06-02 21:51
|
||||||
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import authentik.lib.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ class Migration(migrations.Migration):
|
|||||||
field=models.TextField(
|
field=models.TextField(
|
||||||
blank=True,
|
blank=True,
|
||||||
default="",
|
default="",
|
||||||
validators=[django.core.validators.URLValidator()],
|
validators=[authentik.lib.models.DomainlessURLValidator()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -9,7 +9,6 @@ from deepmerge import always_merger
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
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.core import validators
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, QuerySet, options
|
from django.db.models import Q, QuerySet, options
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
@ -26,10 +25,9 @@ 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, UserSettingSerializer
|
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||||
from authentik.flows.models import Flow
|
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.managed.models import ManagedModel
|
from authentik.managed.models import ManagedModel
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
@ -39,6 +37,8 @@ USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
|||||||
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
||||||
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
|
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
|
||||||
|
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
|
||||||
|
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
|
||||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
||||||
|
|
||||||
GRAVATAR_URL = "https://secure.gravatar.com"
|
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||||
@ -79,6 +79,27 @@ class Group(models.Model):
|
|||||||
)
|
)
|
||||||
attributes = models.JSONField(default=dict, blank=True)
|
attributes = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
def is_member(self, user: "User") -> bool:
|
||||||
|
"""Recursively check if `user` is member of us, or any parent."""
|
||||||
|
query = """
|
||||||
|
WITH RECURSIVE parents AS (
|
||||||
|
SELECT authentik_core_group.*, 0 AS relative_depth
|
||||||
|
FROM authentik_core_group
|
||||||
|
WHERE authentik_core_group.group_uuid = %s
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT authentik_core_group.*, parents.relative_depth - 1
|
||||||
|
FROM authentik_core_group,parents
|
||||||
|
WHERE authentik_core_group.parent_id = parents.group_uuid
|
||||||
|
)
|
||||||
|
SELECT group_uuid
|
||||||
|
FROM parents
|
||||||
|
GROUP BY group_uuid;
|
||||||
|
"""
|
||||||
|
groups = Group.objects.raw(query, [self.group_uuid])
|
||||||
|
return user.ak_groups.filter(pk__in=[group.pk for group in groups]).exists()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Group {self.name}"
|
return f"Group {self.name}"
|
||||||
|
|
||||||
@ -151,7 +172,7 @@ class User(GuardianUserMixin, AbstractUser):
|
|||||||
if mode == "none":
|
if mode == "none":
|
||||||
return DEFAULT_AVATAR
|
return DEFAULT_AVATAR
|
||||||
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
||||||
mail_hash = md5(self.email.encode("utf-8")).hexdigest() # nosec
|
mail_hash = md5(self.email.lower().encode("utf-8")).hexdigest() # nosec
|
||||||
if mode == "gravatar":
|
if mode == "gravatar":
|
||||||
parameters = [
|
parameters = [
|
||||||
("s", "158"),
|
("s", "158"),
|
||||||
@ -181,7 +202,7 @@ class Provider(SerializerModel):
|
|||||||
name = models.TextField()
|
name = models.TextField()
|
||||||
|
|
||||||
authorization_flow = models.ForeignKey(
|
authorization_flow = models.ForeignKey(
|
||||||
Flow,
|
"authentik_flows.Flow",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
help_text=_("Flow used when authorizing this provider."),
|
help_text=_("Flow used when authorizing this provider."),
|
||||||
related_name="provider_authorization",
|
related_name="provider_authorization",
|
||||||
@ -223,7 +244,7 @@ class Application(PolicyBindingModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
meta_launch_url = models.TextField(
|
meta_launch_url = models.TextField(
|
||||||
default="", blank=True, validators=[validators.URLValidator()]
|
default="", blank=True, validators=[DomainlessURLValidator()]
|
||||||
)
|
)
|
||||||
# For template applications, this can be set to /static/authentik/applications/*
|
# For template applications, this can be set to /static/authentik/applications/*
|
||||||
meta_icon = models.FileField(
|
meta_icon = models.FileField(
|
||||||
@ -241,7 +262,7 @@ class Application(PolicyBindingModel):
|
|||||||
it is returned as-is"""
|
it is returned as-is"""
|
||||||
if not self.meta_icon:
|
if not self.meta_icon:
|
||||||
return None
|
return None
|
||||||
if self.meta_icon.name.startswith("http") or self.meta_icon.name.startswith("/static"):
|
if "://" in self.meta_icon.name or self.meta_icon.name.startswith("/static"):
|
||||||
return self.meta_icon.name
|
return self.meta_icon.name
|
||||||
return self.meta_icon.url
|
return self.meta_icon.url
|
||||||
|
|
||||||
@ -283,7 +304,7 @@ class SourceUserMatchingModes(models.TextChoices):
|
|||||||
)
|
)
|
||||||
USERNAME_LINK = "username_link", _(
|
USERNAME_LINK = "username_link", _(
|
||||||
(
|
(
|
||||||
"Link to a user with identical username address. Can have security implications "
|
"Link to a user with identical username. Can have security implications "
|
||||||
"when a username is used with another source."
|
"when a username is used with another source."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -302,7 +323,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
|
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
|
||||||
|
|
||||||
authentication_flow = models.ForeignKey(
|
authentication_flow = models.ForeignKey(
|
||||||
Flow,
|
"authentik_flows.Flow",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
@ -311,7 +332,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
related_name="source_authentication",
|
related_name="source_authentication",
|
||||||
)
|
)
|
||||||
enrollment_flow = models.ForeignKey(
|
enrollment_flow = models.ForeignKey(
|
||||||
Flow,
|
"authentik_flows.Flow",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
|
@ -22,7 +22,7 @@ from authentik.flows.planner import (
|
|||||||
PLAN_CONTEXT_SSO,
|
PLAN_CONTEXT_SSO,
|
||||||
FlowPlanner,
|
FlowPlanner,
|
||||||
)
|
)
|
||||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||||
from authentik.lib.utils.urls import redirect_with_qs
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
from authentik.policies.utils import delete_none_keys
|
from authentik.policies.utils import delete_none_keys
|
||||||
from authentik.stages.password import BACKEND_INBUILT
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
|
@ -6,6 +6,7 @@ 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
|
||||||
from dbbackup.db.exceptions import CommandConnectorError
|
from dbbackup.db.exceptions import CommandConnectorError
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core import management
|
from django.core import management
|
||||||
@ -15,7 +16,12 @@ 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 AuthenticatedSession, ExpiringModel
|
from authentik.core.models import AuthenticatedSession, ExpiringModel
|
||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.events.monitored_tasks import (
|
||||||
|
MonitoredTask,
|
||||||
|
TaskResult,
|
||||||
|
TaskResultStatus,
|
||||||
|
prefill_task,
|
||||||
|
)
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
@ -23,6 +29,7 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||||
|
@prefill_task
|
||||||
def clean_expired_models(self: MonitoredTask):
|
def clean_expired_models(self: MonitoredTask):
|
||||||
"""Remove expired objects"""
|
"""Remove expired objects"""
|
||||||
messages = []
|
messages = []
|
||||||
@ -49,23 +56,25 @@ def clean_expired_models(self: MonitoredTask):
|
|||||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
|
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
|
||||||
|
|
||||||
|
|
||||||
|
def should_backup() -> bool:
|
||||||
|
"""Check if we should be doing backups"""
|
||||||
|
if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup.bucket"):
|
||||||
|
LOGGER.info("Running in k8s and s3 backups are not configured, skipping")
|
||||||
|
return False
|
||||||
|
if not CONFIG.y_bool("postgresql.backup.enabled"):
|
||||||
|
return False
|
||||||
|
if settings.DEBUG:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||||
|
@prefill_task
|
||||||
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"):
|
if not should_backup():
|
||||||
LOGGER.info("Running in k8s and s3 backups are not configured, skipping")
|
self.set_status(TaskResult(TaskResultStatus.UNKNOWN, ["Backups are not configured."]))
|
||||||
self.set_status(
|
|
||||||
TaskResult(
|
|
||||||
TaskResultStatus.WARNING,
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"Skipping backup as authentik is running in Kubernetes "
|
|
||||||
"without S3 backups configured."
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
start = datetime.now()
|
start = datetime.now()
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/AdminInterface.js' %}" type="module"></script>
|
<script src="{% static 'dist/admin/AdminInterface.js' %}" type="module"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
@ -5,13 +5,13 @@
|
|||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% if flow.compatibility_mode %}
|
{% if flow.compatibility_mode and not inspector %}
|
||||||
<script>ShadyDOM = { force: !navigator.webdriver };</script>
|
<script>ShadyDOM = { force: !navigator.webdriver };</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/FlowInterface.js' %}" type="module"></script>
|
<script src="{% static 'dist/flow/FlowInterface.js' %}" type="module"></script>
|
||||||
<style>
|
<style>
|
||||||
.pf-c-background-image::before {
|
.pf-c-background-image::before {
|
||||||
--ak-flow-background: url("{{ flow.background_url }}");
|
--ak-flow-background: url("{{ flow.background_url }}");
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/UserInterface.js' %}" type="module"></script>
|
<script src="{% static 'dist/user/UserInterface.js' %}" type="module"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if tenant.branding_title != "authentik" %}
|
{% if tenant.branding_title != "authentik" %}
|
||||||
<li>
|
<li>
|
||||||
<a href="https://goauthentik.io">
|
<a href="https://goauthentik.io?utm_source=authentik">
|
||||||
{% trans 'Powered by authentik' %}
|
{% trans 'Powered by authentik' %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -3,7 +3,8 @@ from django.urls import reverse
|
|||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
"""Test applications API"""
|
"""Test applications API"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = User.objects.get(username="akadmin")
|
self.user = create_test_admin_user()
|
||||||
self.allowed = Application.objects.create(name="allowed", slug="allowed")
|
self.allowed = Application.objects.create(name="allowed", slug="allowed")
|
||||||
self.denied = Application.objects.create(name="denied", slug="denied")
|
self.denied = Application.objects.create(name="denied", slug="denied")
|
||||||
PolicyBinding.objects.create(
|
PolicyBinding.objects.create(
|
||||||
|
@ -6,6 +6,7 @@ from django.utils.encoding import force_str
|
|||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
|
||||||
|
|
||||||
class TestAuthenticatedSessionsAPI(APITestCase):
|
class TestAuthenticatedSessionsAPI(APITestCase):
|
||||||
@ -13,7 +14,7 @@ class TestAuthenticatedSessionsAPI(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.get(username="akadmin")
|
self.user = create_test_admin_user()
|
||||||
self.other_user = User.objects.create(username="normal-user")
|
self.other_user = User.objects.create(username="normal-user")
|
||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
|
40
authentik/core/tests/test_groups.py
Normal file
40
authentik/core/tests/test_groups.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"""group tests"""
|
||||||
|
from django.test.testcases import TestCase
|
||||||
|
|
||||||
|
from authentik.core.models import Group, User
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroups(TestCase):
|
||||||
|
"""Test group membership"""
|
||||||
|
|
||||||
|
def test_group_membership_simple(self):
|
||||||
|
"""Test simple membership"""
|
||||||
|
user = User.objects.create(username="user")
|
||||||
|
user2 = User.objects.create(username="user2")
|
||||||
|
group = Group.objects.create(name="group")
|
||||||
|
group.users.add(user)
|
||||||
|
self.assertTrue(group.is_member(user))
|
||||||
|
self.assertFalse(group.is_member(user2))
|
||||||
|
|
||||||
|
def test_group_membership_parent(self):
|
||||||
|
"""Test parent membership"""
|
||||||
|
user = User.objects.create(username="user")
|
||||||
|
user2 = User.objects.create(username="user2")
|
||||||
|
first = Group.objects.create(name="first")
|
||||||
|
second = Group.objects.create(name="second", parent=first)
|
||||||
|
second.users.add(user)
|
||||||
|
self.assertTrue(first.is_member(user))
|
||||||
|
self.assertFalse(first.is_member(user2))
|
||||||
|
|
||||||
|
def test_group_membership_parent_extra(self):
|
||||||
|
"""Test parent membership"""
|
||||||
|
user = User.objects.create(username="user")
|
||||||
|
user2 = User.objects.create(username="user2")
|
||||||
|
first = Group.objects.create(name="first")
|
||||||
|
second = Group.objects.create(name="second", parent=first)
|
||||||
|
third = Group.objects.create(name="third", parent=second)
|
||||||
|
second.users.add(user)
|
||||||
|
self.assertTrue(first.is_member(user))
|
||||||
|
self.assertFalse(first.is_member(user2))
|
||||||
|
self.assertFalse(third.is_member(user))
|
||||||
|
self.assertFalse(third.is_member(user2))
|
@ -5,6 +5,7 @@ from django.test.testcases import TestCase
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
|
||||||
|
|
||||||
class TestImpersonation(TestCase):
|
class TestImpersonation(TestCase):
|
||||||
@ -13,14 +14,14 @@ class TestImpersonation(TestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.other_user = User.objects.create(username="to-impersonate")
|
self.other_user = User.objects.create(username="to-impersonate")
|
||||||
self.akadmin = User.objects.get(username="akadmin")
|
self.user = create_test_admin_user()
|
||||||
|
|
||||||
def test_impersonate_simple(self):
|
def test_impersonate_simple(self):
|
||||||
"""test simple impersonation and un-impersonation"""
|
"""test simple impersonation and un-impersonation"""
|
||||||
# test with an inactive user to ensure that still works
|
# test with an inactive user to ensure that still works
|
||||||
self.other_user.is_active = False
|
self.other_user.is_active = False
|
||||||
self.other_user.save()
|
self.other_user.save()
|
||||||
self.client.force_login(self.akadmin)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
self.client.get(
|
self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
@ -32,13 +33,13 @@ class TestImpersonation(TestCase):
|
|||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
response_body = loads(response.content.decode())
|
response_body = loads(response.content.decode())
|
||||||
self.assertEqual(response_body["user"]["username"], self.other_user.username)
|
self.assertEqual(response_body["user"]["username"], self.other_user.username)
|
||||||
self.assertEqual(response_body["original"]["username"], self.akadmin.username)
|
self.assertEqual(response_body["original"]["username"], self.user.username)
|
||||||
|
|
||||||
self.client.get(reverse("authentik_core:impersonate-end"))
|
self.client.get(reverse("authentik_core:impersonate-end"))
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
response_body = loads(response.content.decode())
|
response_body = loads(response.content.decode())
|
||||||
self.assertEqual(response_body["user"]["username"], self.akadmin.username)
|
self.assertEqual(response_body["user"]["username"], self.user.username)
|
||||||
self.assertNotIn("original", response_body)
|
self.assertNotIn("original", response_body)
|
||||||
|
|
||||||
def test_impersonate_denied(self):
|
def test_impersonate_denied(self):
|
||||||
@ -46,7 +47,7 @@ class TestImpersonation(TestCase):
|
|||||||
self.client.force_login(self.other_user)
|
self.client.force_login(self.other_user)
|
||||||
|
|
||||||
self.client.get(
|
self.client.get(
|
||||||
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.akadmin.pk})
|
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
|
@ -49,7 +49,7 @@ def provider_tester_factory(test_model: Type[Stage]) -> Callable:
|
|||||||
|
|
||||||
def tester(self: TestModels):
|
def tester(self: TestModels):
|
||||||
model_class = None
|
model_class = None
|
||||||
if test_model._meta.abstract:
|
if test_model._meta.abstract: # pragma: no cover
|
||||||
model_class = test_model.__bases__[0]()
|
model_class = test_model.__bases__[0]()
|
||||||
else:
|
else:
|
||||||
model_class = test_model()
|
model_class = test_model()
|
||||||
@ -59,6 +59,6 @@ def provider_tester_factory(test_model: Type[Stage]) -> Callable:
|
|||||||
|
|
||||||
|
|
||||||
for model in all_subclasses(Source):
|
for model in all_subclasses(Source):
|
||||||
setattr(TestModels, f"test_model_{model.__name__}", source_tester_factory(model))
|
setattr(TestModels, f"test_source_{model.__name__}", source_tester_factory(model))
|
||||||
for model in all_subclasses(Provider):
|
for model in all_subclasses(Provider):
|
||||||
setattr(TestModels, f"test_model_{model.__name__}", provider_tester_factory(model))
|
setattr(TestModels, f"test_provider_{model.__name__}", provider_tester_factory(model))
|
||||||
|
@ -6,7 +6,8 @@ from rest_framework.serializers import ValidationError
|
|||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||||
from authentik.core.models import PropertyMapping, User
|
from authentik.core.models import PropertyMapping
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
|
||||||
|
|
||||||
class TestPropertyMappingAPI(APITestCase):
|
class TestPropertyMappingAPI(APITestCase):
|
||||||
@ -17,7 +18,7 @@ class TestPropertyMappingAPI(APITestCase):
|
|||||||
self.mapping = PropertyMapping.objects.create(
|
self.mapping = PropertyMapping.objects.create(
|
||||||
name="dummy", expression="""return {'foo': 'bar'}"""
|
name="dummy", expression="""return {'foo': 'bar'}"""
|
||||||
)
|
)
|
||||||
self.user = User.objects.get(username="akadmin")
|
self.user = create_test_admin_user()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_test_call(self):
|
def test_test_call(self):
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import PropertyMapping, User
|
from authentik.core.models import PropertyMapping
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
|
||||||
|
|
||||||
class TestProvidersAPI(APITestCase):
|
class TestProvidersAPI(APITestCase):
|
||||||
@ -13,7 +14,7 @@ class TestProvidersAPI(APITestCase):
|
|||||||
self.mapping = PropertyMapping.objects.create(
|
self.mapping = PropertyMapping.objects.create(
|
||||||
name="dummy", expression="""return {'foo': 'bar'}"""
|
name="dummy", expression="""return {'foo': 'bar'}"""
|
||||||
)
|
)
|
||||||
self.user = User.objects.get(username="akadmin")
|
self.user = create_test_admin_user()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_types(self):
|
def test_types(self):
|
||||||
|
@ -8,6 +8,7 @@ from rest_framework.test import APITestCase
|
|||||||
|
|
||||||
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
|
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
|
||||||
from authentik.core.tasks import clean_expired_models
|
from authentik.core.tasks import clean_expired_models
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
|
||||||
|
|
||||||
class TestTokenAPI(APITestCase):
|
class TestTokenAPI(APITestCase):
|
||||||
@ -16,7 +17,7 @@ class TestTokenAPI(APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.create(username="testuser")
|
self.user = User.objects.create(username="testuser")
|
||||||
self.admin = User.objects.get(username="akadmin")
|
self.admin = create_test_admin_user()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_token_create(self):
|
def test_token_create(self):
|
||||||
|
@ -4,7 +4,7 @@ from django.test import TestCase
|
|||||||
from authentik.core.auth import TokenBackend
|
from authentik.core.auth import TokenBackend
|
||||||
from authentik.core.models import Token, TokenIntents, User
|
from authentik.core.models import Token, TokenIntents, User
|
||||||
from authentik.flows.planner import FlowPlan
|
from authentik.flows.planner import FlowPlan
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.lib.tests.utils import get_request
|
from authentik.lib.tests.utils import get_request
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import USER_ATTRIBUTE_CHANGE_EMAIL, USER_ATTRIBUTE_CHANGE_USERNAME, User
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
||||||
|
from authentik.flows.models import FlowDesignation
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
@ -12,9 +13,37 @@ class TestUsersAPI(APITestCase):
|
|||||||
"""Test Users API"""
|
"""Test Users API"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.admin = User.objects.get(username="akadmin")
|
self.admin = create_test_admin_user()
|
||||||
self.user = User.objects.create(username="test-user")
|
self.user = User.objects.create(username="test-user")
|
||||||
|
|
||||||
|
def test_update_self(self):
|
||||||
|
"""Test update_self"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("authentik_api:user-update-self"), data={"username": "foo", "name": "foo"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_update_self_username_denied(self):
|
||||||
|
"""Test update_self"""
|
||||||
|
self.admin.attributes[USER_ATTRIBUTE_CHANGE_USERNAME] = False
|
||||||
|
self.admin.save()
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("authentik_api:user-update-self"), data={"username": "foo", "name": "foo"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_update_self_email_denied(self):
|
||||||
|
"""Test update_self"""
|
||||||
|
self.admin.attributes[USER_ATTRIBUTE_CHANGE_EMAIL] = False
|
||||||
|
self.admin.save()
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("authentik_api:user-update-self"), data={"email": "foo", "name": "foo"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_metrics(self):
|
def test_metrics(self):
|
||||||
"""Test user's metrics"""
|
"""Test user's metrics"""
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
@ -41,10 +70,8 @@ class TestUsersAPI(APITestCase):
|
|||||||
|
|
||||||
def test_recovery(self):
|
def test_recovery(self):
|
||||||
"""Test user recovery link (no recovery flow set)"""
|
"""Test user recovery link (no recovery flow set)"""
|
||||||
flow = Flow.objects.create(
|
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||||
name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY
|
tenant: Tenant = create_test_tenant()
|
||||||
)
|
|
||||||
tenant: Tenant = Tenant.objects.first()
|
|
||||||
tenant.flow_recovery = flow
|
tenant.flow_recovery = flow
|
||||||
tenant.save()
|
tenant.save()
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
@ -71,10 +98,8 @@ class TestUsersAPI(APITestCase):
|
|||||||
"""Test user recovery link (no email stage)"""
|
"""Test user recovery link (no email stage)"""
|
||||||
self.user.email = "foo@bar.baz"
|
self.user.email = "foo@bar.baz"
|
||||||
self.user.save()
|
self.user.save()
|
||||||
flow = Flow.objects.create(
|
flow = create_test_flow(designation=FlowDesignation.RECOVERY)
|
||||||
name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY
|
tenant: Tenant = create_test_tenant()
|
||||||
)
|
|
||||||
tenant: Tenant = Tenant.objects.first()
|
|
||||||
tenant.flow_recovery = flow
|
tenant.flow_recovery = flow
|
||||||
tenant.save()
|
tenant.save()
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
@ -87,10 +112,8 @@ class TestUsersAPI(APITestCase):
|
|||||||
"""Test user recovery link"""
|
"""Test user recovery link"""
|
||||||
self.user.email = "foo@bar.baz"
|
self.user.email = "foo@bar.baz"
|
||||||
self.user.save()
|
self.user.save()
|
||||||
flow = Flow.objects.create(
|
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||||
name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY
|
tenant: Tenant = create_test_tenant()
|
||||||
)
|
|
||||||
tenant: Tenant = Tenant.objects.first()
|
|
||||||
tenant.flow_recovery = flow
|
tenant.flow_recovery = flow
|
||||||
tenant.save()
|
tenant.save()
|
||||||
|
|
||||||
|
57
authentik/core/tests/utils.py
Normal file
57
authentik/core/tests/utils.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""Test Utils"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
from authentik.core.models import Group, User
|
||||||
|
from authentik.crypto.builder import CertificateBuilder
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_flow(designation: FlowDesignation = FlowDesignation.STAGE_CONFIGURATION) -> Flow:
|
||||||
|
"""Generate a flow that can be used for testing"""
|
||||||
|
uid = generate_id(10)
|
||||||
|
return Flow.objects.create(
|
||||||
|
name=uid,
|
||||||
|
title=uid,
|
||||||
|
slug=slugify(uid),
|
||||||
|
designation=designation,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_admin_user(name: Optional[str] = None) -> User:
|
||||||
|
"""Generate a test-admin user"""
|
||||||
|
uid = generate_id(20) if not name else name
|
||||||
|
group = Group.objects.create(name=uid, is_superuser=True)
|
||||||
|
user: User = User.objects.create(
|
||||||
|
username=uid,
|
||||||
|
name=uid,
|
||||||
|
email=f"{uid}@goauthentik.io",
|
||||||
|
)
|
||||||
|
user.set_password(uid)
|
||||||
|
user.save()
|
||||||
|
group.users.add(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_tenant() -> Tenant:
|
||||||
|
"""Generate a test tenant, removing all other tenants to make sure this one
|
||||||
|
matches."""
|
||||||
|
uid = generate_id(20)
|
||||||
|
Tenant.objects.all().delete()
|
||||||
|
return Tenant.objects.create(domain=uid, default=True)
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_cert() -> CertificateKeyPair:
|
||||||
|
"""Generate a certificate for testing"""
|
||||||
|
CertificateKeyPair.objects.filter(name="goauthentik.io").delete()
|
||||||
|
builder = CertificateBuilder()
|
||||||
|
builder.common_name = "goauthentik.io"
|
||||||
|
builder.build(
|
||||||
|
subject_alt_names=["goauthentik.io"],
|
||||||
|
validity_days=360,
|
||||||
|
)
|
||||||
|
return builder.save()
|
@ -14,4 +14,5 @@ class FlowInterfaceView(TemplateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||||
|
kwargs["inspector"] = "inspector" in self.request.GET
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
@ -20,6 +20,7 @@ from authentik.api.decorators import permission_required
|
|||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.crypto.builder import CertificateBuilder
|
from authentik.crypto.builder import CertificateBuilder
|
||||||
|
from authentik.crypto.managed import MANAGED_KEY
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
@ -99,6 +100,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||||||
"private_key_available",
|
"private_key_available",
|
||||||
"certificate_download_url",
|
"certificate_download_url",
|
||||||
"private_key_download_url",
|
"private_key_download_url",
|
||||||
|
"managed",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"key_data": {"write_only": True},
|
"key_data": {"write_only": True},
|
||||||
@ -134,15 +136,17 @@ class CertificateKeyPairFilter(FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CertificateKeyPair
|
model = CertificateKeyPair
|
||||||
fields = ["name"]
|
fields = ["name", "managed"]
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""CertificateKeyPair Viewset"""
|
"""CertificateKeyPair Viewset"""
|
||||||
|
|
||||||
queryset = CertificateKeyPair.objects.all()
|
queryset = CertificateKeyPair.objects.exclude(managed=MANAGED_KEY)
|
||||||
serializer_class = CertificateKeyPairSerializer
|
serializer_class = CertificateKeyPairSerializer
|
||||||
filterset_class = CertificateKeyPairFilter
|
filterset_class = CertificateKeyPairFilter
|
||||||
|
ordering = ["name"]
|
||||||
|
search_fields = ["name"]
|
||||||
|
|
||||||
@permission_required(None, ["authentik_crypto.add_certificatekeypair"])
|
@permission_required(None, ["authentik_crypto.add_certificatekeypair"])
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
@ -188,7 +192,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
|||||||
secret=certificate,
|
secret=certificate,
|
||||||
type="certificate",
|
type="certificate",
|
||||||
).from_http(request)
|
).from_http(request)
|
||||||
if "download" in request._request.GET:
|
if "download" in request.query_params:
|
||||||
# Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html
|
# Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html
|
||||||
response = HttpResponse(
|
response = HttpResponse(
|
||||||
certificate.certificate_data, content_type="application/x-pem-file"
|
certificate.certificate_data, content_type="application/x-pem-file"
|
||||||
@ -219,7 +223,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
|||||||
secret=certificate,
|
secret=certificate,
|
||||||
type="private_key",
|
type="private_key",
|
||||||
).from_http(request)
|
).from_http(request)
|
||||||
if "download" in request._request.GET:
|
if "download" in request.query_params:
|
||||||
# Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html
|
# Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html
|
||||||
response = HttpResponse(certificate.key_data, content_type="application/x-pem-file")
|
response = HttpResponse(certificate.key_data, content_type="application/x-pem-file")
|
||||||
response[
|
response[
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""authentik crypto app config"""
|
"""authentik crypto app config"""
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
@ -8,3 +10,7 @@ class AuthentikCryptoConfig(AppConfig):
|
|||||||
name = "authentik.crypto"
|
name = "authentik.crypto"
|
||||||
label = "authentik_crypto"
|
label = "authentik_crypto"
|
||||||
verbose_name = "authentik Crypto"
|
verbose_name = "authentik Crypto"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import_module("authentik.crypto.managed")
|
||||||
|
import_module("authentik.crypto.tasks")
|
||||||
|
@ -24,16 +24,17 @@ class CertificateBuilder:
|
|||||||
self.__builder = None
|
self.__builder = None
|
||||||
self.__certificate = None
|
self.__certificate = None
|
||||||
self.common_name = "authentik Self-signed Certificate"
|
self.common_name = "authentik Self-signed Certificate"
|
||||||
|
self.cert = CertificateKeyPair()
|
||||||
|
|
||||||
def save(self) -> Optional[CertificateKeyPair]:
|
def save(self) -> Optional[CertificateKeyPair]:
|
||||||
"""Save generated certificate as model"""
|
"""Save generated certificate as model"""
|
||||||
if not self.__certificate:
|
if not self.__certificate:
|
||||||
raise ValueError("Certificated hasn't been built yet")
|
raise ValueError("Certificated hasn't been built yet")
|
||||||
return CertificateKeyPair.objects.create(
|
self.cert.name = self.common_name
|
||||||
name=self.common_name,
|
self.cert.certificate_data = self.certificate
|
||||||
certificate_data=self.certificate,
|
self.cert.key_data = self.private_key
|
||||||
key_data=self.private_key,
|
self.cert.save()
|
||||||
)
|
return self.cert
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
self,
|
self,
|
||||||
|
40
authentik/crypto/managed.py
Normal file
40
authentik/crypto/managed.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"""Crypto managed objects"""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from authentik.crypto.builder import CertificateBuilder
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
from authentik.managed.manager import ObjectManager
|
||||||
|
|
||||||
|
MANAGED_KEY = "goauthentik.io/crypto/jwt-managed"
|
||||||
|
|
||||||
|
|
||||||
|
class CryptoManager(ObjectManager):
|
||||||
|
"""Crypto managed objects"""
|
||||||
|
|
||||||
|
def _create(self, cert: Optional[CertificateKeyPair] = None):
|
||||||
|
builder = CertificateBuilder()
|
||||||
|
builder.common_name = "goauthentik.io"
|
||||||
|
builder.build(
|
||||||
|
subject_alt_names=["goauthentik.io"],
|
||||||
|
validity_days=360,
|
||||||
|
)
|
||||||
|
if not cert:
|
||||||
|
cert = CertificateKeyPair()
|
||||||
|
cert.certificate_data = builder.certificate
|
||||||
|
cert.key_data = builder.private_key
|
||||||
|
cert.name = "authentik Internal JWT Certificate"
|
||||||
|
cert.managed = MANAGED_KEY
|
||||||
|
cert.save()
|
||||||
|
|
||||||
|
def reconcile(self):
|
||||||
|
certs = CertificateKeyPair.objects.filter(managed=MANAGED_KEY)
|
||||||
|
if not certs.exists():
|
||||||
|
self._create()
|
||||||
|
return []
|
||||||
|
cert: CertificateKeyPair = certs.first()
|
||||||
|
now = datetime.now()
|
||||||
|
if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after:
|
||||||
|
self._create(cert)
|
||||||
|
return []
|
||||||
|
return []
|
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-09 17:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_crypto", "0002_create_self_signed_kp"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="certificatekeypair",
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -13,9 +13,10 @@ from django.db import models
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from authentik.lib.models import CreatedUpdatedModel
|
from authentik.lib.models import CreatedUpdatedModel
|
||||||
|
from authentik.managed.models import ManagedModel
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPair(CreatedUpdatedModel):
|
class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
|
||||||
"""CertificateKeyPair that can be used for signing or encrypting if `key_data`
|
"""CertificateKeyPair that can be used for signing or encrypting if `key_data`
|
||||||
is set, otherwise it can be used to verify remote data."""
|
is set, otherwise it can be used to verify remote data."""
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
|||||||
@property
|
@property
|
||||||
def private_key(self) -> Optional[RSAPrivateKey]:
|
def private_key(self) -> Optional[RSAPrivateKey]:
|
||||||
"""Get python cryptography PrivateKey instance"""
|
"""Get python cryptography PrivateKey instance"""
|
||||||
if not self._private_key and self._private_key != "":
|
if not self._private_key and self.key_data != "":
|
||||||
try:
|
try:
|
||||||
self._private_key = load_pem_private_key(
|
self._private_key = load_pem_private_key(
|
||||||
str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])),
|
str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])),
|
||||||
|
10
authentik/crypto/settings.py
Normal file
10
authentik/crypto/settings.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"""Crypto task Settings"""
|
||||||
|
from celery.schedules import crontab
|
||||||
|
|
||||||
|
CELERY_BEAT_SCHEDULE = {
|
||||||
|
"crypto_certificate_discovery": {
|
||||||
|
"task": "authentik.crypto.tasks.certificate_discovery",
|
||||||
|
"schedule": crontab(minute="*/5"),
|
||||||
|
"options": {"queue": "authentik_scheduled"},
|
||||||
|
},
|
||||||
|
}
|
73
authentik/crypto/tasks.py
Normal file
73
authentik/crypto/tasks.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""Crypto tasks"""
|
||||||
|
from glob import glob
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
from authentik.events.monitored_tasks import (
|
||||||
|
MonitoredTask,
|
||||||
|
TaskResult,
|
||||||
|
TaskResultStatus,
|
||||||
|
prefill_task,
|
||||||
|
)
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
MANAGED_DISCOVERED = "goauthentik.io/crypto/discovered/%s"
|
||||||
|
|
||||||
|
|
||||||
|
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||||
|
@prefill_task
|
||||||
|
def certificate_discovery(self: MonitoredTask):
|
||||||
|
"""Discover and update certificates form the filesystem"""
|
||||||
|
certs = {}
|
||||||
|
private_keys = {}
|
||||||
|
discovered = 0
|
||||||
|
for file in glob(CONFIG.y("cert_discovery_dir") + "/**", recursive=True):
|
||||||
|
path = Path(file)
|
||||||
|
if not path.exists():
|
||||||
|
continue
|
||||||
|
if path.is_dir():
|
||||||
|
continue
|
||||||
|
# Support certbot's directory structure
|
||||||
|
if path.name in ["fullchain.pem", "privkey.pem"]:
|
||||||
|
cert_name = path.parent.name
|
||||||
|
else:
|
||||||
|
cert_name = path.name.replace(path.suffix, "")
|
||||||
|
try:
|
||||||
|
with open(path, "r+", encoding="utf-8") as _file:
|
||||||
|
body = _file.read()
|
||||||
|
if "BEGIN RSA PRIVATE KEY" in body:
|
||||||
|
private_keys[cert_name] = body
|
||||||
|
else:
|
||||||
|
certs[cert_name] = body
|
||||||
|
except OSError as exc:
|
||||||
|
LOGGER.warning("Failed to open file", exc=exc, file=path)
|
||||||
|
discovered += 1
|
||||||
|
for name, cert_data in certs.items():
|
||||||
|
cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first()
|
||||||
|
if not cert:
|
||||||
|
cert = CertificateKeyPair(
|
||||||
|
name=name,
|
||||||
|
managed=MANAGED_DISCOVERED % name,
|
||||||
|
)
|
||||||
|
dirty = False
|
||||||
|
if cert.certificate_data != cert_data:
|
||||||
|
cert.certificate_data = cert_data
|
||||||
|
dirty = True
|
||||||
|
if name in private_keys:
|
||||||
|
if cert.key_data == private_keys[name]:
|
||||||
|
cert.key_data = private_keys[name]
|
||||||
|
dirty = True
|
||||||
|
if dirty:
|
||||||
|
cert.save()
|
||||||
|
self.set_status(
|
||||||
|
TaskResult(
|
||||||
|
TaskResultStatus.SUCCESSFUL,
|
||||||
|
messages=[_("Successfully imported %(count)d files." % {"count": discovered})],
|
||||||
|
)
|
||||||
|
)
|
@ -1,25 +1,37 @@
|
|||||||
"""Crypto tests"""
|
"""Crypto tests"""
|
||||||
import datetime
|
import datetime
|
||||||
|
from os import makedirs
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.api.used_by import DeleteAction
|
from authentik.core.api.used_by import DeleteAction
|
||||||
from authentik.core.models import User
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.crypto.api import CertificateKeyPairSerializer
|
from authentik.crypto.api import CertificateKeyPairSerializer
|
||||||
from authentik.crypto.builder import CertificateBuilder
|
from authentik.crypto.builder import CertificateBuilder
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.flows.models import Flow
|
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.generators import generate_key
|
from authentik.lib.generators import generate_key
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
|
|
||||||
|
|
||||||
class TestCrypto(TestCase):
|
class TestCrypto(APITestCase):
|
||||||
"""Test Crypto validation"""
|
"""Test Crypto validation"""
|
||||||
|
|
||||||
|
def test_model_private(self):
|
||||||
|
"""Test model private key"""
|
||||||
|
cert = CertificateKeyPair.objects.create(
|
||||||
|
name="test",
|
||||||
|
certificate_data="foo",
|
||||||
|
key_data="foo",
|
||||||
|
)
|
||||||
|
self.assertIsNone(cert.private_key)
|
||||||
|
|
||||||
def test_serializer(self):
|
def test_serializer(self):
|
||||||
"""Test API Validation"""
|
"""Test API Validation"""
|
||||||
keypair = CertificateKeyPair.objects.first()
|
keypair = create_test_cert()
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
CertificateKeyPairSerializer(
|
CertificateKeyPairSerializer(
|
||||||
data={
|
data={
|
||||||
@ -54,10 +66,38 @@ class TestCrypto(TestCase):
|
|||||||
self.assertEqual(instance.name, "test-cert")
|
self.assertEqual(instance.name, "test-cert")
|
||||||
self.assertEqual((instance.certificate.not_valid_after - now).days, 2)
|
self.assertEqual((instance.certificate.not_valid_after - now).days, 2)
|
||||||
|
|
||||||
|
def test_builder_api(self):
|
||||||
|
"""Test Builder (via API)"""
|
||||||
|
self.client.force_login(create_test_admin_user())
|
||||||
|
self.client.post(
|
||||||
|
reverse("authentik_api:certificatekeypair-generate"),
|
||||||
|
data={"common_name": "foo", "subject_alt_name": "bar,baz", "validity_days": 3},
|
||||||
|
)
|
||||||
|
self.assertTrue(CertificateKeyPair.objects.filter(name="foo").exists())
|
||||||
|
|
||||||
|
def test_builder_api_invalid(self):
|
||||||
|
"""Test Builder (via API) (invalid)"""
|
||||||
|
self.client.force_login(create_test_admin_user())
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:certificatekeypair-generate"),
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
"""Test API List"""
|
||||||
|
self.client.force_login(create_test_admin_user())
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"authentik_api:certificatekeypair-list",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
def test_certificate_download(self):
|
def test_certificate_download(self):
|
||||||
"""Test certificate export (download)"""
|
"""Test certificate export (download)"""
|
||||||
self.client.force_login(User.objects.get(username="akadmin"))
|
self.client.force_login(create_test_admin_user())
|
||||||
keypair = CertificateKeyPair.objects.first()
|
keypair = create_test_cert()
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_api:certificatekeypair-view-certificate",
|
"authentik_api:certificatekeypair-view-certificate",
|
||||||
@ -77,8 +117,8 @@ class TestCrypto(TestCase):
|
|||||||
|
|
||||||
def test_private_key_download(self):
|
def test_private_key_download(self):
|
||||||
"""Test private_key export (download)"""
|
"""Test private_key export (download)"""
|
||||||
self.client.force_login(User.objects.get(username="akadmin"))
|
self.client.force_login(create_test_admin_user())
|
||||||
keypair = CertificateKeyPair.objects.first()
|
keypair = create_test_cert()
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_api:certificatekeypair-view-private-key",
|
"authentik_api:certificatekeypair-view-private-key",
|
||||||
@ -98,15 +138,15 @@ class TestCrypto(TestCase):
|
|||||||
|
|
||||||
def test_used_by(self):
|
def test_used_by(self):
|
||||||
"""Test used_by endpoint"""
|
"""Test used_by endpoint"""
|
||||||
self.client.force_login(User.objects.get(username="akadmin"))
|
self.client.force_login(create_test_admin_user())
|
||||||
keypair = CertificateKeyPair.objects.first()
|
keypair = create_test_cert()
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
client_secret=generate_key(),
|
client_secret=generate_key(),
|
||||||
authorization_flow=Flow.objects.first(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://localhost",
|
redirect_uris="http://localhost",
|
||||||
rsa_key=CertificateKeyPair.objects.first(),
|
rsa_key=keypair,
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
@ -127,3 +167,33 @@ class TestCrypto(TestCase):
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_discovery(self):
|
||||||
|
"""Test certificate discovery"""
|
||||||
|
builder = CertificateBuilder()
|
||||||
|
builder.common_name = "test-cert"
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
builder.save()
|
||||||
|
builder.build(
|
||||||
|
subject_alt_names=[],
|
||||||
|
validity_days=3,
|
||||||
|
)
|
||||||
|
with TemporaryDirectory() as temp_dir:
|
||||||
|
with open(f"{temp_dir}/foo.pem", "w+", encoding="utf-8") as _cert:
|
||||||
|
_cert.write(builder.certificate)
|
||||||
|
with open(f"{temp_dir}/foo.key", "w+", encoding="utf-8") as _key:
|
||||||
|
_key.write(builder.private_key)
|
||||||
|
makedirs(f"{temp_dir}/foo.bar", exist_ok=True)
|
||||||
|
with open(f"{temp_dir}/foo.bar/fullchain.pem", "w+", encoding="utf-8") as _cert:
|
||||||
|
_cert.write(builder.certificate)
|
||||||
|
with open(f"{temp_dir}/foo.bar/privkey.pem", "w+", encoding="utf-8") as _key:
|
||||||
|
_key.write(builder.private_key)
|
||||||
|
with CONFIG.patch("cert_discovery_dir", temp_dir):
|
||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
certificate_discovery() # pylint: disable=no-value-for-parameter
|
||||||
|
self.assertTrue(
|
||||||
|
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo").exists()
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists()
|
||||||
|
)
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
"""NotificationTransport API Views"""
|
"""NotificationTransport API Views"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
from rest_framework.fields import CharField, ListField, 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
|
||||||
@ -29,6 +32,14 @@ class NotificationTransportSerializer(ModelSerializer):
|
|||||||
"""Return selected mode with a UI Label"""
|
"""Return selected mode with a UI Label"""
|
||||||
return TransportMode(instance.mode).label
|
return TransportMode(instance.mode).label
|
||||||
|
|
||||||
|
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||||
|
"""Ensure the required fields are set."""
|
||||||
|
mode = attrs.get("mode")
|
||||||
|
if mode in [TransportMode.WEBHOOK, TransportMode.WEBHOOK_SLACK]:
|
||||||
|
if "webhook_url" not in attrs or attrs.get("webhook_url", "") == "":
|
||||||
|
raise ValidationError("Webhook URL may not be empty.")
|
||||||
|
return attrs
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = NotificationTransport
|
model = NotificationTransport
|
||||||
|
@ -7,16 +7,25 @@ from django.core.exceptions import SuspiciousOperation
|
|||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django_otp.plugins.otp_static.models import StaticToken
|
||||||
from guardian.models import UserObjectPermission
|
from guardian.models import UserObjectPermission
|
||||||
|
|
||||||
from authentik.core.middleware import LOCAL
|
from authentik.core.middleware import LOCAL
|
||||||
from authentik.core.models import User
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
from authentik.events.models import Event, EventAction, Notification
|
from authentik.events.models import Event, EventAction, Notification
|
||||||
from authentik.events.signals import EventNewThread
|
from authentik.events.signals import EventNewThread
|
||||||
from authentik.events.utils import model_to_dict
|
from authentik.events.utils import model_to_dict
|
||||||
from authentik.lib.sentry import before_send
|
from authentik.lib.sentry import before_send
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
|
|
||||||
|
IGNORED_MODELS = (
|
||||||
|
Event,
|
||||||
|
Notification,
|
||||||
|
UserObjectPermission,
|
||||||
|
AuthenticatedSession,
|
||||||
|
StaticToken,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuditMiddleware:
|
class AuditMiddleware:
|
||||||
"""Register handlers for duration of request-response that log creation/update/deletion
|
"""Register handlers for duration of request-response that log creation/update/deletion
|
||||||
@ -82,7 +91,7 @@ class AuditMiddleware:
|
|||||||
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
|
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
|
||||||
):
|
):
|
||||||
"""Signal handler for all object's post_save"""
|
"""Signal handler for all object's post_save"""
|
||||||
if isinstance(instance, (Event, Notification, UserObjectPermission)):
|
if isinstance(instance, IGNORED_MODELS):
|
||||||
return
|
return
|
||||||
|
|
||||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||||
@ -92,7 +101,7 @@ class AuditMiddleware:
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def pre_delete_handler(user: User, request: HttpRequest, sender, instance: Model, **_):
|
def pre_delete_handler(user: User, request: HttpRequest, sender, instance: Model, **_):
|
||||||
"""Signal handler for all object's pre_delete"""
|
"""Signal handler for all object's pre_delete"""
|
||||||
if isinstance(instance, (Event, Notification, UserObjectPermission)): # pragma: no cover
|
if isinstance(instance, IGNORED_MODELS): # pragma: no cover
|
||||||
return
|
return
|
||||||
|
|
||||||
EventNewThread(
|
EventNewThread(
|
||||||
|
@ -0,0 +1,833 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-10 16:01
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
import authentik.events.models
|
||||||
|
import authentik.lib.models
|
||||||
|
from authentik.events.models import EventAction, NotificationSeverity, TransportMode
|
||||||
|
|
||||||
|
|
||||||
|
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
Event = apps.get_model("authentik_events", "Event")
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
for event in Event.objects.all():
|
||||||
|
event.delete()
|
||||||
|
# Because event objects cannot be updated, we have to re-create them
|
||||||
|
event.pk = None
|
||||||
|
event.user_json = authentik.events.models.get_user(event.user) if event.user else {}
|
||||||
|
event._state.adding = True
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
|
||||||
|
def notify_configuration_error(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
Group = apps.get_model("authentik_core", "Group")
|
||||||
|
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
|
||||||
|
EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
|
||||||
|
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
|
||||||
|
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
|
||||||
|
|
||||||
|
admin_group = (
|
||||||
|
Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
|
||||||
|
)
|
||||||
|
|
||||||
|
policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-match-configuration-error",
|
||||||
|
defaults={"action": EventAction.CONFIGURATION_ERROR},
|
||||||
|
)
|
||||||
|
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-notify-configuration-error",
|
||||||
|
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
|
||||||
|
)
|
||||||
|
trigger.transports.set(
|
||||||
|
NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
|
||||||
|
)
|
||||||
|
trigger.save()
|
||||||
|
PolicyBinding.objects.using(db_alias).update_or_create(
|
||||||
|
target=trigger,
|
||||||
|
policy=policy,
|
||||||
|
defaults={
|
||||||
|
"order": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_update(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
Group = apps.get_model("authentik_core", "Group")
|
||||||
|
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
|
||||||
|
EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
|
||||||
|
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
|
||||||
|
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
|
||||||
|
|
||||||
|
admin_group = (
|
||||||
|
Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
|
||||||
|
)
|
||||||
|
|
||||||
|
policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-match-update",
|
||||||
|
defaults={"action": EventAction.UPDATE_AVAILABLE},
|
||||||
|
)
|
||||||
|
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-notify-update",
|
||||||
|
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
|
||||||
|
)
|
||||||
|
trigger.transports.set(
|
||||||
|
NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
|
||||||
|
)
|
||||||
|
trigger.save()
|
||||||
|
PolicyBinding.objects.using(db_alias).update_or_create(
|
||||||
|
target=trigger,
|
||||||
|
policy=policy,
|
||||||
|
defaults={
|
||||||
|
"order": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_exception(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
Group = apps.get_model("authentik_core", "Group")
|
||||||
|
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
|
||||||
|
EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
|
||||||
|
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
|
||||||
|
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
|
||||||
|
|
||||||
|
admin_group = (
|
||||||
|
Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
|
||||||
|
)
|
||||||
|
|
||||||
|
policy_policy_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-match-policy-exception",
|
||||||
|
defaults={"action": EventAction.POLICY_EXCEPTION},
|
||||||
|
)
|
||||||
|
policy_pm_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-match-property-mapping-exception",
|
||||||
|
defaults={"action": EventAction.PROPERTY_MAPPING_EXCEPTION},
|
||||||
|
)
|
||||||
|
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-notify-exception",
|
||||||
|
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
|
||||||
|
)
|
||||||
|
trigger.transports.set(
|
||||||
|
NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
|
||||||
|
)
|
||||||
|
trigger.save()
|
||||||
|
PolicyBinding.objects.using(db_alias).update_or_create(
|
||||||
|
target=trigger,
|
||||||
|
policy=policy_policy_exc,
|
||||||
|
defaults={
|
||||||
|
"order": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
PolicyBinding.objects.using(db_alias).update_or_create(
|
||||||
|
target=trigger,
|
||||||
|
policy=policy_pm_exc,
|
||||||
|
defaults={
|
||||||
|
"order": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def transport_email_global(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
|
||||||
|
|
||||||
|
NotificationTransport.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-email-transport",
|
||||||
|
defaults={"mode": TransportMode.EMAIL},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def token_view_to_secret_view(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
from authentik.events.models import EventAction
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
Event = apps.get_model("authentik_events", "Event")
|
||||||
|
|
||||||
|
events = Event.objects.using(db_alias).filter(action="token_view")
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
event.context["secret"] = event.context.pop("token")
|
||||||
|
event.action = EventAction.SECRET_VIEW
|
||||||
|
|
||||||
|
Event.objects.using(db_alias).bulk_update(events, ["context", "action"])
|
||||||
|
|
||||||
|
|
||||||
|
# Taken from https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
|
||||||
|
def progress_bar(
|
||||||
|
iterable: Iterable,
|
||||||
|
prefix="Writing: ",
|
||||||
|
suffix=" finished",
|
||||||
|
decimals=1,
|
||||||
|
length=100,
|
||||||
|
fill="█",
|
||||||
|
print_end="\r",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Call in a loop to create terminal progress bar
|
||||||
|
@params:
|
||||||
|
iteration - Required : current iteration (Int)
|
||||||
|
total - Required : total iterations (Int)
|
||||||
|
prefix - Optional : prefix string (Str)
|
||||||
|
suffix - Optional : suffix string (Str)
|
||||||
|
decimals - Optional : positive number of decimals in percent complete (Int)
|
||||||
|
length - Optional : character length of bar (Int)
|
||||||
|
fill - Optional : bar fill character (Str)
|
||||||
|
print_end - Optional : end character (e.g. "\r", "\r\n") (Str)
|
||||||
|
"""
|
||||||
|
total = len(iterable)
|
||||||
|
if total < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
def print_progress_bar(iteration):
|
||||||
|
"""Progress Bar Printing Function"""
|
||||||
|
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||||
|
filledLength = int(length * iteration // total)
|
||||||
|
bar = fill * filledLength + "-" * (length - filledLength)
|
||||||
|
print(f"\r{prefix} |{bar}| {percent}% {suffix}", end=print_end)
|
||||||
|
|
||||||
|
# Initial Call
|
||||||
|
print_progress_bar(0)
|
||||||
|
# Update Progress Bar
|
||||||
|
for i, item in enumerate(iterable):
|
||||||
|
yield item
|
||||||
|
print_progress_bar(i + 1)
|
||||||
|
# Print New Line on Complete
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def update_expires(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
Event = apps.get_model("authentik_events", "event")
|
||||||
|
all_events = Event.objects.using(db_alias).all()
|
||||||
|
if all_events.count() < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\nAdding expiry to events, this might take a couple of minutes...")
|
||||||
|
for event in progress_bar(all_events):
|
||||||
|
event.expires = event.created + timedelta(days=365)
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [
|
||||||
|
("authentik_events", "0001_initial"),
|
||||||
|
("authentik_events", "0002_auto_20200918_2116"),
|
||||||
|
("authentik_events", "0003_auto_20200917_1155"),
|
||||||
|
("authentik_events", "0004_auto_20200921_1829"),
|
||||||
|
("authentik_events", "0005_auto_20201005_2139"),
|
||||||
|
("authentik_events", "0006_auto_20201017_2024"),
|
||||||
|
("authentik_events", "0007_auto_20201215_0939"),
|
||||||
|
("authentik_events", "0008_auto_20201220_1651"),
|
||||||
|
("authentik_events", "0009_auto_20201227_1210"),
|
||||||
|
("authentik_events", "0010_notification_notificationtransport_notificationrule"),
|
||||||
|
("authentik_events", "0011_notification_rules_default_v1"),
|
||||||
|
("authentik_events", "0012_auto_20210202_1821"),
|
||||||
|
("authentik_events", "0013_auto_20210209_1657"),
|
||||||
|
("authentik_events", "0014_expiry"),
|
||||||
|
("authentik_events", "0015_alter_event_action"),
|
||||||
|
("authentik_events", "0016_add_tenant"),
|
||||||
|
("authentik_events", "0017_alter_event_action"),
|
||||||
|
("authentik_events", "0018_auto_20210911_2217"),
|
||||||
|
("authentik_events", "0019_alter_notificationtransport_webhook_url"),
|
||||||
|
]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_policies", "0004_policy_execution_logging"),
|
||||||
|
("authentik_core", "0016_auto_20201202_2234"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("authentik_policies_event_matcher", "0003_auto_20210110_1907"),
|
||||||
|
("authentik_core", "0028_alter_token_intent"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Event",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"event_uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"action",
|
||||||
|
models.TextField(
|
||||||
|
choices=[
|
||||||
|
("LOGIN", "login"),
|
||||||
|
("LOGIN_FAILED", "login_failed"),
|
||||||
|
("LOGOUT", "logout"),
|
||||||
|
("AUTHORIZE_APPLICATION", "authorize_application"),
|
||||||
|
("SUSPICIOUS_REQUEST", "suspicious_request"),
|
||||||
|
("SIGN_UP", "sign_up"),
|
||||||
|
("PASSWORD_RESET", "password_reset"),
|
||||||
|
("INVITE_CREATED", "invitation_created"),
|
||||||
|
("INVITE_USED", "invitation_used"),
|
||||||
|
("IMPERSONATION_STARTED", "impersonation_started"),
|
||||||
|
("IMPERSONATION_ENDED", "impersonation_ended"),
|
||||||
|
("CUSTOM", "custom"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("app", models.TextField()),
|
||||||
|
("context", models.JSONField(blank=True, default=dict)),
|
||||||
|
("client_ip", models.GenericIPAddressField(null=True)),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("user_json", models.JSONField(default=dict)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Event",
|
||||||
|
"verbose_name_plural": "Events",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=convert_user_to_json,
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="event",
|
||||||
|
name="user",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="event",
|
||||||
|
old_name="user_json",
|
||||||
|
new_name="user",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("sign_up", "Sign Up"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("invitation_created", "Invite Created"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("user_write", "User Write"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("invitation_created", "Invite Created"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="event",
|
||||||
|
name="date",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("user_write", "User Write"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("token_view", "Token View"),
|
||||||
|
("invitation_created", "Invite Created"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("user_write", "User Write"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("token_view", "Token View"),
|
||||||
|
("invitation_created", "Invite Created"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("policy_execution", "Policy Execution"),
|
||||||
|
("policy_exception", "Policy Exception"),
|
||||||
|
("property_mapping_exception", "Property Mapping Exception"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("user_write", "User Write"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("token_view", "Token View"),
|
||||||
|
("invitation_created", "Invite Created"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("policy_execution", "Policy Execution"),
|
||||||
|
("policy_exception", "Policy Exception"),
|
||||||
|
("property_mapping_exception", "Property Mapping Exception"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("update_available", "Update Available"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("user_write", "User Write"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("token_view", "Token View"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("policy_execution", "Policy Execution"),
|
||||||
|
("policy_exception", "Policy Exception"),
|
||||||
|
("property_mapping_exception", "Property Mapping Exception"),
|
||||||
|
("configuration_error", "Configuration Error"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("update_available", "Update Available"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="NotificationTransport",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.TextField(unique=True)),
|
||||||
|
(
|
||||||
|
"mode",
|
||||||
|
models.TextField(
|
||||||
|
choices=[
|
||||||
|
("webhook", "Generic Webhook"),
|
||||||
|
("webhook_slack", "Slack Webhook (Slack/Discord)"),
|
||||||
|
("email", "Email"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("webhook_url", models.TextField(blank=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Notification Transport",
|
||||||
|
"verbose_name_plural": "Notification Transports",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="NotificationRule",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"policybindingmodel_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="authentik_policies.policybindingmodel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.TextField(unique=True)),
|
||||||
|
(
|
||||||
|
"severity",
|
||||||
|
models.TextField(
|
||||||
|
choices=[("notice", "Notice"), ("warning", "Warning"), ("alert", "Alert")],
|
||||||
|
default="notice",
|
||||||
|
help_text="Controls which severity level the created notifications will have.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"group",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Define which group of users this notification should be sent and shown to. If left empty, Notification won't ben sent.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="authentik_core.group",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"transports",
|
||||||
|
models.ManyToManyField(
|
||||||
|
help_text="Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",
|
||||||
|
to="authentik_events.NotificationTransport",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Notification Rule",
|
||||||
|
"verbose_name_plural": "Notification Rules",
|
||||||
|
},
|
||||||
|
bases=("authentik_policies.policybindingmodel",),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Notification",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"severity",
|
||||||
|
models.TextField(
|
||||||
|
choices=[("notice", "Notice"), ("warning", "Warning"), ("alert", "Alert")]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("body", models.TextField()),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("seen", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"event",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="authentik_events.event",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Notification",
|
||||||
|
"verbose_name_plural": "Notifications",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=transport_email_global,
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=notify_configuration_error,
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=notify_update,
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=notify_exception,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="notificationtransport",
|
||||||
|
name="send_once",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Only send notification once, for example when sending a webhook into a chat channel.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("user_write", "User Write"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("token_view", "Token View"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("policy_execution", "Policy Execution"),
|
||||||
|
("policy_exception", "Policy Exception"),
|
||||||
|
("property_mapping_exception", "Property Mapping Exception"),
|
||||||
|
("system_task_execution", "System Task Execution"),
|
||||||
|
("system_task_exception", "System Task Exception"),
|
||||||
|
("configuration_error", "Configuration Error"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("update_available", "Update Available"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("user_write", "User Write"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("secret_view", "Secret View"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("policy_execution", "Policy Execution"),
|
||||||
|
("policy_exception", "Policy Exception"),
|
||||||
|
("property_mapping_exception", "Property Mapping Exception"),
|
||||||
|
("system_task_execution", "System Task Execution"),
|
||||||
|
("system_task_exception", "System Task Exception"),
|
||||||
|
("configuration_error", "Configuration Error"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("update_available", "Update Available"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=token_view_to_secret_view,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="event",
|
||||||
|
name="expires",
|
||||||
|
field=models.DateTimeField(default=authentik.events.models.default_event_duration),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="event",
|
||||||
|
name="expiring",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=update_expires,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("user_write", "User Write"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("secret_view", "Secret View"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("policy_execution", "Policy Execution"),
|
||||||
|
("policy_exception", "Policy Exception"),
|
||||||
|
("property_mapping_exception", "Property Mapping Exception"),
|
||||||
|
("system_task_execution", "System Task Execution"),
|
||||||
|
("system_task_exception", "System Task Exception"),
|
||||||
|
("configuration_error", "Configuration Error"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("email_sent", "Email Sent"),
|
||||||
|
("update_available", "Update Available"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="event",
|
||||||
|
name="tenant",
|
||||||
|
field=models.JSONField(blank=True, default=authentik.events.models.default_tenant),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("user_write", "User Write"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("secret_view", "Secret View"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("policy_execution", "Policy Execution"),
|
||||||
|
("policy_exception", "Policy Exception"),
|
||||||
|
("property_mapping_exception", "Property Mapping Exception"),
|
||||||
|
("system_task_execution", "System Task Execution"),
|
||||||
|
("system_task_exception", "System Task Exception"),
|
||||||
|
("system_exception", "System Exception"),
|
||||||
|
("configuration_error", "Configuration Error"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("email_sent", "Email Sent"),
|
||||||
|
("update_available", "Update Available"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("user_write", "User Write"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("secret_view", "Secret View"),
|
||||||
|
("secret_rotate", "Secret Rotate"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("policy_execution", "Policy Execution"),
|
||||||
|
("policy_exception", "Policy Exception"),
|
||||||
|
("property_mapping_exception", "Property Mapping Exception"),
|
||||||
|
("system_task_execution", "System Task Execution"),
|
||||||
|
("system_task_exception", "System Task Exception"),
|
||||||
|
("system_exception", "System Exception"),
|
||||||
|
("configuration_error", "Configuration Error"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("email_sent", "Email Sent"),
|
||||||
|
("update_available", "Update Available"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="NotificationWebhookMapping",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"propertymapping_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="authentik_core.propertymapping",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Notification Webhook Mapping",
|
||||||
|
"verbose_name_plural": "Notification Webhook Mappings",
|
||||||
|
},
|
||||||
|
bases=("authentik_core.propertymapping",),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="notificationtransport",
|
||||||
|
name="webhook_mapping",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||||
|
to="authentik_events.notificationwebhookmapping",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="notificationtransport",
|
||||||
|
name="webhook_url",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True, validators=[authentik.lib.models.DomainlessURLValidator()]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2021-10-04 15:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import authentik.lib.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_events", "0018_auto_20210911_2217"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="notificationtransport",
|
||||||
|
name="webhook_url",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True, validators=[authentik.lib.models.DomainlessURLValidator()]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -19,6 +19,7 @@ from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION
|
|||||||
from authentik.core.models import ExpiringModel, Group, PropertyMapping, User
|
from authentik.core.models import ExpiringModel, Group, PropertyMapping, User
|
||||||
from authentik.events.geo import GEOIP_READER
|
from authentik.events.geo import GEOIP_READER
|
||||||
from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict
|
from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict
|
||||||
|
from authentik.lib.models import DomainlessURLValidator
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.lib.utils.http import get_client_ip, get_http_session
|
from authentik.lib.utils.http import get_client_ip, get_http_session
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
@ -223,7 +224,7 @@ class NotificationTransport(models.Model):
|
|||||||
name = models.TextField(unique=True)
|
name = models.TextField(unique=True)
|
||||||
mode = models.TextField(choices=TransportMode.choices)
|
mode = models.TextField(choices=TransportMode.choices)
|
||||||
|
|
||||||
webhook_url = models.TextField(blank=True)
|
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
|
||||||
webhook_mapping = models.ForeignKey(
|
webhook_mapping = models.ForeignKey(
|
||||||
"NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
|
"NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
|
||||||
)
|
)
|
||||||
|
@ -7,7 +7,9 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
from celery import Task
|
from celery import Task
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from prometheus_client import Gauge
|
from prometheus_client import Gauge
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
@ -18,6 +20,8 @@ GAUGE_TASKS = Gauge(
|
|||||||
["task_name", "task_uid", "status"],
|
["task_name", "task_uid", "status"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class TaskResultStatus(Enum):
|
class TaskResultStatus(Enum):
|
||||||
"""Possible states of tasks"""
|
"""Possible states of tasks"""
|
||||||
@ -25,6 +29,7 @@ class TaskResultStatus(Enum):
|
|||||||
SUCCESSFUL = 1
|
SUCCESSFUL = 1
|
||||||
WARNING = 2
|
WARNING = 2
|
||||||
ERROR = 4
|
ERROR = 4
|
||||||
|
UNKNOWN = 8
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -76,7 +81,7 @@ class TaskInfo:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def by_name(name: str) -> Optional["TaskInfo"]:
|
def by_name(name: str) -> Optional["TaskInfo"]:
|
||||||
"""Get TaskInfo Object by name"""
|
"""Get TaskInfo Object by name"""
|
||||||
return cache.get(f"task_{name}")
|
return cache.get(f"task_{name}", None)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""Delete task info from cache"""
|
"""Delete task info from cache"""
|
||||||
@ -181,5 +186,21 @@ class MonitoredTask(Task):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
for task in TaskInfo.all().values():
|
def prefill_task(func):
|
||||||
task.set_prom_metrics()
|
"""Ensure a task's details are always in cache, so it can always be triggered via API"""
|
||||||
|
status = TaskInfo.by_name(func.__name__)
|
||||||
|
if status:
|
||||||
|
return func
|
||||||
|
TaskInfo(
|
||||||
|
task_name=func.__name__,
|
||||||
|
task_description=func.__doc__,
|
||||||
|
result=TaskResult(TaskResultStatus.UNKNOWN, messages=[_("Task has not been run yet.")]),
|
||||||
|
task_call_module=func.__module__,
|
||||||
|
task_call_func=func.__name__,
|
||||||
|
# We don't have real values for these attributes but they cannot be null
|
||||||
|
start_timestamp=default_timer(),
|
||||||
|
finish_timestamp=default_timer(),
|
||||||
|
finish_time=datetime.now(),
|
||||||
|
).save(86400)
|
||||||
|
LOGGER.debug("prefilled task", task_name=func.__name__)
|
||||||
|
return func
|
||||||
|
@ -3,16 +3,16 @@ from threading import Thread
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
|
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.signals import password_changed
|
from authentik.core.signals import password_changed
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.events.tasks import event_notification_handler
|
from authentik.events.tasks import event_notification_handler, gdpr_cleanup
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.stages.invitation.models import Invitation
|
from authentik.stages.invitation.models import Invitation
|
||||||
from authentik.stages.invitation.signals import invitation_used
|
from authentik.stages.invitation.signals import invitation_used
|
||||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||||
@ -108,3 +108,10 @@ def on_password_changed(sender, user: User, password: str, **_):
|
|||||||
def event_post_save_notification(sender, instance: Event, **_):
|
def event_post_save_notification(sender, instance: Event, **_):
|
||||||
"""Start task to check if any policies trigger an notification on this event"""
|
"""Start task to check if any policies trigger an notification on this event"""
|
||||||
event_notification_handler.delay(instance.event_uuid.hex)
|
event_notification_handler.delay(instance.event_uuid.hex)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_delete, sender=User)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def event_user_pre_delete_cleanup(sender, instance: User, **_):
|
||||||
|
"""If gdpr_compliance is enabled, remove all the user's events"""
|
||||||
|
gdpr_cleanup.delay(instance.pk)
|
||||||
|
@ -98,9 +98,19 @@ def notification_transport(self: MonitoredTask, notification_pk: int, transport_
|
|||||||
notification: Notification = Notification.objects.filter(pk=notification_pk).first()
|
notification: Notification = Notification.objects.filter(pk=notification_pk).first()
|
||||||
if not notification:
|
if not notification:
|
||||||
return
|
return
|
||||||
transport: NotificationTransport = NotificationTransport.objects.get(pk=transport_pk)
|
transport = NotificationTransport.objects.filter(pk=transport_pk).first()
|
||||||
|
if not transport:
|
||||||
|
return
|
||||||
transport.send(notification)
|
transport.send(notification)
|
||||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
|
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
|
||||||
except NotificationTransportError as exc:
|
except NotificationTransportError as exc:
|
||||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
@CELERY_APP.task()
|
||||||
|
def gdpr_cleanup(user_pk: int):
|
||||||
|
"""cleanup events from gdpr_compliance"""
|
||||||
|
events = Event.objects.filter(user__pk=user_pk)
|
||||||
|
LOGGER.debug("GDPR cleanup, removing events from user", events=events.count())
|
||||||
|
events.delete()
|
||||||
|
@ -3,15 +3,21 @@
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.events.models import Event, EventAction, Notification, NotificationSeverity
|
from authentik.events.models import (
|
||||||
|
Event,
|
||||||
|
EventAction,
|
||||||
|
Notification,
|
||||||
|
NotificationSeverity,
|
||||||
|
TransportMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestEventsAPI(APITestCase):
|
class TestEventsAPI(APITestCase):
|
||||||
"""Test Event API"""
|
"""Test Event API"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = User.objects.get(username="akadmin")
|
self.user = create_test_admin_user()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_top_n(self):
|
def test_top_n(self):
|
||||||
@ -41,3 +47,23 @@ class TestEventsAPI(APITestCase):
|
|||||||
)
|
)
|
||||||
notification.refresh_from_db()
|
notification.refresh_from_db()
|
||||||
self.assertTrue(notification.seen)
|
self.assertTrue(notification.seen)
|
||||||
|
|
||||||
|
def test_transport(self):
|
||||||
|
"""Test transport API"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:notificationtransport-list"),
|
||||||
|
data={
|
||||||
|
"name": "foo-with",
|
||||||
|
"mode": TransportMode.WEBHOOK,
|
||||||
|
"webhook_url": "http://foo.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:notificationtransport-list"),
|
||||||
|
data={
|
||||||
|
"name": "foo-without",
|
||||||
|
"mode": TransportMode.WEBHOOK,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ class TestEventsMiddleware(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.get(username="akadmin")
|
self.user = create_test_admin_user()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
|
@ -77,7 +77,7 @@ def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:
|
|||||||
final_dict = {}
|
final_dict = {}
|
||||||
for key, value in source.items():
|
for key, value in source.items():
|
||||||
if is_dataclass(value):
|
if is_dataclass(value):
|
||||||
# Because asdict calls `copy.deepcopy(obj)` on everything thats not tuple/dict,
|
# Because asdict calls `copy.deepcopy(obj)` on everything that's not tuple/dict,
|
||||||
# and deepcopy doesn't work with HttpRequests (neither django nor rest_framework).
|
# and deepcopy doesn't work with HttpRequests (neither django nor rest_framework).
|
||||||
# Currently, the only dataclass that actually holds an http request is a PolicyRequest
|
# Currently, the only dataclass that actually holds an http request is a PolicyRequest
|
||||||
if isinstance(value, PolicyRequest):
|
if isinstance(value, PolicyRequest):
|
||||||
|
@ -32,7 +32,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cach
|
|||||||
from authentik.flows.transfer.common import DataclassEncoder
|
from authentik.flows.transfer.common import DataclassEncoder
|
||||||
from authentik.flows.transfer.exporter import FlowExporter
|
from authentik.flows.transfer.exporter import FlowExporter
|
||||||
from authentik.flows.transfer.importer import FlowImporter
|
from authentik.flows.transfer.importer import FlowImporter
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -108,6 +108,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
queryset = Flow.objects.all()
|
queryset = Flow.objects.all()
|
||||||
serializer_class = FlowSerializer
|
serializer_class = FlowSerializer
|
||||||
lookup_field = "slug"
|
lookup_field = "slug"
|
||||||
|
ordering = ["slug", "name"]
|
||||||
search_fields = ["name", "slug", "designation", "title"]
|
search_fields = ["name", "slug", "designation", "title"]
|
||||||
filterset_fields = ["flow_uuid", "name", "slug", "designation"]
|
filterset_fields = ["flow_uuid", "name", "slug", "designation"]
|
||||||
|
|
||||||
@ -333,6 +334,9 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def execute(self, request: Request, slug: str):
|
def execute(self, request: Request, slug: str):
|
||||||
"""Execute flow for current user"""
|
"""Execute flow for current user"""
|
||||||
|
# Because we pre-plan the flow here, and not in the planner, we need to manually clear
|
||||||
|
# the history of the inspector
|
||||||
|
request.session[SESSION_KEY_HISTORY] = []
|
||||||
flow: Flow = self.get_object()
|
flow: Flow = self.get_object()
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
planner.use_cache = False
|
planner.use_cache = False
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
"""Flow Stage API Views"""
|
"""Flow Stage API Views"""
|
||||||
from typing import Iterable
|
|
||||||
|
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
@ -15,7 +13,7 @@ from authentik.core.api.used_by import UsedByMixin
|
|||||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||||
from authentik.core.types import UserSettingSerializer
|
from authentik.core.types import UserSettingSerializer
|
||||||
from authentik.flows.api.flows import FlowSerializer
|
from authentik.flows.api.flows import FlowSerializer
|
||||||
from authentik.flows.models import Stage
|
from authentik.flows.models import ConfigurableStage, Stage
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -43,6 +41,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
"component",
|
"component",
|
||||||
"verbose_name",
|
"verbose_name",
|
||||||
"verbose_name_plural",
|
"verbose_name_plural",
|
||||||
|
"meta_model_name",
|
||||||
"flow_set",
|
"flow_set",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -86,9 +85,11 @@ class StageViewSet(
|
|||||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
def user_settings(self, request: Request) -> Response:
|
def user_settings(self, request: Request) -> Response:
|
||||||
"""Get all stages the user can configure"""
|
"""Get all stages the user can configure"""
|
||||||
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses().order_by("name")
|
stages = []
|
||||||
|
for configurable_stage in all_subclasses(ConfigurableStage):
|
||||||
|
stages += list(configurable_stage.objects.all().order_by("name"))
|
||||||
matching_stages: list[dict] = []
|
matching_stages: list[dict] = []
|
||||||
for stage in _all_stages:
|
for stage in stages:
|
||||||
user_settings = stage.ui_user_settings
|
user_settings = stage.ui_user_settings
|
||||||
if not user_settings:
|
if not user_settings:
|
||||||
continue
|
continue
|
||||||
|
@ -10,7 +10,7 @@ from django.test import RequestFactory
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
from authentik.core.models import User
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ class Command(BaseCommand): # pragma: no cover
|
|||||||
def benchmark_flows(self, proc_count):
|
def benchmark_flows(self, proc_count):
|
||||||
"""Get full recovery link"""
|
"""Get full recovery link"""
|
||||||
flow = Flow.objects.get(slug="default-authentication-flow")
|
flow = Flow.objects.get(slug="default-authentication-flow")
|
||||||
user = User.objects.get(username="akadmin")
|
user = create_test_admin_user()
|
||||||
manager = Manager()
|
manager = Manager()
|
||||||
return_dict = manager.dict()
|
return_dict = manager.dict()
|
||||||
|
|
||||||
|
@ -0,0 +1,180 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-10 16:08
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [
|
||||||
|
("authentik_flows", "0001_initial"),
|
||||||
|
("authentik_flows", "0003_auto_20200523_1133"),
|
||||||
|
("authentik_flows", "0006_auto_20200629_0857"),
|
||||||
|
("authentik_flows", "0007_auto_20200703_2059"),
|
||||||
|
]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_policies", "0001_initial"),
|
||||||
|
("authentik_policies", "0002_auto_20200528_1647"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Flow",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"flow_uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.TextField()),
|
||||||
|
("slug", models.SlugField(unique=True)),
|
||||||
|
(
|
||||||
|
"designation",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("authentication", "Authentication"),
|
||||||
|
("invalidation", "Invalidation"),
|
||||||
|
("enrollment", "Enrollment"),
|
||||||
|
("unenrollment", "Unrenollment"),
|
||||||
|
("recovery", "Recovery"),
|
||||||
|
("password_change", "Password Change"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"pbm",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
related_name="+",
|
||||||
|
to="authentik_policies.policybindingmodel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Flow",
|
||||||
|
"verbose_name_plural": "Flows",
|
||||||
|
},
|
||||||
|
bases=("authentik_policies.policybindingmodel",),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Stage",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"stage_uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.TextField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FlowStageBinding",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"policybindingmodel_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
to="authentik_policies.policybindingmodel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"fsb_uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"re_evaluate_policies",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="When this option is enabled, the planner will re-evaluate policies bound to this.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("order", models.IntegerField()),
|
||||||
|
(
|
||||||
|
"target",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="authentik_flows.flow"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"stage",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="authentik_flows.stage"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Flow Stage Binding",
|
||||||
|
"verbose_name_plural": "Flow Stage Bindings",
|
||||||
|
"ordering": ["order", "target"],
|
||||||
|
"unique_together": {("target", "stage", "order")},
|
||||||
|
},
|
||||||
|
bases=("authentik_policies.policybindingmodel",),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="flow",
|
||||||
|
name="stages",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, through="authentik_flows.FlowStageBinding", to="authentik_flows.Stage"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="designation",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("authentication", "Authentication"),
|
||||||
|
("authorization", "Authorization"),
|
||||||
|
("invalidation", "Invalidation"),
|
||||||
|
("enrollment", "Enrollment"),
|
||||||
|
("unenrollment", "Unrenollment"),
|
||||||
|
("recovery", "Recovery"),
|
||||||
|
("password_change", "Password Change"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="designation",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("authentication", "Authentication"),
|
||||||
|
("authorization", "Authorization"),
|
||||||
|
("invalidation", "Invalidation"),
|
||||||
|
("enrollment", "Enrollment"),
|
||||||
|
("unenrollment", "Unrenollment"),
|
||||||
|
("recovery", "Recovery"),
|
||||||
|
("stage_setup", "Stage Setup"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="flow",
|
||||||
|
old_name="pbm",
|
||||||
|
new_name="policybindingmodel_ptr",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="policybindingmodel_ptr",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
to="authentik_policies.policybindingmodel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,171 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-10 16:08
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
import authentik.lib.models
|
||||||
|
from authentik.flows.models import FlowDesignation
|
||||||
|
|
||||||
|
|
||||||
|
def update_flow_designation(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
Flow = apps.get_model("authentik_flows", "Flow")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
for flow in Flow.objects.using(db_alias).all():
|
||||||
|
if flow.designation == "stage_setup":
|
||||||
|
flow.designation = FlowDesignation.STAGE_CONFIGURATION
|
||||||
|
flow.save()
|
||||||
|
|
||||||
|
|
||||||
|
# First stage for default-source-enrollment flow (prompt stage)
|
||||||
|
# needs to have its policy re-evaluated
|
||||||
|
def update_default_source_enrollment_flow_binding(
|
||||||
|
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
||||||
|
):
|
||||||
|
Flow = apps.get_model("authentik_flows", "Flow")
|
||||||
|
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
flows = Flow.objects.using(db_alias).filter(slug="default-source-enrollment")
|
||||||
|
if not flows.exists():
|
||||||
|
return
|
||||||
|
flow = flows.first()
|
||||||
|
|
||||||
|
binding = FlowStageBinding.objects.get(target=flow, order=0)
|
||||||
|
binding.re_evaluate_policies = True
|
||||||
|
binding.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [
|
||||||
|
("authentik_flows", "0012_auto_20200908_1542"),
|
||||||
|
("authentik_flows", "0013_auto_20200924_1605"),
|
||||||
|
("authentik_flows", "0014_auto_20200925_2332"),
|
||||||
|
("authentik_flows", "0015_flowstagebinding_evaluate_on_plan"),
|
||||||
|
("authentik_flows", "0016_auto_20201202_1307"),
|
||||||
|
("authentik_flows", "0017_auto_20210329_1334"),
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_flows", "0011_flow_title"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flowstagebinding",
|
||||||
|
name="stage",
|
||||||
|
field=authentik.lib.models.InheritanceForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="authentik_flows.stage"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="stage",
|
||||||
|
name="name",
|
||||||
|
field=models.TextField(unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="designation",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("authentication", "Authentication"),
|
||||||
|
("authorization", "Authorization"),
|
||||||
|
("invalidation", "Invalidation"),
|
||||||
|
("enrollment", "Enrollment"),
|
||||||
|
("unenrollment", "Unrenollment"),
|
||||||
|
("recovery", "Recovery"),
|
||||||
|
("stage_configuration", "Stage Configuration"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=update_flow_designation,
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="flowstagebinding",
|
||||||
|
options={
|
||||||
|
"ordering": ["target", "order"],
|
||||||
|
"verbose_name": "Flow Stage Binding",
|
||||||
|
"verbose_name_plural": "Flow Stage Bindings",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flowstagebinding",
|
||||||
|
name="re_evaluate_policies",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="When this option is enabled, the planner will re-evaluate policies bound to this binding.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=update_default_source_enrollment_flow_binding,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flowstagebinding",
|
||||||
|
name="re_evaluate_policies",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False, help_text="Evaluate policies when the Stage is present to the user."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="flowstagebinding",
|
||||||
|
name="evaluate_on_plan",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Evaluate policies during the Flow planning process. Disable this for input-based policies.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="flow",
|
||||||
|
name="background",
|
||||||
|
field=models.FileField(
|
||||||
|
blank=True,
|
||||||
|
default="../static/dist/assets/images/flow_background.jpg",
|
||||||
|
help_text="Background shown during execution",
|
||||||
|
upload_to="flow-backgrounds/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="designation",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("authentication", "Authentication"),
|
||||||
|
("authorization", "Authorization"),
|
||||||
|
("invalidation", "Invalidation"),
|
||||||
|
("enrollment", "Enrollment"),
|
||||||
|
("unenrollment", "Unrenollment"),
|
||||||
|
("recovery", "Recovery"),
|
||||||
|
("stage_configuration", "Stage Configuration"),
|
||||||
|
],
|
||||||
|
help_text="Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik.",
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="slug",
|
||||||
|
field=models.SlugField(help_text="Visible in the URL.", unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="title",
|
||||||
|
field=models.TextField(help_text="Shown as the Title in Flow pages."),
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="flow",
|
||||||
|
options={
|
||||||
|
"permissions": [
|
||||||
|
("export_flow", "Can export a Flow"),
|
||||||
|
("view_flow_cache", "View Flow's cache metrics"),
|
||||||
|
("clear_flow_cache", "Clear Flow's cache metrics"),
|
||||||
|
],
|
||||||
|
"verbose_name": "Flow",
|
||||||
|
"verbose_name_plural": "Flows",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,64 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-10 16:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [
|
||||||
|
("authentik_flows", "0019_alter_flow_background"),
|
||||||
|
("authentik_flows", "0020_flow_compatibility_mode"),
|
||||||
|
("authentik_flows", "0021_flowstagebinding_invalid_response_action"),
|
||||||
|
("authentik_flows", "0022_alter_flowstagebinding_invalid_response_action"),
|
||||||
|
("authentik_flows", "0023_alter_flow_background"),
|
||||||
|
("authentik_flows", "0024_alter_flow_compatibility_mode"),
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_flows", "0018_oob_flows"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="background",
|
||||||
|
field=models.FileField(
|
||||||
|
default=None,
|
||||||
|
help_text="Background shown during execution",
|
||||||
|
null=True,
|
||||||
|
upload_to="flow-backgrounds/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="flowstagebinding",
|
||||||
|
name="invalid_response_action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("retry", "Retry"),
|
||||||
|
("restart", "Restart"),
|
||||||
|
("restart_with_context", "Restart With Context"),
|
||||||
|
],
|
||||||
|
default="retry",
|
||||||
|
help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="background",
|
||||||
|
field=models.FileField(
|
||||||
|
default=None,
|
||||||
|
help_text="Background shown during execution",
|
||||||
|
max_length=500,
|
||||||
|
null=True,
|
||||||
|
upload_to="flow-backgrounds/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="flow",
|
||||||
|
name="compatibility_mode",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Enable compatibility mode, increases compatibility with password managers on mobile devices.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
46
authentik/flows/migrations/0020_flowtoken.py
Normal file
46
authentik/flows/migrations/0020_flowtoken.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 3.2.9 on 2021-12-05 13:50
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0018_auto_20210330_1345_squashed_0028_alter_token_intent"),
|
||||||
|
(
|
||||||
|
"authentik_flows",
|
||||||
|
"0019_alter_flow_background_squashed_0024_alter_flow_compatibility_mode",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FlowToken",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"token_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="authentik_core.token",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("_plan", models.TextField()),
|
||||||
|
(
|
||||||
|
"flow",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="authentik_flows.flow"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Flow Token",
|
||||||
|
"verbose_name_plural": "Flow Tokens",
|
||||||
|
},
|
||||||
|
bases=("authentik_core.token",),
|
||||||
|
),
|
||||||
|
]
|
@ -1,4 +1,6 @@
|
|||||||
"""Flow models"""
|
"""Flow models"""
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
from pickle import dumps, loads # nosec
|
||||||
from typing import TYPE_CHECKING, Optional, Type
|
from typing import TYPE_CHECKING, Optional, Type
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@ -9,11 +11,13 @@ from model_utils.managers import InheritanceManager
|
|||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import Token
|
||||||
from authentik.core.types import UserSettingSerializer
|
from authentik.core.types import UserSettingSerializer
|
||||||
from authentik.lib.models import InheritanceForeignKey, SerializerModel
|
from authentik.lib.models import InheritanceForeignKey, SerializerModel
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from authentik.flows.planner import FlowPlan
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -260,3 +264,30 @@ class ConfigurableStage(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class FlowToken(Token):
|
||||||
|
"""Subclass of a standard Token, stores the currently active flow plan upon creation.
|
||||||
|
Can be used to later resume a flow."""
|
||||||
|
|
||||||
|
flow = models.ForeignKey(Flow, on_delete=models.CASCADE)
|
||||||
|
_plan = models.TextField()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def pickle(plan) -> str:
|
||||||
|
"""Pickle into string"""
|
||||||
|
data = dumps(plan)
|
||||||
|
return b64encode(data).decode()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plan(self) -> "FlowPlan":
|
||||||
|
"""Load Flow plan from pickled version"""
|
||||||
|
return loads(b64decode(self._plan.encode())) # nosec
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Flow Token {super().__str__()}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
verbose_name = _("Flow Token")
|
||||||
|
verbose_name_plural = _("Flow Tokens")
|
||||||
|
@ -24,6 +24,9 @@ PLAN_CONTEXT_SSO = "is_sso"
|
|||||||
PLAN_CONTEXT_REDIRECT = "redirect"
|
PLAN_CONTEXT_REDIRECT = "redirect"
|
||||||
PLAN_CONTEXT_APPLICATION = "application"
|
PLAN_CONTEXT_APPLICATION = "application"
|
||||||
PLAN_CONTEXT_SOURCE = "source"
|
PLAN_CONTEXT_SOURCE = "source"
|
||||||
|
# Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan
|
||||||
|
# was restored.
|
||||||
|
PLAN_CONTEXT_IS_RESTORED = "is_restored"
|
||||||
GAUGE_FLOWS_CACHED = UpdatingGauge(
|
GAUGE_FLOWS_CACHED = UpdatingGauge(
|
||||||
"authentik_flows_cached",
|
"authentik_flows_cached",
|
||||||
"Cached flows",
|
"Cached flows",
|
||||||
@ -57,11 +60,11 @@ class FlowPlan:
|
|||||||
markers: list[StageMarker] = field(default_factory=list)
|
markers: list[StageMarker] = field(default_factory=list)
|
||||||
|
|
||||||
def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
|
def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
|
||||||
"""Append `stage` to all stages, optionall with stage marker"""
|
"""Append `stage` to all stages, optionally with stage marker"""
|
||||||
return self.append(FlowStageBinding(stage=stage), marker)
|
return self.append(FlowStageBinding(stage=stage), marker)
|
||||||
|
|
||||||
def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
|
def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
|
||||||
"""Append `stage` to all stages, optionall with stage marker"""
|
"""Append `stage` to all stages, optionally with stage marker"""
|
||||||
self.bindings.append(binding)
|
self.bindings.append(binding)
|
||||||
self.markers.append(marker or StageMarker())
|
self.markers.append(marker or StageMarker())
|
||||||
|
|
||||||
@ -123,7 +126,7 @@ class FlowPlanner:
|
|||||||
) -> FlowPlan:
|
) -> FlowPlan:
|
||||||
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
|
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
|
||||||
and return ordered list"""
|
and return ordered list"""
|
||||||
with Hub.current.start_span(op="flow.planner.plan") as span:
|
with Hub.current.start_span(op="flow.planner.plan", description=self.flow.slug) as span:
|
||||||
span: Span
|
span: Span
|
||||||
span.set_data("flow", self.flow)
|
span.set_data("flow", self.flow)
|
||||||
span.set_data("request", request)
|
span.set_data("request", request)
|
||||||
@ -178,7 +181,8 @@ class FlowPlanner:
|
|||||||
"""Build flow plan by checking each stage in their respective
|
"""Build flow plan by checking each stage in their respective
|
||||||
order and checking the applied policies"""
|
order and checking the applied policies"""
|
||||||
with Hub.current.start_span(
|
with Hub.current.start_span(
|
||||||
op="flow.planner.build_plan"
|
op="flow.planner.build_plan",
|
||||||
|
description=self.flow.slug,
|
||||||
) as span, HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug).time():
|
) as span, HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug).time():
|
||||||
span: Span
|
span: Span
|
||||||
span.set_data("flow", self.flow)
|
span.set_data("flow", self.flow)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""authentik flow signals"""
|
"""authentik flow signals"""
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
@ -15,6 +15,7 @@ def delete_cache_prefix(prefix: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
@receiver(post_save)
|
||||||
|
@receiver(pre_delete)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def invalidate_flow_cache(sender, instance, **_):
|
def invalidate_flow_cache(sender, instance, **_):
|
||||||
"""Invalidate flow cache when flow is updated"""
|
"""Invalidate flow cache when flow is updated"""
|
||||||
|
@ -18,7 +18,7 @@ from authentik.flows.challenge import (
|
|||||||
)
|
)
|
||||||
from authentik.flows.models import InvalidResponseAction
|
from authentik.flows.models import InvalidResponseAction
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.views import FlowExecutorView
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
|
|
||||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
|
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -149,7 +149,7 @@ class ChallengeStageView(StageView):
|
|||||||
)
|
)
|
||||||
challenge_response.initial_data["response_errors"] = full_errors
|
challenge_response.initial_data["response_errors"] = full_errors
|
||||||
if not challenge_response.is_valid():
|
if not challenge_response.is_valid():
|
||||||
LOGGER.warning(
|
LOGGER.error(
|
||||||
"f(ch): invalid challenge response",
|
"f(ch): invalid challenge response",
|
||||||
binding=self.executor.current_binding,
|
binding=self.executor.current_binding,
|
||||||
errors=challenge_response.errors,
|
errors=challenge_response.errors,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.flows.api.stages import StageSerializer, StageViewSet
|
from authentik.flows.api.stages import StageSerializer, StageViewSet
|
||||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage
|
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
@ -47,7 +47,7 @@ class TestFlowsAPI(APITestCase):
|
|||||||
|
|
||||||
def test_api_diagram(self):
|
def test_api_diagram(self):
|
||||||
"""Test flow diagram."""
|
"""Test flow diagram."""
|
||||||
user = User.objects.get(username="akadmin")
|
user = create_test_admin_user()
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
|
|
||||||
flow = Flow.objects.create(
|
flow = Flow.objects.create(
|
||||||
@ -77,7 +77,7 @@ class TestFlowsAPI(APITestCase):
|
|||||||
|
|
||||||
def test_api_diagram_no_stages(self):
|
def test_api_diagram_no_stages(self):
|
||||||
"""Test flow diagram with no stages."""
|
"""Test flow diagram with no stages."""
|
||||||
user = User.objects.get(username="akadmin")
|
user = create_test_admin_user()
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
|
|
||||||
flow = Flow.objects.create(
|
flow = Flow.objects.create(
|
||||||
@ -93,7 +93,7 @@ class TestFlowsAPI(APITestCase):
|
|||||||
|
|
||||||
def test_types(self):
|
def test_types(self):
|
||||||
"""Test Stage's types endpoint"""
|
"""Test Stage's types endpoint"""
|
||||||
user = User.objects.get(username="akadmin")
|
user = create_test_admin_user()
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user