Compare commits
1004 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
eddca478dc | |||
99a7fca08e | |||
a7e3602908 | |||
74169860cf | |||
52bb774f73 | |||
f26fcaf825 | |||
b8e92e2f11 | |||
08adfc94d6 | |||
236fafb735 | |||
5ad9ddee3c | |||
24d220ff49 | |||
3364c195b7 | |||
50aa87d141 | |||
72b375023d | |||
77ba186818 | |||
2fe6de0505 | |||
bf9e969b53 | |||
184f119b16 | |||
ebc06f1abe | |||
0f8880ab0a | |||
ee56da5092 | |||
2152004502 | |||
45d0b80d02 | |||
96065eb942 | |||
ac944fee8b | |||
1d0e5fc353 | |||
1f97420207 | |||
ae07f13a87 | |||
0aec504170 | |||
3b4c9bcc57 | |||
5182a6741e | |||
da7635ae5c | |||
a92a0fb60a | |||
cb10c1753b | |||
ae654bd4c8 | |||
28192655ec | |||
9582294eb8 | |||
0172430d7d | |||
1454b65933 | |||
432a7792e2 | |||
54069618b4 | |||
81feb313df | |||
e6b275add3 | |||
27016a5527 | |||
4c29d517f0 | |||
180d27cc37 | |||
5a8b356dc7 | |||
3195640776 | |||
f463296d47 | |||
adf4b23c01 | |||
d900a2b6a9 | |||
95a2fddfa8 | |||
8f7d21b692 | |||
3f84abec2f | |||
b5c857aff4 | |||
f8dee09107 | |||
84a800583c | |||
88de94f014 | |||
25549ec339 | |||
fe4923bff6 | |||
bb1a0b6bd2 | |||
879b5ead71 | |||
1670ec9167 | |||
ac52667327 | |||
0d7c5c2108 | |||
73e3d19384 | |||
f6e0f0282d | |||
3f42067a8f | |||
ed6f5b98df | |||
dd290e264c | |||
c85484fc00 | |||
663dffd8be | |||
c15d0c3d17 | |||
bf09a54f35 | |||
930dd51663 | |||
12a523c7aa | |||
ea9a6d57dd | |||
91958e1232 | |||
8925afb089 | |||
ccafe7be4f | |||
8279690a8f | |||
763d3ae76a | |||
b775e7f4d3 | |||
3d8d93ece5 | |||
06af306e8a | |||
9257f3c919 | |||
2fe7f4cf04 | |||
04399bc8bb | |||
fcbcfbc3c0 | |||
3e4ce62dfe | |||
d8292151e6 | |||
3d01a59b34 | |||
5df15c4105 | |||
75d695105d | |||
28189bdddf | |||
f6885c7cf8 | |||
2c43f0824e | |||
13e2eea72f | |||
9441be1ee2 | |||
d7ab2a362a | |||
e920be3a72 | |||
f771383c4b | |||
65c75f085a | |||
17503365f7 | |||
ebf9f0ca63 | |||
ae26d2756f | |||
124071f9be | |||
471f7d9c62 | |||
a6a6b3bd06 | |||
48ad3dccda | |||
341c58a722 | |||
9b04f2da48 | |||
f7a296544f | |||
78641a57ad | |||
a77ff5ffec | |||
bdd5e16db1 | |||
d4672bfe79 | |||
abd9fab41a | |||
7c8bf42ef9 | |||
274b555912 | |||
916530f0d8 | |||
95efd47f65 | |||
90ecb1af7f | |||
d7fdca1b44 | |||
37346763dc | |||
c35fd2755f | |||
281e3a0518 | |||
6349cdad2f | |||
ef341dd405 | |||
198e5ce642 | |||
923fbac5b0 | |||
5f28c7ace7 | |||
d96c96006f | |||
3ddf2d6f85 | |||
ba6849f29c | |||
942170f902 | |||
248f993541 | |||
56d40bddd0 | |||
3a700a449a | |||
a20f552bcf | |||
32331a56eb | |||
d752b7e41c | |||
0b4223c6ca | |||
a3ec5c13f0 | |||
128b582dd6 | |||
e59ede5422 | |||
6d08ba2513 | |||
23444f4df0 | |||
3338f7a401 | |||
b126519275 | |||
71e68b498e | |||
fb267ee223 | |||
8e59b06611 | |||
a4b3519428 | |||
4895fc3bbb | |||
3daabd6fa8 | |||
9fccb14065 | |||
12efe94fd1 | |||
375ef27b9f | |||
9a7fa39de4 | |||
c779ad2e3b | |||
7e7ef289ba | |||
223d9ad414 | |||
948ea7b087 | |||
bf771f8b6c | |||
6dc8aa396c | |||
92a48f9dc6 | |||
d0ad9fcb1f | |||
539e6deca5 | |||
df4c8003b8 | |||
169e748a78 | |||
39b365c6ae | |||
9a79bab43d | |||
e229eda96e | |||
4448145aa9 | |||
3d042e708a | |||
2428d5f1c2 | |||
f1dc2b4d2a | |||
7dfbcdbb81 | |||
5fd4f56fa2 | |||
b9d5ba6b0a | |||
2a4cb07ba8 | |||
7939286176 | |||
46ef49b897 | |||
b923d85f6a | |||
2862b4ecfb | |||
094acc62f0 | |||
13d17dc729 | |||
5cf3a13ca8 | |||
d0898a3869 | |||
7158c9d2ea | |||
c5cf17b60b | |||
da58796768 | |||
d98499a3fa | |||
e5944567e8 | |||
d296c12d01 | |||
4c3a9e69f2 | |||
eb2540a3c8 | |||
bf9a3615d9 | |||
33fb22e3e7 | |||
f3ff398a44 | |||
533eb59a04 | |||
8ca29f6d49 | |||
0a33d38adf | |||
f7afb60c1f | |||
b9c605bf1a | |||
2983adc719 | |||
502393ee56 | |||
121bba1d9f | |||
3c1b70c355 | |||
27508dd1f0 | |||
6d962dbdf3 | |||
9194e6368a | |||
917fb7d626 | |||
3cf5794b96 | |||
631b0a1819 | |||
6662dcc4b0 | |||
95db54b819 | |||
bc7d5042df | |||
de3e1c3dbc | |||
3c6aac5435 | |||
eeb755ab7d | |||
70d0dd51a5 | |||
073dd8b560 | |||
b5d2924d46 | |||
597e279f34 | |||
fc28def83d | |||
f6efdfded4 | |||
91312496e0 | |||
b557b4337d | |||
bfde186aa0 | |||
2bd75dd1a9 | |||
27ab31a9b0 | |||
44a8b737d9 | |||
b939ee7a09 | |||
0bae550520 | |||
b5cc2f2bda | |||
9ad4cf1db9 | |||
9dbafaaea2 | |||
2db8b07578 | |||
7c1a7bfd9d | |||
b7ef076798 | |||
37c29a073e | |||
0c288ea64b | |||
2476475174 | |||
71913c8164 | |||
6ec8432217 | |||
7a12c0e4d1 | |||
23a7eba16b | |||
3ba84a8e8b | |||
75476217a0 | |||
7771c0b905 | |||
3378e82ec7 | |||
126e43dea4 | |||
f725009530 | |||
70d1e3a0cb | |||
e751ce1220 | |||
e09a27cf87 | |||
06fbf44724 | |||
200e409d91 | |||
5e5854e256 | |||
3df8bcfc9c | |||
e76c14f9e0 | |||
6b6748b1c7 | |||
d92d8e6dbb | |||
c2b9dc5c75 | |||
5c1d27de2b | |||
6ab9e7cd68 | |||
3ef56e9ec1 | |||
6d8d157772 | |||
cadd466eec | |||
3fea0c1e49 | |||
4c58201adc | |||
4fb4e72624 | |||
276d8fe5cf | |||
92ce5f0931 | |||
7fea20375f | |||
d4d4034d2c | |||
f0db408699 | |||
5e200655d9 | |||
d5d1f2a645 | |||
cc5cc43baa | |||
e512f085db | |||
f323c01bd8 | |||
f56cacb406 | |||
eaecd31e9f | |||
36989d82e1 | |||
50777d9022 | |||
a15571bd3e | |||
26fd66d831 | |||
0be873025a | |||
28ada49910 | |||
4fc8e61f8c | |||
7d26ea1a9c | |||
3a58dc62e1 | |||
71fe7bc827 | |||
933336c38b | |||
371feb9a31 | |||
95a2fd3c9e | |||
17cb76c334 | |||
88f0dfc8cc | |||
f82aada23b | |||
ecaee92634 | |||
89252ec47b | |||
f0f25ab291 | |||
e4d0fec15a | |||
6b10baf086 | |||
f148b5d341 | |||
1471ff8940 | |||
d9a6ec2ac0 | |||
5745ffa0a8 | |||
b26202db35 | |||
6318577a51 | |||
6a2cd45847 | |||
ef5cea2c01 | |||
69f4d54bae | |||
b1eec5a7d2 | |||
1b8271d767 | |||
3e9f5ec5ef | |||
63f57b6a77 | |||
a016f99450 | |||
adc18b2991 | |||
e37a326b95 | |||
048467e97d | |||
cc2cd6919f | |||
0c6e781e5b | |||
7294d8fca5 | |||
16ec5680b4 | |||
87920fb1d7 | |||
523b96a6d2 | |||
45731d8069 | |||
e872371970 | |||
08e8cf850a | |||
b1ed2154ac | |||
7ef2aa3eb9 | |||
160139813d | |||
582ad92c76 | |||
f61736e3d1 | |||
eb02c96281 | |||
8619552920 | |||
6237352e25 | |||
2d8b4f543b | |||
8542dc10ab | |||
c55b63337c | |||
12ddee3bb6 | |||
dc41d0af27 | |||
3323b50036 | |||
8acb15a7fd | |||
f601e04b38 | |||
f50529cb5b | |||
3f1b6f9ed4 | |||
f1ab0f4314 | |||
4d1129f385 | |||
03ac9c6e16 | |||
c0839924f1 | |||
91e3aa760a | |||
5c0681d57b | |||
c4f72c2bc1 | |||
e92f9836e3 | |||
3818dc834b | |||
cda011a049 | |||
897f6f3473 | |||
b70b44490b | |||
77a5a58cb9 | |||
f3b227434e | |||
2ae164df78 | |||
9b09793230 | |||
f8a401aeca | |||
ffbab2cd68 | |||
734e5fcab4 | |||
78578c6c9d | |||
0ccec96490 | |||
8022d0801d | |||
d79975c409 | |||
20d65035d5 | |||
8d6227377f | |||
4bc50e7f57 | |||
945e42c940 | |||
052bb28086 | |||
4a84b7e2d5 | |||
4d27694706 | |||
16cfa8cae2 | |||
1a20c8ffc1 | |||
d7ad5f6a16 | |||
5af9a3d3be | |||
dec34bc948 | |||
cff37caa57 | |||
cc6d5765f2 | |||
2ec1ff2ebb | |||
884c2bd0e9 | |||
2c938ec9dc | |||
9733caf3b7 | |||
494af0a430 | |||
10e50bc77f | |||
44bfbb9e49 | |||
5be152e12d | |||
b0efab6d6d | |||
f2725b88c8 | |||
24cc123029 | |||
d75c9997f6 | |||
0a20a30af3 | |||
c60ba91fee | |||
37927c9361 | |||
0a63441935 | |||
6b7a8b6ac7 | |||
cba255eaaa | |||
859cf2bd8f | |||
a2578ffaad | |||
888526a2a7 | |||
0d00b9cc0d | |||
27cc5d7138 | |||
b2f077645a | |||
2878597603 | |||
5face5410f | |||
1b8750e13b | |||
e27a6fdeeb | |||
a9af40f85c | |||
59f04963be | |||
033c9a3bd3 | |||
09e3d616e9 | |||
0b280c0a47 | |||
07a4f474f4 | |||
244dc671db | |||
4308136108 | |||
69a0153619 | |||
2655768f5a | |||
73c55b56a0 | |||
bcbdd6c26f | |||
00e9b91f56 | |||
4cf76fdcda | |||
c4832206fa | |||
d05562a388 | |||
f217d34a98 | |||
89f2967f69 | |||
9a6a3e66b8 | |||
2f4b18ebbd | |||
20572c728d | |||
aad753de68 | |||
a79a150a1f | |||
8b23e4701a | |||
a366d61891 | |||
9a13dfd63a | |||
32d80829e2 | |||
f6953296d8 | |||
e4790f9060 | |||
58712047e1 | |||
85915905dc | |||
52f2838f57 | |||
12e2f7b945 | |||
45d47f828a | |||
cf7eb88661 | |||
6a14ae7975 | |||
08f3294a1d | |||
ac47fc9295 | |||
1ff19e1467 | |||
439454a71b | |||
2a11964e1a | |||
507b8d43fb | |||
7efec281be | |||
9469f86f65 | |||
e998919097 | |||
450d69a1a4 | |||
b74681f22c | |||
f95a7c26e5 | |||
ffc9bd2cec | |||
bb7db0c828 | |||
aec3e08201 | |||
0651fbba06 | |||
bd9cd086a0 | |||
14fb0c3d61 | |||
c52afe5952 | |||
1d4b941a3b | |||
0344e5d9b3 | |||
d8e8cc062b |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2021.8.1-rc1
|
current_version = 2021.10.2
|
||||||
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>.*)
|
||||||
@ -23,7 +23,7 @@ values =
|
|||||||
|
|
||||||
[bumpversion:file:schema.yml]
|
[bumpversion:file:schema.yml]
|
||||||
|
|
||||||
[bumpversion:file:.github/workflows/release.yml]
|
[bumpversion:file:.github/workflows/release-publish.yml]
|
||||||
|
|
||||||
[bumpversion:file:authentik/__init__.py]
|
[bumpversion:file:authentik/__init__.py]
|
||||||
|
|
||||||
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -27,7 +27,7 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
Output of docker-compose logs or kubectl logs respectively
|
Output of docker-compose logs or kubectl logs respectively
|
||||||
|
|
||||||
**Version and Deployment (please complete the following information):**
|
**Version and Deployment (please complete the following information):**
|
||||||
- authentik version: [e.g. 0.10.0-stable]
|
- authentik version: [e.g. 2021.8.5]
|
||||||
- Deployment: [e.g. docker-compose, helm]
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
@ -20,7 +20,7 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
Output of docker-compose logs or kubectl logs respectively
|
Output of docker-compose logs or kubectl logs respectively
|
||||||
|
|
||||||
**Version and Deployment (please complete the following information):**
|
**Version and Deployment (please complete the following information):**
|
||||||
- authentik version: [e.g. 0.10.0-stable]
|
- authentik version: [e.g. 2021.8.5]
|
||||||
- Deployment: [e.g. docker-compose, helm]
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
3
.github/codespell-words.txt
vendored
Normal file
3
.github/codespell-words.txt
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
keypair
|
||||||
|
keypairs
|
||||||
|
hass
|
316
.github/workflows/ci-main.yml
vendored
Normal file
316
.github/workflows/ci-main.yml
vendored
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
name: authentik-ci-main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next
|
||||||
|
- version-*
|
||||||
|
paths-ignore:
|
||||||
|
- website
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: authentik
|
||||||
|
POSTGRES_USER: authentik
|
||||||
|
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-pylint:
|
||||||
|
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 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
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- name: prepare
|
||||||
|
run: |
|
||||||
|
scripts/ci_prepare.sh
|
||||||
|
npm install -g pyright@1.1.136
|
||||||
|
- name: run bandit
|
||||||
|
run: pipenv run pyright e2e lifecycle
|
||||||
|
test-migrations:
|
||||||
|
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 migrations
|
||||||
|
run: pipenv run python -m lifecycle.migrate
|
||||||
|
test-migrations-from-stable:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- name: prepare variables
|
||||||
|
id: ev
|
||||||
|
run: |
|
||||||
|
python ./scripts/gh_env.py
|
||||||
|
- id: cache-pipenv
|
||||||
|
uses: actions/cache@v2.1.6
|
||||||
|
with:
|
||||||
|
path: ~/.local/share/virtualenvs
|
||||||
|
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
|
- name: checkout stable
|
||||||
|
run: |
|
||||||
|
# Copy current, latest config to local
|
||||||
|
cp authentik/lib/default.yml local.env.yml
|
||||||
|
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||||
|
- name: prepare
|
||||||
|
env:
|
||||||
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
|
run: |
|
||||||
|
scripts/ci_prepare.sh
|
||||||
|
# Sync anyways since stable will have different dependencies
|
||||||
|
pipenv sync --dev
|
||||||
|
- name: run migrations to stable
|
||||||
|
run: pipenv run python -m lifecycle.migrate
|
||||||
|
- name: checkout current code
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
git fetch
|
||||||
|
git checkout ${{ steps.ev.outputs.branchName }}
|
||||||
|
pipenv sync --dev
|
||||||
|
- name: prepare
|
||||||
|
env:
|
||||||
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
|
run: scripts/ci_prepare.sh
|
||||||
|
- name: migrate to latest
|
||||||
|
run: pipenv run python -m lifecycle.migrate
|
||||||
|
test-unittest:
|
||||||
|
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
|
||||||
|
- uses: testspace-com/setup-testspace@v1
|
||||||
|
with:
|
||||||
|
domain: ${{github.repository_owner}}
|
||||||
|
- name: run unittest
|
||||||
|
run: |
|
||||||
|
pipenv run make test
|
||||||
|
pipenv run coverage xml
|
||||||
|
- name: run testspace
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
testspace [unittest]unittest.xml --link=codecov
|
||||||
|
- if: ${{ always() }}
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
|
test-integration:
|
||||||
|
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
|
||||||
|
- uses: testspace-com/setup-testspace@v1
|
||||||
|
with:
|
||||||
|
domain: ${{github.repository_owner}}
|
||||||
|
- name: Create k8s Kind Cluster
|
||||||
|
uses: helm/kind-action@v1.2.0
|
||||||
|
- name: run integration
|
||||||
|
run: |
|
||||||
|
pipenv run make test-integration
|
||||||
|
pipenv run coverage xml
|
||||||
|
- name: run testspace
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
testspace [integration]unittest.xml --link=codecov
|
||||||
|
- if: ${{ always() }}
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
|
test-e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- uses: testspace-com/setup-testspace@v1
|
||||||
|
with:
|
||||||
|
domain: ${{github.repository_owner}}
|
||||||
|
- 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
|
||||||
|
docker-compose -f tests/e2e/docker-compose.yml up -d
|
||||||
|
- id: cache-web
|
||||||
|
uses: actions/cache@v2.1.6
|
||||||
|
with:
|
||||||
|
path: web/dist
|
||||||
|
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }}
|
||||||
|
- name: prepare web ui
|
||||||
|
if: steps.cache-web.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm i
|
||||||
|
npm run build
|
||||||
|
- name: run e2e
|
||||||
|
run: |
|
||||||
|
pipenv run make test-e2e
|
||||||
|
pipenv run coverage xml
|
||||||
|
- name: run testspace
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
testspace [e2e]unittest.xml --link=codecov
|
||||||
|
- if: ${{ always() }}
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
|
build:
|
||||||
|
needs:
|
||||||
|
- lint-pylint
|
||||||
|
- lint-black
|
||||||
|
- lint-isort
|
||||||
|
- lint-bandit
|
||||||
|
- lint-pyright
|
||||||
|
- test-migrations
|
||||||
|
- test-migrations-from-stable
|
||||||
|
- test-unittest
|
||||||
|
- test-integration
|
||||||
|
- test-e2e
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: prepare variables
|
||||||
|
id: ev
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||||
|
run: |
|
||||||
|
python ./scripts/gh_env.py
|
||||||
|
- name: Login to Container Registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Building Docker Image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
|
tags: |
|
||||||
|
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}
|
||||||
|
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }}
|
||||||
|
build-args: |
|
||||||
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
69
.github/workflows/ci-outpost.yml
vendored
Normal file
69
.github/workflows/ci-outpost.yml
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
name: authentik-ci-outpost
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next
|
||||||
|
- version-*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-golint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '^1.16.3'
|
||||||
|
- name: Run linter
|
||||||
|
run: |
|
||||||
|
# Create folder structure for go embeds
|
||||||
|
mkdir -p web/dist
|
||||||
|
mkdir -p website/help
|
||||||
|
touch web/dist/test website/help/test
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v $(pwd):/app \
|
||||||
|
-w /app \
|
||||||
|
golangci/golangci-lint:v1.39.0 \
|
||||||
|
golangci-lint run -v --timeout 200s
|
||||||
|
build:
|
||||||
|
needs:
|
||||||
|
- lint-golint
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
type:
|
||||||
|
- proxy
|
||||||
|
- ldap
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: prepare variables
|
||||||
|
id: ev
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||||
|
run: |
|
||||||
|
python ./scripts/gh_env.py
|
||||||
|
- name: Login to Container Registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Building Docker Image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
|
tags: |
|
||||||
|
ghcr.io/goauthentik/dev-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchNameContainer }}
|
||||||
|
ghcr.io/goauthentik/dev-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}
|
||||||
|
ghcr.io/goauthentik/dev-${{ matrix.type }}:gh-${{ steps.ev.outputs.sha }}
|
||||||
|
file: ${{ matrix.type }}.Dockerfile
|
||||||
|
build-args: |
|
||||||
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
89
.github/workflows/ci-web.yml
vendored
Normal file
89
.github/workflows/ci-web.yml
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
name: authentik-ci-web
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next
|
||||||
|
- version-*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-eslint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
- name: Generate API
|
||||||
|
run: make gen-web
|
||||||
|
- name: Eslint
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm run lint
|
||||||
|
lint-prettier:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
- name: Generate API
|
||||||
|
run: make gen-web
|
||||||
|
- name: prettier
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm run prettier-check
|
||||||
|
lint-lit-analyse:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
- name: Generate API
|
||||||
|
run: make gen-web
|
||||||
|
- name: lit-analyse
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm run lit-analyse
|
||||||
|
build:
|
||||||
|
needs:
|
||||||
|
- lint-eslint
|
||||||
|
- lint-prettier
|
||||||
|
- lint-lit-analyse
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
- name: Generate API
|
||||||
|
run: make gen-web
|
||||||
|
- name: build
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm run build
|
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
|
@ -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.8.1-rc1,
|
beryju/authentik:2021.10.2,
|
||||||
beryju/authentik:latest,
|
beryju/authentik:latest,
|
||||||
ghcr.io/goauthentik/server:2021.8.1-rc1,
|
ghcr.io/goauthentik/server:2021.10.2,
|
||||||
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.8.1-rc1', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.10.2', '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
|
||||||
@ -75,14 +72,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-proxy:2021.8.1-rc1,
|
beryju/authentik-proxy:2021.10.2,
|
||||||
beryju/authentik-proxy:latest,
|
beryju/authentik-proxy:latest,
|
||||||
ghcr.io/goauthentik/proxy:2021.8.1-rc1,
|
ghcr.io/goauthentik/proxy:2021.10.2,
|
||||||
ghcr.io/goauthentik/proxy:latest
|
ghcr.io/goauthentik/proxy:latest
|
||||||
file: proxy.Dockerfile
|
file: proxy.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.8.1-rc1', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.10.2', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik-proxy:latest
|
docker pull beryju/authentik-proxy:latest
|
||||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||||
@ -117,14 +114,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-ldap:2021.8.1-rc1,
|
beryju/authentik-ldap:2021.10.2,
|
||||||
beryju/authentik-ldap:latest,
|
beryju/authentik-ldap:latest,
|
||||||
ghcr.io/goauthentik/ldap:2021.8.1-rc1,
|
ghcr.io/goauthentik/ldap:2021.10.2,
|
||||||
ghcr.io/goauthentik/ldap:latest
|
ghcr.io/goauthentik/ldap:latest
|
||||||
file: ldap.Dockerfile
|
file: ldap.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.8.1-rc1', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.10.2', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik-ldap:latest
|
docker pull beryju/authentik-ldap:latest
|
||||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||||
@ -142,28 +139,25 @@ jobs:
|
|||||||
- 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: Setup Node.js environment
|
||||||
uses: actions/setup-node@v2.4.0
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: '16'
|
||||||
- name: Build web api client and web ui
|
- name: Build web api client and web ui
|
||||||
run: |
|
run: |
|
||||||
export NODE_ENV=production
|
export NODE_ENV=production
|
||||||
make gen-web
|
|
||||||
cd web
|
cd web
|
||||||
npm i
|
npm i
|
||||||
npm run build
|
npm run build
|
||||||
@ -176,7 +170,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.8.1-rc1
|
version: authentik@2021.10.2
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
sourcemaps: './web/dist'
|
sourcemaps: './web/dist'
|
||||||
url_prefix: '~/static/dist'
|
url_prefix: '~/static/dist'
|
@ -13,21 +13,20 @@ 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
|
|
||||||
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: |
|
46
.github/workflows/translation-compile.yml
vendored
Normal file
46
.github/workflows/translation-compile.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
name: authentik-backend-translate-compile
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
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.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: |
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
branch: compile-backend-translation
|
||||||
|
commit-message: "core: compile backend translations"
|
||||||
|
title: "core: compile backend translations"
|
||||||
|
delete-branch: true
|
||||||
|
signoff: true
|
39
.github/workflows/web-api-publish.yml
vendored
Normal file
39
.github/workflows/web-api-publish.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: authentik-web-api-publish
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
paths:
|
||||||
|
- 'schema.yml'
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
# Setup .npmrc file to publish to npm
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
- name: Generate API Client
|
||||||
|
run: make gen-web
|
||||||
|
- name: Publish package
|
||||||
|
run: |
|
||||||
|
cd web-api/
|
||||||
|
npm i
|
||||||
|
npm publish
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||||
|
- name: Upgrade /web
|
||||||
|
run: |
|
||||||
|
cd web/
|
||||||
|
export VERSION=`node -e 'console.log(require("../web-api/package.json").version)'`
|
||||||
|
npm i @goauthentik/api@$VERSION
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
branch: update-web-api-client
|
||||||
|
commit-message: "web: Update Web API Client version"
|
||||||
|
title: "web: Update Web API Client version"
|
||||||
|
delete-branch: true
|
||||||
|
signoff: true
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -201,3 +201,4 @@ media/
|
|||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
/api/
|
/api/
|
||||||
|
/web-api/
|
||||||
|
22
.vscode/settings.json
vendored
Normal file
22
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"asgi",
|
||||||
|
"authentik",
|
||||||
|
"authn",
|
||||||
|
"goauthentik",
|
||||||
|
"jwks",
|
||||||
|
"oidc",
|
||||||
|
"openid",
|
||||||
|
"plex",
|
||||||
|
"saml",
|
||||||
|
"totp",
|
||||||
|
"webauthn"
|
||||||
|
],
|
||||||
|
"python.linting.pylintEnabled": true,
|
||||||
|
"todo-tree.tree.showCountsInTree": true,
|
||||||
|
"todo-tree.tree.showBadges": true,
|
||||||
|
"python.formatting.provider": "black",
|
||||||
|
"files.associations": {
|
||||||
|
"*.akflow": "json"
|
||||||
|
}
|
||||||
|
}
|
@ -11,8 +11,8 @@ The following is a set of guidelines for contributing to authentik and its compo
|
|||||||
[I don't want to read this whole thing, I just have a question!!!](#i-dont-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!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
|
||||||
|
|
||||||
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
||||||
* [Atom and Packages](#atom-and-packages)
|
* [The components](#the-components)
|
||||||
* [Atom Design Decisions](#design-decisions)
|
* [authentik's structure](#authentiks-structure)
|
||||||
|
|
||||||
[How Can I Contribute?](#how-can-i-contribute)
|
[How Can I Contribute?](#how-can-i-contribute)
|
||||||
* [Reporting Bugs](#reporting-bugs)
|
* [Reporting Bugs](#reporting-bugs)
|
||||||
@ -22,21 +22,16 @@ The following is a set of guidelines for contributing to authentik and its compo
|
|||||||
|
|
||||||
[Styleguides](#styleguides)
|
[Styleguides](#styleguides)
|
||||||
* [Git Commit Messages](#git-commit-messages)
|
* [Git Commit Messages](#git-commit-messages)
|
||||||
* [JavaScript Styleguide](#javascript-styleguide)
|
* [Python Styleguide](#python-styleguide)
|
||||||
* [CoffeeScript Styleguide](#coffeescript-styleguide)
|
|
||||||
* [Specs Styleguide](#specs-styleguide)
|
|
||||||
* [Documentation Styleguide](#documentation-styleguide)
|
* [Documentation Styleguide](#documentation-styleguide)
|
||||||
|
|
||||||
[Additional Notes](#additional-notes)
|
|
||||||
* [Issue and Pull Request Labels](#issue-and-pull-request-labels)
|
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
Basically, don't be a dickhead. This is an open-source non-profit project, that is made in the free time of Volunteers. If there's something you dislike or think can be done better, tell us! We'd love to hear any suggestions for improvement.
|
Basically, don't be a dickhead. This is an open-source non-profit project, that is made in the free time of Volunteers. If there's something you dislike or think can be done better, tell us! We'd love to hear any suggestions for improvement.
|
||||||
|
|
||||||
## 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?
|
||||||
|
|
||||||
@ -122,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
|
||||||
|
|
||||||
@ -136,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
|
||||||
|
|
||||||
|
55
Dockerfile
55
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.9-bullseye as locker
|
||||||
|
|
||||||
COPY ./Pipfile /app/
|
COPY ./Pipfile /app/
|
||||||
COPY ./Pipfile.lock /app/
|
COPY ./Pipfile.lock /app/
|
||||||
@ -11,50 +11,23 @@ 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 docker.io/node:16 as website-builder
|
||||||
|
|
||||||
COPY ./website /static/
|
COPY ./website /static/
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN cd /static && npm i && npm run build-docs-only
|
RUN cd /static && npm i && npm run build-docs-only
|
||||||
|
|
||||||
# Stage 3: Build web API
|
# Stage 3: Build webui
|
||||||
FROM openapitools/openapi-generator-cli as web-api-builder
|
FROM docker.io/node:16 as web-builder
|
||||||
|
|
||||||
COPY ./schema.yml /local/schema.yml
|
|
||||||
|
|
||||||
RUN docker-entrypoint.sh generate \
|
|
||||||
-i /local/schema.yml \
|
|
||||||
-g typescript-fetch \
|
|
||||||
-o /local/web/api \
|
|
||||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
|
||||||
|
|
||||||
# Stage 3: Generate API Client
|
|
||||||
FROM openapitools/openapi-generator-cli as go-api-builder
|
|
||||||
|
|
||||||
COPY ./schema.yml /local/schema.yml
|
|
||||||
|
|
||||||
RUN docker-entrypoint.sh generate \
|
|
||||||
--git-host goauthentik.io \
|
|
||||||
--git-repo-id outpost \
|
|
||||||
--git-user-id api \
|
|
||||||
-i /local/schema.yml \
|
|
||||||
-g go \
|
|
||||||
-o /local/api \
|
|
||||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true && \
|
|
||||||
rm -f /local/api/go.mod /local/api/go.sum
|
|
||||||
|
|
||||||
# Stage 4: Build webui
|
|
||||||
FROM node as web-builder
|
|
||||||
|
|
||||||
COPY ./web /static/
|
COPY ./web /static/
|
||||||
COPY --from=web-api-builder /local/web/api /static/api
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN cd /static && npm i && npm run build
|
RUN cd /static && npm i && npm run build
|
||||||
|
|
||||||
# Stage 5: Build go proxy
|
# Stage 4: Build go proxy
|
||||||
FROM golang:1.17.0 AS builder
|
FROM docker.io/golang:1.17.2-bullseye AS builder
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
@ -64,7 +37,6 @@ COPY --from=web-builder /static/dist/ /work/web/dist/
|
|||||||
COPY --from=web-builder /static/authentik/ /work/web/authentik/
|
COPY --from=web-builder /static/authentik/ /work/web/authentik/
|
||||||
COPY --from=website-builder /static/help/ /work/website/help/
|
COPY --from=website-builder /static/help/ /work/website/help/
|
||||||
|
|
||||||
COPY --from=go-api-builder /local/api api
|
|
||||||
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 ./website/static.go /work/website/static.go
|
||||||
@ -74,8 +46,8 @@ 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 6: Run
|
# Stage 5: Run
|
||||||
FROM python:3.9-slim-buster
|
FROM docker.io/python:3.9-bullseye
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY --from=locker /app/requirements.txt /
|
COPY --from=locker /app/requirements.txt /
|
||||||
@ -87,7 +59,7 @@ 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 ca-certificates gnupg git runit && \
|
||||||
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
||||||
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends libpq-dev postgresql-client build-essential libxmlsec1-dev pkg-config libmaxminddb0 && \
|
apt-get install -y --no-install-recommends libpq-dev postgresql-client build-essential libxmlsec1-dev pkg-config libmaxminddb0 && \
|
||||||
pip install -r /requirements.txt --no-cache-dir && \
|
pip install -r /requirements.txt --no-cache-dir && \
|
||||||
@ -108,6 +80,11 @@ COPY ./lifecycle/ /lifecycle
|
|||||||
COPY --from=builder /work/authentik /authentik-proxy
|
COPY --from=builder /work/authentik /authentik-proxy
|
||||||
|
|
||||||
USER authentik
|
USER authentik
|
||||||
|
|
||||||
ENV TMPDIR /dev/shm/
|
ENV TMPDIR /dev/shm/
|
||||||
ENV PYTHONUBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]
|
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" ]
|
||||||
|
31
Makefile
31
Makefile
@ -2,12 +2,11 @@
|
|||||||
PWD = $(shell pwd)
|
PWD = $(shell pwd)
|
||||||
UID = $(shell id -u)
|
UID = $(shell id -u)
|
||||||
GID = $(shell id -g)
|
GID = $(shell id -g)
|
||||||
|
NPM_VERSION = $(shell python -m scripts.npm_version)
|
||||||
|
|
||||||
all: lint-fix lint test gen
|
all: lint-fix lint test gen
|
||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
k3d cluster create || exit 0
|
|
||||||
k3d kubeconfig write -o ~/.kube/config --overwrite
|
|
||||||
coverage run manage.py test -v 3 tests/integration
|
coverage run manage.py test -v 3 tests/integration
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
@ -21,12 +20,23 @@ test:
|
|||||||
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:
|
||||||
|
./manage.py makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en
|
||||||
|
cd web && npm run extract
|
||||||
|
|
||||||
gen-build:
|
gen-build:
|
||||||
./manage.py spectacular --file schema.yml
|
./manage.py spectacular --file schema.yml
|
||||||
|
|
||||||
@ -41,10 +51,13 @@ gen-web:
|
|||||||
openapitools/openapi-generator-cli generate \
|
openapitools/openapi-generator-cli generate \
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g typescript-fetch \
|
-g typescript-fetch \
|
||||||
-o /local/web/api \
|
-o /local/web-api \
|
||||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=@goauthentik/api,npmVersion=${NPM_VERSION}
|
||||||
# npm i runs tsc as part of the installation process
|
mkdir -p web/node_modules/@goauthentik/api
|
||||||
cd web/api && npm i
|
python -m scripts.web_api_esm
|
||||||
|
\cp -fv scripts/web_api_readme.md web-api/README.md
|
||||||
|
cd web-api && npm i
|
||||||
|
\cp -rfv web-api/* web/node_modules/@goauthentik/api
|
||||||
|
|
||||||
gen-outpost:
|
gen-outpost:
|
||||||
docker run \
|
docker run \
|
||||||
@ -57,10 +70,10 @@ gen-outpost:
|
|||||||
-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
|
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false
|
||||||
rm -f api/go.mod api/go.sum
|
rm -f api/go.mod api/go.sum
|
||||||
|
|
||||||
gen: gen-build gen-clean gen-web gen-outpost
|
gen: gen-build gen-clean gen-web
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
python -m lifecycle.migrate
|
python -m lifecycle.migrate
|
||||||
|
12
Pipfile
12
Pipfile
@ -26,9 +26,9 @@ drf-spectacular = "*"
|
|||||||
facebook-sdk = "*"
|
facebook-sdk = "*"
|
||||||
geoip2 = "*"
|
geoip2 = "*"
|
||||||
gunicorn = "*"
|
gunicorn = "*"
|
||||||
kubernetes = "*"
|
kubernetes = "==v19.15.0"
|
||||||
ldap3 = "*"
|
ldap3 = "*"
|
||||||
lxml = ">=4.6.3"
|
lxml = "*"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
psycopg2-binary = "*"
|
psycopg2-binary = "*"
|
||||||
pycryptodome = "*"
|
pycryptodome = "*"
|
||||||
@ -48,16 +48,14 @@ duo-client = "*"
|
|||||||
ua-parser = "*"
|
ua-parser = "*"
|
||||||
deepmerge = "*"
|
deepmerge = "*"
|
||||||
colorama = "*"
|
colorama = "*"
|
||||||
|
codespell = "*"
|
||||||
[requires]
|
|
||||||
python_version = "3.9"
|
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
bandit = "*"
|
bandit = "*"
|
||||||
black = "==21.5b1"
|
black = "==21.9b0"
|
||||||
bump2version = "*"
|
bump2version = "*"
|
||||||
colorama = "*"
|
colorama = "*"
|
||||||
coverage = "*"
|
coverage = {extras = ["toml"],version = "*"}
|
||||||
pylint = "*"
|
pylint = "*"
|
||||||
pylint-django = "*"
|
pylint-django = "*"
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
|
1791
Pipfile.lock
generated
1791
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
17
README.md
17
README.md
@ -4,14 +4,15 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[](https://discord.gg/jg33eMhnj6)
|
[](https://goauthentik.io/discord)
|
||||||
[](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6)
|
[](https://github.com/goauthentik/authentik/actions/workflows/ci-main.yml)
|
||||||
[](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6)
|
[](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
|
||||||
|
[](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
|
||||||
[](https://codecov.io/gh/goauthentik/authentik)
|
[](https://codecov.io/gh/goauthentik/authentik)
|
||||||
|
[](https://goauthentik.testspace.com/)
|
||||||

|

|
||||||

|

|
||||||

|
[](https://www.transifex.com/beryjuorg/authentik/)
|
||||||
[Transifex](https://www.transifex.com/beryjuorg/authentik/)
|
|
||||||
|
|
||||||
## What is authentik?
|
## What is authentik?
|
||||||
|
|
||||||
@ -19,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
|
||||||
|
|
||||||
@ -32,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,9 +6,8 @@
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ---------- | ------------------ |
|
| ---------- | ------------------ |
|
||||||
| 2021.5.x | :white_check_mark: |
|
| 2021.8.x | :white_check_mark: |
|
||||||
| 2021.6.x | :white_check_mark: |
|
| 2021.9.x | :white_check_mark: |
|
||||||
| 2021.7.x | :white_check_mark: |
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
"""authentik"""
|
"""authentik"""
|
||||||
__version__ = "2021.8.1-rc1"
|
__version__ = "2021.10.2"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
@ -34,6 +34,7 @@ class RuntimeDict(TypedDict):
|
|||||||
class SystemSerializer(PassiveSerializer):
|
class SystemSerializer(PassiveSerializer):
|
||||||
"""Get system information."""
|
"""Get system information."""
|
||||||
|
|
||||||
|
env = SerializerMethodField()
|
||||||
http_headers = SerializerMethodField()
|
http_headers = SerializerMethodField()
|
||||||
http_host = SerializerMethodField()
|
http_host = SerializerMethodField()
|
||||||
http_is_secure = SerializerMethodField()
|
http_is_secure = SerializerMethodField()
|
||||||
@ -42,6 +43,10 @@ class SystemSerializer(PassiveSerializer):
|
|||||||
server_time = SerializerMethodField()
|
server_time = SerializerMethodField()
|
||||||
embedded_outpost_host = SerializerMethodField()
|
embedded_outpost_host = SerializerMethodField()
|
||||||
|
|
||||||
|
def get_env(self, request: Request) -> dict[str, str]:
|
||||||
|
"""Get Environment"""
|
||||||
|
return os.environ.copy()
|
||||||
|
|
||||||
def get_http_headers(self, request: Request) -> dict[str, str]:
|
def get_http_headers(self, request: Request) -> dict[str, str]:
|
||||||
"""Get HTTP Request headers"""
|
"""Get HTTP Request headers"""
|
||||||
headers = {}
|
headers = {}
|
||||||
@ -79,7 +84,7 @@ 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():
|
||||||
return ""
|
return ""
|
||||||
|
@ -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:
|
||||||
|
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()
|
||||||
|
@ -6,12 +6,19 @@ from django.core.cache import cache
|
|||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
from packaging.version import parse
|
from packaging.version import parse
|
||||||
from prometheus_client import Info
|
from prometheus_client import Info
|
||||||
from requests import RequestException, get
|
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.utils.http import get_http_session
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -33,15 +40,33 @@ 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 notification_version == __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"):
|
||||||
|
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
|
||||||
|
self.set_status(TaskResult(TaskResultStatus.WARNING, messages=["Version check disabled."]))
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
response = get("https://api.github.com/repos/goauthentik/authentik/releases/latest")
|
response = get_http_session().get(
|
||||||
|
"https://version.goauthentik.io/version.json",
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
tag_name = data.get("tag_name")
|
upstream_version = data.get("stable", {}).get("version")
|
||||||
upstream_version = tag_name.split("/")[1]
|
|
||||||
cache.set(VERSION_CACHE_KEY, upstream_version, VERSION_CACHE_TIMEOUT)
|
cache.set(VERSION_CACHE_KEY, upstream_version, VERSION_CACHE_TIMEOUT)
|
||||||
self.set_status(
|
self.set_status(
|
||||||
TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"])
|
TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"])
|
||||||
@ -58,7 +83,7 @@ def update_latest_version(self: MonitoredTask):
|
|||||||
).exists():
|
).exists():
|
||||||
return
|
return
|
||||||
event_dict = {"new_version": upstream_version}
|
event_dict = {"new_version": upstream_version}
|
||||||
if match := re.search(URL_FINDER, data.get("body", "")):
|
if match := re.search(URL_FINDER, data.get("stable", {}).get("changelog", "")):
|
||||||
event_dict["message"] = f"Changelog: {match.group()}"
|
event_dict["message"] = f"Changelog: {match.group()}"
|
||||||
Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save()
|
Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save()
|
||||||
except (RequestException, IndexError) as exc:
|
except (RequestException, IndexError) as exc:
|
||||||
|
@ -1,81 +1,58 @@
|
|||||||
"""test admin tasks"""
|
"""test admin tasks"""
|
||||||
import json
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from unittest.mock import Mock, patch
|
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from requests.exceptions import RequestException
|
from requests_mock import Mocker
|
||||||
|
|
||||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
RESPONSE_VALID = {
|
||||||
@dataclass
|
"$schema": "https://version.goauthentik.io/schema.json",
|
||||||
class MockResponse:
|
"stable": {
|
||||||
"""Mock class to emulate the methods of requests's Response we need"""
|
"version": "99999999.9999999",
|
||||||
|
"changelog": "See https://goauthentik.io/test",
|
||||||
status_code: int
|
"reason": "bugfix",
|
||||||
response: str
|
},
|
||||||
|
}
|
||||||
def json(self) -> dict:
|
|
||||||
"""Get json parsed response"""
|
|
||||||
return json.loads(self.response)
|
|
||||||
|
|
||||||
def raise_for_status(self):
|
|
||||||
"""raise RequestException if status code is 400 or more"""
|
|
||||||
if self.status_code >= 400:
|
|
||||||
raise RequestException
|
|
||||||
|
|
||||||
|
|
||||||
REQUEST_MOCK_VALID = Mock(
|
|
||||||
return_value=MockResponse(
|
|
||||||
200,
|
|
||||||
"""{
|
|
||||||
"tag_name": "version/99999999.9999999",
|
|
||||||
"body": "https://goauthentik.io/test"
|
|
||||||
}""",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
REQUEST_MOCK_INVALID = Mock(return_value=MockResponse(400, "{}"))
|
|
||||||
|
|
||||||
|
|
||||||
class TestAdminTasks(TestCase):
|
class TestAdminTasks(TestCase):
|
||||||
"""test admin tasks"""
|
"""test admin tasks"""
|
||||||
|
|
||||||
@patch("authentik.admin.tasks.get", REQUEST_MOCK_VALID)
|
|
||||||
def test_version_valid_response(self):
|
def test_version_valid_response(self):
|
||||||
"""Test Update checker with valid response"""
|
"""Test Update checker with valid response"""
|
||||||
update_latest_version.delay().get()
|
with Mocker() as mocker:
|
||||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999")
|
mocker.get("https://version.goauthentik.io/version.json", json=RESPONSE_VALID)
|
||||||
self.assertTrue(
|
update_latest_version.delay().get()
|
||||||
Event.objects.filter(
|
self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999")
|
||||||
action=EventAction.UPDATE_AVAILABLE,
|
self.assertTrue(
|
||||||
context__new_version="99999999.9999999",
|
|
||||||
context__message="Changelog: https://goauthentik.io/test",
|
|
||||||
).exists()
|
|
||||||
)
|
|
||||||
# test that a consecutive check doesn't create a duplicate event
|
|
||||||
update_latest_version.delay().get()
|
|
||||||
self.assertEqual(
|
|
||||||
len(
|
|
||||||
Event.objects.filter(
|
Event.objects.filter(
|
||||||
action=EventAction.UPDATE_AVAILABLE,
|
action=EventAction.UPDATE_AVAILABLE,
|
||||||
context__new_version="99999999.9999999",
|
context__new_version="99999999.9999999",
|
||||||
context__message="Changelog: https://goauthentik.io/test",
|
context__message="Changelog: https://goauthentik.io/test",
|
||||||
)
|
).exists()
|
||||||
),
|
)
|
||||||
1,
|
# test that a consecutive check doesn't create a duplicate event
|
||||||
)
|
update_latest_version.delay().get()
|
||||||
|
self.assertEqual(
|
||||||
|
len(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.UPDATE_AVAILABLE,
|
||||||
|
context__new_version="99999999.9999999",
|
||||||
|
context__message="Changelog: https://goauthentik.io/test",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
@patch("authentik.admin.tasks.get", REQUEST_MOCK_INVALID)
|
|
||||||
def test_version_error(self):
|
def test_version_error(self):
|
||||||
"""Test Update checker with invalid response"""
|
"""Test Update checker with invalid response"""
|
||||||
update_latest_version.delay().get()
|
with Mocker() as mocker:
|
||||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0")
|
mocker.get("https://version.goauthentik.io/version.json", status_code=400)
|
||||||
self.assertFalse(
|
update_latest_version.delay().get()
|
||||||
Event.objects.filter(
|
self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0")
|
||||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
|
self.assertFalse(
|
||||||
).exists()
|
Event.objects.filter(
|
||||||
)
|
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
|
||||||
|
).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
|
||||||
|
|
||||||
@ -33,18 +34,19 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
|
|||||||
raise AuthenticationFailed("Malformed header")
|
raise AuthenticationFailed("Malformed header")
|
||||||
# Accept credentials with username and without
|
# Accept credentials with username and without
|
||||||
if ":" in auth_credentials:
|
if ":" in auth_credentials:
|
||||||
_, password = auth_credentials.split(":")
|
_, _, password = auth_credentials.partition(":")
|
||||||
else:
|
else:
|
||||||
password = auth_credentials
|
password = auth_credentials
|
||||||
if password == "": # nosec
|
if password == "": # nosec
|
||||||
raise AuthenticationFailed("Malformed header")
|
raise AuthenticationFailed("Malformed header")
|
||||||
tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
|
tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
|
||||||
if not tokens.exists():
|
if not tokens.exists():
|
||||||
LOGGER.info("Authenticating via secret_key")
|
|
||||||
user = token_secret_key(password)
|
user = token_secret_key(password)
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@ -58,6 +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
|
||||||
|
if hasattr(LOCAL, "authentik"):
|
||||||
|
LOCAL.authentik[KEY_AUTH_VIA] = "secret_key"
|
||||||
outpost = outposts.first()
|
outpost = outposts.first()
|
||||||
return outpost.user
|
return outpost.user
|
||||||
|
|
||||||
|
@ -33,3 +33,12 @@ class OwnerPermissions(BasePermission):
|
|||||||
if owner != request.user:
|
if owner != request.user:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class OwnerSuperuserPermissions(OwnerPermissions):
|
||||||
|
"""Similar to OwnerPermissions, except always allow access for superusers"""
|
||||||
|
|
||||||
|
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
|
||||||
|
if request.user.is_superuser:
|
||||||
|
return True
|
||||||
|
return super().has_object_permission(request, view, obj)
|
||||||
|
@ -5,6 +5,9 @@ from typing import Callable, Optional
|
|||||||
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.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
def permission_required(perm: Optional[str] = None, other_perms: Optional[list[str]] = None):
|
def permission_required(perm: Optional[str] = None, other_perms: Optional[list[str]] = None):
|
||||||
@ -18,10 +21,12 @@ def permission_required(perm: Optional[str] = None, other_perms: Optional[list[s
|
|||||||
if perm:
|
if perm:
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
if not request.user.has_perm(perm, obj):
|
if not request.user.has_perm(perm, obj):
|
||||||
|
LOGGER.debug("denying access for object", user=request.user, perm=perm, obj=obj)
|
||||||
return self.permission_denied(request)
|
return self.permission_denied(request)
|
||||||
if other_perms:
|
if other_perms:
|
||||||
for other_perm in other_perms:
|
for other_perm in other_perms:
|
||||||
if not request.user.has_perm(other_perm):
|
if not request.user.has_perm(other_perm):
|
||||||
|
LOGGER.debug("denying access for other", user=request.user, perm=perm)
|
||||||
return self.permission_denied(request)
|
return self.permission_denied(request)
|
||||||
return func(self, request, *args, **kwargs)
|
return func(self, request, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -31,7 +31,7 @@ VALIDATION_ERROR = build_object_type(
|
|||||||
"non_field_errors": build_array_type(build_standard_type(OpenApiTypes.STR)),
|
"non_field_errors": build_array_type(build_standard_type(OpenApiTypes.STR)),
|
||||||
"code": build_standard_type(OpenApiTypes.STR),
|
"code": build_standard_type(OpenApiTypes.STR),
|
||||||
},
|
},
|
||||||
required=["detail"],
|
required=[],
|
||||||
additionalProperties={},
|
additionalProperties={},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
18
authentik/api/throttle.py
Normal file
18
authentik/api/throttle.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"""Throttling classes"""
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from django.views import View
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.throttling import ScopedRateThrottle
|
||||||
|
|
||||||
|
|
||||||
|
class SessionThrottle(ScopedRateThrottle):
|
||||||
|
"""Throttle based on session key"""
|
||||||
|
|
||||||
|
def allow_request(self, request: Request, view):
|
||||||
|
if request._request.user.is_superuser:
|
||||||
|
return True
|
||||||
|
return super().allow_request(request, view)
|
||||||
|
|
||||||
|
def get_cache_key(self, request: Request, view: Type[View]) -> str:
|
||||||
|
return f"authentik-throttle-session-{request._request.session.session_key}"
|
@ -1,8 +1,10 @@
|
|||||||
"""authentik api urls"""
|
"""authentik api urls"""
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from authentik.api.v2.urls import urlpatterns as v2_urls
|
from authentik.api.v3.urls import urlpatterns as v3_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("v2beta/", include(v2_urls)),
|
# Remove in 2022.1
|
||||||
|
path("v2beta/", include(v3_urls)),
|
||||||
|
path("v3/", include(v3_urls)),
|
||||||
]
|
]
|
||||||
|
@ -1,38 +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 django.views.generic.base import View
|
|
||||||
from requests import post
|
|
||||||
from requests.exceptions import RequestException
|
|
||||||
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
|
|
||||||
|
|
||||||
class SentryTunnelView(View):
|
|
||||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
|
||||||
|
|
||||||
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):
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
# Body is 2 json objects separated by \n
|
|
||||||
full_body = request.body
|
|
||||||
header = loads(full_body.splitlines()[0])
|
|
||||||
# Check that the DSN is what we expect
|
|
||||||
dsn = header.get("dsn", "")
|
|
||||||
if dsn != settings.SENTRY_DSN:
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
response = post(
|
|
||||||
"https://sentry.beryju.org/api/8/envelope/",
|
|
||||||
data=full_body,
|
|
||||||
headers={"Content-Type": "application/octet-stream"},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
response.raise_for_status()
|
|
||||||
except RequestException:
|
|
||||||
return HttpResponse(status=500)
|
|
||||||
return HttpResponse(status=response.status_code)
|
|
@ -63,7 +63,7 @@ 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_enabled": CONFIG.y("error_reporting.enabled"),
|
@ -1,6 +1,6 @@
|
|||||||
"""api v2 urls"""
|
"""api v3 urls"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.cache import cache_page
|
||||||
from drf_spectacular.views import SpectacularAPIView
|
from drf_spectacular.views import SpectacularAPIView
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
@ -10,26 +10,28 @@ from authentik.admin.api.system import SystemView
|
|||||||
from authentik.admin.api.tasks import TaskViewSet
|
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.v2.config import ConfigView
|
from authentik.api.v3.config import ConfigView
|
||||||
from authentik.api.v2.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
|
||||||
from authentik.events.api.event import EventViewSet
|
from authentik.events.api.event import EventViewSet
|
||||||
from authentik.events.api.notification import NotificationViewSet
|
from authentik.events.api.notification import NotificationViewSet
|
||||||
|
from authentik.events.api.notification_mapping import NotificationWebhookMappingViewSet
|
||||||
from authentik.events.api.notification_rule import NotificationRuleViewSet
|
from authentik.events.api.notification_rule import NotificationRuleViewSet
|
||||||
from authentik.events.api.notification_transport import NotificationTransportViewSet
|
from authentik.events.api.notification_transport import NotificationTransportViewSet
|
||||||
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,
|
||||||
@ -66,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,
|
||||||
@ -98,6 +105,7 @@ from authentik.stages.user_write.api import UserWriteStageViewSet
|
|||||||
from authentik.tenants.api import TenantViewSet
|
from authentik.tenants.api import TenantViewSet
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
|
router.include_format_suffixes = False
|
||||||
|
|
||||||
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
|
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
|
||||||
router.register("admin/apps", AppsViewSet, basename="apps")
|
router.register("admin/apps", AppsViewSet, basename="apps")
|
||||||
@ -128,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)
|
||||||
@ -159,8 +168,11 @@ router.register("propertymappings/all", PropertyMappingViewSet)
|
|||||||
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
||||||
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||||
router.register("propertymappings/scope", ScopeMappingViewSet)
|
router.register("propertymappings/scope", ScopeMappingViewSet)
|
||||||
|
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)
|
||||||
@ -169,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,
|
||||||
@ -183,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)
|
||||||
@ -225,7 +243,11 @@ urlpatterns = (
|
|||||||
FlowExecutorView.as_view(),
|
FlowExecutorView.as_view(),
|
||||||
name="flow-executor",
|
name="flow-executor",
|
||||||
),
|
),
|
||||||
path("sentry/", csrf_exempt(SentryTunnelView.as_view()), name="sentry"),
|
path(
|
||||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
"flows/inspector/<slug:flow_slug>/",
|
||||||
|
FlowInspectorView.as_view(),
|
||||||
|
name="flow-inspector",
|
||||||
|
),
|
||||||
|
path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
|
||||||
]
|
]
|
||||||
)
|
)
|
@ -4,14 +4,9 @@ from django.db.models import QuerySet
|
|||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import (
|
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||||
OpenApiParameter,
|
|
||||||
OpenApiResponse,
|
|
||||||
extend_schema,
|
|
||||||
inline_serializer,
|
|
||||||
)
|
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import BooleanField, CharField, FileField, ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -24,6 +19,7 @@ from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
|||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
from authentik.core.api.utils import FilePathSerializer, FileUploadSerializer
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application, User
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import EventAction
|
||||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||||
@ -71,7 +67,7 @@ class ApplicationSerializer(ModelSerializer):
|
|||||||
class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Application Viewset"""
|
"""Application Viewset"""
|
||||||
|
|
||||||
queryset = Application.objects.all()
|
queryset = Application.objects.all().prefetch_related("provider")
|
||||||
serializer_class = ApplicationSerializer
|
serializer_class = ApplicationSerializer
|
||||||
search_fields = [
|
search_fields = [
|
||||||
"name",
|
"name",
|
||||||
@ -122,7 +118,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
# If the current user is superuser, they can set `for_user`
|
# If the current user is superuser, they can set `for_user`
|
||||||
for_user = request.user
|
for_user = request.user
|
||||||
if request.user.is_superuser and "for_user" in request.query_params:
|
if request.user.is_superuser and "for_user" in request.query_params:
|
||||||
for_user = get_object_or_404(User, pk=request.query_params.get("for_user"))
|
try:
|
||||||
|
for_user = get_object_or_404(User, pk=request.query_params.get("for_user"))
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest("for_user must be numerical")
|
||||||
engine = PolicyEngine(application, for_user, request)
|
engine = PolicyEngine(application, for_user, request)
|
||||||
engine.use_cache = False
|
engine.use_cache = False
|
||||||
engine.build()
|
engine.build()
|
||||||
@ -177,13 +176,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
@permission_required("authentik_core.change_application")
|
@permission_required("authentik_core.change_application")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request={
|
request={
|
||||||
"multipart/form-data": inline_serializer(
|
"multipart/form-data": FileUploadSerializer,
|
||||||
"SetIcon",
|
|
||||||
fields={
|
|
||||||
"file": FileField(required=False),
|
|
||||||
"clear": BooleanField(default=False),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
responses={
|
responses={
|
||||||
200: OpenApiResponse(description="Success"),
|
200: OpenApiResponse(description="Success"),
|
||||||
@ -215,7 +208,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
|
|
||||||
@permission_required("authentik_core.change_application")
|
@permission_required("authentik_core.change_application")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=inline_serializer("SetIconURL", fields={"url": CharField()}),
|
request=FilePathSerializer,
|
||||||
responses={
|
responses={
|
||||||
200: OpenApiResponse(description="Success"),
|
200: OpenApiResponse(description="Success"),
|
||||||
400: OpenApiResponse(description="Bad request"),
|
400: OpenApiResponse(description="Bad request"),
|
||||||
|
@ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer
|
|||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from ua_parser import user_agent_parser
|
from ua_parser import user_agent_parser
|
||||||
|
|
||||||
|
from authentik.api.authorization import OwnerSuperuserPermissions
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.models import AuthenticatedSession
|
from authentik.core.models import AuthenticatedSession
|
||||||
from authentik.events.geo import GEOIP_READER, GeoIPDict
|
from authentik.events.geo import GEOIP_READER, GeoIPDict
|
||||||
@ -102,11 +103,8 @@ class AuthenticatedSessionViewSet(
|
|||||||
search_fields = ["user__username", "last_ip", "last_user_agent"]
|
search_fields = ["user__username", "last_ip", "last_user_agent"]
|
||||||
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
|
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
|
||||||
ordering = ["user__username"]
|
ordering = ["user__username"]
|
||||||
filter_backends = [
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
DjangoFilterBackend,
|
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
OrderingFilter,
|
|
||||||
SearchFilter,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
user = self.request.user if self.request else get_anonymous_user()
|
||||||
|
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)
|
@ -2,7 +2,7 @@
|
|||||||
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 ModelMultipleChoiceFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from rest_framework.fields import BooleanField, CharField, JSONField
|
from rest_framework.fields import CharField, JSONField
|
||||||
from rest_framework.serializers import ListSerializer, ModelSerializer
|
from rest_framework.serializers import ListSerializer, ModelSerializer
|
||||||
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
|
||||||
@ -15,7 +15,6 @@ from authentik.core.models import Group, User
|
|||||||
class GroupMemberSerializer(ModelSerializer):
|
class GroupMemberSerializer(ModelSerializer):
|
||||||
"""Stripped down user serializer to show relevant users for groups"""
|
"""Stripped down user serializer to show relevant users for groups"""
|
||||||
|
|
||||||
is_superuser = BooleanField(read_only=True)
|
|
||||||
avatar = CharField(read_only=True)
|
avatar = CharField(read_only=True)
|
||||||
attributes = JSONField(validators=[is_dict], required=False)
|
attributes = JSONField(validators=[is_dict], required=False)
|
||||||
uid = CharField(read_only=True)
|
uid = CharField(read_only=True)
|
||||||
@ -29,7 +28,6 @@ class GroupMemberSerializer(ModelSerializer):
|
|||||||
"name",
|
"name",
|
||||||
"is_active",
|
"is_active",
|
||||||
"last_login",
|
"last_login",
|
||||||
"is_superuser",
|
|
||||||
"email",
|
"email",
|
||||||
"avatar",
|
"avatar",
|
||||||
"attributes",
|
"attributes",
|
||||||
@ -81,7 +79,7 @@ class GroupFilter(FilterSet):
|
|||||||
class GroupViewSet(UsedByMixin, ModelViewSet):
|
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Group Viewset"""
|
"""Group Viewset"""
|
||||||
|
|
||||||
queryset = Group.objects.all()
|
queryset = Group.objects.all().select_related("parent").prefetch_related("users")
|
||||||
serializer_class = GroupSerializer
|
serializer_class = GroupSerializer
|
||||||
search_fields = ["name", "is_superuser"]
|
search_fields = ["name", "is_superuser"]
|
||||||
filterset_class = GroupFilter
|
filterset_class = GroupFilter
|
||||||
|
@ -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
|
||||||
@ -95,7 +98,9 @@ class SourceViewSet(
|
|||||||
@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 sources the user can configure"""
|
"""Get all sources the user can configure"""
|
||||||
_all_sources: Iterable[Source] = Source.objects.filter(enabled=True).select_subclasses()
|
_all_sources: Iterable[Source] = (
|
||||||
|
Source.objects.filter(enabled=True).select_subclasses().order_by("name")
|
||||||
|
)
|
||||||
matching_sources: list[UserSettingSerializer] = []
|
matching_sources: list[UserSettingSerializer] = []
|
||||||
for source in _all_sources:
|
for source in _all_sources:
|
||||||
user_settings = source.ui_user_settings
|
user_settings = source.ui_user_settings
|
||||||
@ -111,3 +116,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"]
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
"""Tokens API Viewset"""
|
"""Tokens API Viewset"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
|
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
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.api.authorization import OwnerSuperuserPermissions
|
||||||
from authentik.api.decorators import permission_required
|
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.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
@ -20,7 +27,16 @@ from authentik.managed.api import ManagedSerializer
|
|||||||
class TokenSerializer(ManagedSerializer, ModelSerializer):
|
class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||||
"""Token Serializer"""
|
"""Token Serializer"""
|
||||||
|
|
||||||
user = UserSerializer(required=False)
|
user_obj = UserSerializer(required=False, source="user")
|
||||||
|
|
||||||
|
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||||
|
"""Ensure only API or App password tokens are created."""
|
||||||
|
request: Request = self.context["request"]
|
||||||
|
attrs.setdefault("user", request.user)
|
||||||
|
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
||||||
|
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
||||||
|
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
|
||||||
|
return attrs
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -31,11 +47,14 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
|||||||
"identifier",
|
"identifier",
|
||||||
"intent",
|
"intent",
|
||||||
"user",
|
"user",
|
||||||
|
"user_obj",
|
||||||
"description",
|
"description",
|
||||||
"expires",
|
"expires",
|
||||||
"expiring",
|
"expiring",
|
||||||
]
|
]
|
||||||
depth = 2
|
extra_kwargs = {
|
||||||
|
"user": {"required": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TokenViewSerializer(PassiveSerializer):
|
class TokenViewSerializer(PassiveSerializer):
|
||||||
@ -63,15 +82,25 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"description",
|
"description",
|
||||||
"expires",
|
"expires",
|
||||||
"expiring",
|
"expiring",
|
||||||
|
"managed",
|
||||||
]
|
]
|
||||||
ordering = ["expires"]
|
ordering = ["identifier", "expires"]
|
||||||
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
|
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user if self.request else get_anonymous_user()
|
||||||
|
if user.is_superuser:
|
||||||
|
return super().get_queryset()
|
||||||
|
return super().get_queryset().filter(user=user.pk)
|
||||||
|
|
||||||
def perform_create(self, serializer: TokenSerializer):
|
def perform_create(self, serializer: TokenSerializer):
|
||||||
serializer.save(
|
if not self.request.user.is_superuser:
|
||||||
user=self.request.user,
|
return serializer.save(
|
||||||
intent=TokenIntents.INTENT_API,
|
user=self.request.user,
|
||||||
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
|
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
|
||||||
)
|
)
|
||||||
|
return super().perform_create(serializer)
|
||||||
|
|
||||||
@permission_required("authentik_core.view_token_key")
|
@permission_required("authentik_core.view_token_key")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
|
from datetime import timedelta
|
||||||
from json import loads
|
from json import loads
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
from django.db.transaction import atomic
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
|
from django.utils.text import slugify
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
|
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_field
|
from drf_spectacular.utils import (
|
||||||
|
OpenApiParameter,
|
||||||
|
extend_schema,
|
||||||
|
extend_schema_field,
|
||||||
|
inline_serializer,
|
||||||
|
)
|
||||||
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
|
||||||
@ -34,7 +44,16 @@ from authentik.core.api.groups import GroupSerializer
|
|||||||
from authentik.core.api.used_by import UsedByMixin
|
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 Group, Token, TokenIntents, User
|
from authentik.core.models import (
|
||||||
|
USER_ATTRIBUTE_CHANGE_EMAIL,
|
||||||
|
USER_ATTRIBUTE_CHANGE_USERNAME,
|
||||||
|
USER_ATTRIBUTE_SA,
|
||||||
|
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||||
|
Group,
|
||||||
|
Token,
|
||||||
|
TokenIntents,
|
||||||
|
User,
|
||||||
|
)
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import EventAction
|
||||||
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
|
||||||
@ -73,6 +92,9 @@ class UserSerializer(ModelSerializer):
|
|||||||
"attributes",
|
"attributes",
|
||||||
"uid",
|
"uid",
|
||||||
]
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
"name": {"allow_blank": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserSelfSerializer(ModelSerializer):
|
class UserSelfSerializer(ModelSerializer):
|
||||||
@ -81,8 +103,41 @@ 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, 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, True):
|
||||||
|
return username
|
||||||
|
if username != self.instance.username:
|
||||||
|
raise ValidationError("Not allowed to change username.")
|
||||||
|
return username
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -97,9 +152,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},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -191,6 +248,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
|
||||||
@ -220,18 +278,65 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
)
|
)
|
||||||
return link, token
|
return link, token
|
||||||
|
|
||||||
|
@permission_required(None, ["authentik_core.add_user", "authentik_core.add_token"])
|
||||||
|
@extend_schema(
|
||||||
|
request=inline_serializer(
|
||||||
|
"UserServiceAccountSerializer",
|
||||||
|
{
|
||||||
|
"name": CharField(required=True),
|
||||||
|
"create_group": BooleanField(default=False),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
200: inline_serializer(
|
||||||
|
"UserServiceAccountResponse",
|
||||||
|
{
|
||||||
|
"username": CharField(required=True),
|
||||||
|
"token": CharField(required=True),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=False, methods=["POST"], pagination_class=None, filter_backends=[])
|
||||||
|
def service_account(self, request: Request) -> Response:
|
||||||
|
"""Create a new user account that is marked as a service account"""
|
||||||
|
username = request.data.get("name")
|
||||||
|
create_group = request.data.get("create_group", False)
|
||||||
|
with atomic():
|
||||||
|
try:
|
||||||
|
user = User.objects.create(
|
||||||
|
username=username,
|
||||||
|
name=username,
|
||||||
|
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
||||||
|
)
|
||||||
|
if create_group:
|
||||||
|
group = Group.objects.create(
|
||||||
|
name=username,
|
||||||
|
)
|
||||||
|
group.users.add(user)
|
||||||
|
token = Token.objects.create(
|
||||||
|
identifier=slugify(f"service-account-{username}-password"),
|
||||||
|
intent=TokenIntents.INTENT_APP_PASSWORD,
|
||||||
|
user=user,
|
||||||
|
expires=now() + timedelta(days=360),
|
||||||
|
)
|
||||||
|
return Response({"username": user.username, "token": token.key})
|
||||||
|
except (IntegrityError) as exc:
|
||||||
|
return Response(data={"non_field_errors": [str(exc)]}, status=400)
|
||||||
|
|
||||||
@extend_schema(responses={200: SessionUserSerializer(many=False)})
|
@extend_schema(responses={200: SessionUserSerializer(many=False)})
|
||||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
def me(self, request: Request) -> Response:
|
def me(self, request: Request) -> Response:
|
||||||
"""Get information about current user"""
|
"""Get information about current user"""
|
||||||
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(
|
||||||
@ -245,13 +350,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
|
||||||
return self.me(request)
|
return Response({"user": data.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)})
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from rest_framework.fields import CharField, IntegerField
|
from rest_framework.fields import BooleanField, CharField, FileField, IntegerField
|
||||||
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
|
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
|
||||||
|
|
||||||
|
|
||||||
@ -22,8 +22,18 @@ class PassiveSerializer(Serializer):
|
|||||||
def update(self, instance: Model, validated_data: dict) -> Model: # pragma: no cover
|
def update(self, instance: Model, validated_data: dict) -> Model: # pragma: no cover
|
||||||
return Model()
|
return Model()
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Model
|
class FileUploadSerializer(PassiveSerializer):
|
||||||
|
"""Serializer to upload file"""
|
||||||
|
|
||||||
|
file = FileField(required=False)
|
||||||
|
clear = BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class FilePathSerializer(PassiveSerializer):
|
||||||
|
"""Serializer to upload file"""
|
||||||
|
|
||||||
|
url = CharField()
|
||||||
|
|
||||||
|
|
||||||
class MetaNameSerializer(PassiveSerializer):
|
class MetaNameSerializer(PassiveSerializer):
|
||||||
|
59
authentik/core/auth.py
Normal file
59
authentik/core/auth.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"""Authenticate with tokens"""
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
|
||||||
|
from authentik.core.models import Token, TokenIntents, User
|
||||||
|
from authentik.events.utils import cleanse_dict, sanitize_dict
|
||||||
|
from authentik.flows.planner import FlowPlan
|
||||||
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
|
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||||
|
|
||||||
|
|
||||||
|
class InbuiltBackend(ModelBackend):
|
||||||
|
"""Inbuilt backend"""
|
||||||
|
|
||||||
|
def authenticate(
|
||||||
|
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
|
||||||
|
) -> Optional[User]:
|
||||||
|
user = super().authenticate(request, username=username, password=password, **kwargs)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
self.set_method("password", request)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def set_method(self, method: str, request: Optional[HttpRequest], **kwargs):
|
||||||
|
"""Set method data on current flow, if possbiel"""
|
||||||
|
if not request:
|
||||||
|
return
|
||||||
|
# Since we can't directly pass other variables to signals, and we want to log the method
|
||||||
|
# and the token used, we assume we're running in a flow and set a variable in the context
|
||||||
|
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
||||||
|
flow_plan.context[PLAN_CONTEXT_METHOD] = method
|
||||||
|
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = cleanse_dict(sanitize_dict(kwargs))
|
||||||
|
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||||
|
|
||||||
|
|
||||||
|
class TokenBackend(InbuiltBackend):
|
||||||
|
"""Authenticate with token"""
|
||||||
|
|
||||||
|
def authenticate(
|
||||||
|
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
|
||||||
|
) -> Optional[User]:
|
||||||
|
try:
|
||||||
|
user = User._default_manager.get_by_natural_key(username)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
# Run the default password hasher once to reduce the timing
|
||||||
|
# difference between an existing and a nonexistent user (#20760).
|
||||||
|
User().set_password(password)
|
||||||
|
return None
|
||||||
|
tokens = Token.filter_not_expired(
|
||||||
|
user=user, key=password, intent=TokenIntents.INTENT_APP_PASSWORD
|
||||||
|
)
|
||||||
|
if not tokens.exists():
|
||||||
|
return None
|
||||||
|
token = tokens.first()
|
||||||
|
self.set_method("password", request, token=token)
|
||||||
|
return token.user
|
@ -10,6 +10,9 @@ 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"
|
||||||
|
INTERNAL_HEADER_PREFIX = "X-authentik-internal-"
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateMiddleware:
|
class ImpersonateMiddleware:
|
||||||
@ -50,15 +53,17 @@ 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"]
|
if auth_via := LOCAL.authentik.get(KEY_AUTH_VIA, None):
|
||||||
del LOCAL.authentik["host"]
|
response[INTERNAL_HEADER_PREFIX + KEY_AUTH_VIA] = auth_via
|
||||||
|
response[INTERNAL_HEADER_PREFIX + 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", "")
|
|
||||||
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.core.validators
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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=[django.core.validators.URLValidator()]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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",
|
||||||
|
26
authentik/core/migrations/0028_alter_token_intent.py
Normal file
26
authentik/core/migrations/0028_alter_token_intent.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 3.2.6 on 2021-08-23 14:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0027_bootstrap_token"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -28,6 +28,7 @@ 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.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.models import CreatedUpdatedModel, SerializerModel
|
from authentik.lib.models import CreatedUpdatedModel, 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
|
||||||
@ -38,6 +39,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"
|
||||||
@ -54,7 +57,9 @@ def default_token_duration():
|
|||||||
|
|
||||||
def default_token_key():
|
def default_token_key():
|
||||||
"""Default token key"""
|
"""Default token key"""
|
||||||
return uuid4().hex
|
# We use generate_id since the chars in the key should be easy
|
||||||
|
# to use in Emails (for verification) and URLs (for recovery)
|
||||||
|
return generate_id(128)
|
||||||
|
|
||||||
|
|
||||||
class Group(models.Model):
|
class Group(models.Model):
|
||||||
@ -280,7 +285,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."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -408,6 +413,9 @@ class TokenIntents(models.TextChoices):
|
|||||||
# Recovery use for the recovery app
|
# Recovery use for the recovery app
|
||||||
INTENT_RECOVERY = "recovery"
|
INTENT_RECOVERY = "recovery"
|
||||||
|
|
||||||
|
# App-specific passwords
|
||||||
|
INTENT_APP_PASSWORD = "app_password" # nosec
|
||||||
|
|
||||||
|
|
||||||
class Token(ManagedModel, ExpiringModel):
|
class Token(ManagedModel, ExpiringModel):
|
||||||
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
|
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
|
||||||
|
@ -22,10 +22,10 @@ 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_DJANGO
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
|
|
||||||
@ -184,12 +184,12 @@ class SourceFlowManager:
|
|||||||
# Ensure redirect is carried through when user was trying to
|
# Ensure redirect is carried through when user was trying to
|
||||||
# authorize application
|
# authorize application
|
||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
NEXT_ARG_NAME, "authentik_core:if-admin"
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
)
|
)
|
||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
# Since we authenticate the user by their token, they have no backend set
|
# Since we authenticate the user by their token, they have no backend set
|
||||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||||
PLAN_CONTEXT_SSO: True,
|
PLAN_CONTEXT_SSO: True,
|
||||||
PLAN_CONTEXT_SOURCE: self.source,
|
PLAN_CONTEXT_SOURCE: self.source,
|
||||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||||
@ -243,9 +243,9 @@ class SourceFlowManager:
|
|||||||
return self.handle_auth_user(connection)
|
return self.handle_auth_user(connection)
|
||||||
return redirect(
|
return redirect(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_core:if-admin",
|
"authentik_core:if-user",
|
||||||
)
|
)
|
||||||
+ f"#/user;page-{self.source.slug}"
|
+ f"#/settings;page-{self.source.slug}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_enroll(
|
def handle_enroll(
|
||||||
|
@ -28,3 +28,7 @@ class PostUserEnrollmentStage(StageView):
|
|||||||
source=connection.source,
|
source=connection.source,
|
||||||
).from_http(self.request)
|
).from_http(self.request)
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|
||||||
|
def post(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
"""Wrapper for post requests"""
|
||||||
|
return self.get(request)
|
||||||
|
@ -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()
|
||||||
|
@ -8,16 +8,15 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
|
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
|
||||||
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
|
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}">
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
<script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script>
|
<script src="{% static 'dist/poly.js' %}" type="module"></script>
|
||||||
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/AdminInterface.js' %}?v={{ ak_version }}" type="module"></script>
|
<script src="{% static 'dist/AdminInterface.js' %}" type="module"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
@ -21,7 +21,7 @@ You've logged out of {{ application }}.
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a id="ak-back-home" href="{% url 'authentik_core:if-admin' %}" class="pf-c-button pf-m-primary">{% trans 'Go back to overview' %}</a>
|
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">{% trans 'Go back to overview' %}</a>
|
||||||
|
|
||||||
<a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">{% trans 'Log out of authentik' %}</a>
|
<a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">{% trans 'Log out of authentik' %}</a>
|
||||||
|
|
||||||
|
@ -4,13 +4,14 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% if flow.compatibility_mode %}
|
{{ block.super }}
|
||||||
|
{% 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' %}?v={{ ak_version }}" type="module"></script>
|
<script src="{% static 'dist/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 }}");
|
||||||
|
28
authentik/core/templates/if/user.html
Normal file
28
authentik/core/templates/if/user.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="{% static 'dist/UserInterface.js' %}" type="module"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<ak-message-container></ak-message-container>
|
||||||
|
<ak-interface-user>
|
||||||
|
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||||
|
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||||
|
<div class="pf-c-empty-state__content">
|
||||||
|
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
||||||
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
</span>
|
||||||
|
<h1 class="pf-c-title pf-m-lg">
|
||||||
|
{% trans "Loading..." %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</ak-interface-user>
|
||||||
|
{% endblock %}
|
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
@ -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>
|
||||||
|
@ -58,4 +58,4 @@ class TestImpersonation(TestCase):
|
|||||||
self.client.force_login(self.other_user)
|
self.client.force_login(self.other_user)
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_core:impersonate-end"))
|
response = self.client.get(reverse("authentik_core:impersonate-end"))
|
||||||
self.assertRedirects(response, reverse("authentik_core:if-admin"))
|
self.assertRedirects(response, reverse("authentik_core:if-user"))
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
"""Test Source flow_manager"""
|
"""Test Source flow_manager"""
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.messages.middleware import MessageMiddleware
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
|
||||||
from django.http.request import HttpRequest
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from guardian.utils import get_anonymous_user
|
from guardian.utils import get_anonymous_user
|
||||||
|
|
||||||
from authentik.core.models import SourceUserMatchingModes, User
|
from authentik.core.models import SourceUserMatchingModes, User
|
||||||
from authentik.core.sources.flow_manager import Action
|
from authentik.core.sources.flow_manager import Action
|
||||||
from authentik.flows.tests.test_planner import dummy_get_response
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.generators import generate_client_id
|
from authentik.lib.tests.utils import get_request
|
||||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||||
from authentik.sources.oauth.views.callback import OAuthSourceFlowManager
|
from authentik.sources.oauth.views.callback import OAuthSourceFlowManager
|
||||||
|
|
||||||
@ -22,24 +19,12 @@ class TestSourceFlowManager(TestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
self.source = OAuthSource.objects.create(name="test")
|
self.source = OAuthSource.objects.create(name="test")
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.identifier = generate_client_id()
|
self.identifier = generate_id()
|
||||||
|
|
||||||
def get_request(self, user: User) -> HttpRequest:
|
|
||||||
"""Helper to create a get request with session and message middleware"""
|
|
||||||
request = self.factory.get("/")
|
|
||||||
request.user = user
|
|
||||||
middleware = SessionMiddleware(dummy_get_response)
|
|
||||||
middleware.process_request(request)
|
|
||||||
request.session.save()
|
|
||||||
middleware = MessageMiddleware(dummy_get_response)
|
|
||||||
middleware.process_request(request)
|
|
||||||
request.session.save()
|
|
||||||
return request
|
|
||||||
|
|
||||||
def test_unauthenticated_enroll(self):
|
def test_unauthenticated_enroll(self):
|
||||||
"""Test un-authenticated user enrolling"""
|
"""Test un-authenticated user enrolling"""
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||||
)
|
)
|
||||||
action, _ = flow_manager.get_action()
|
action, _ = flow_manager.get_action()
|
||||||
self.assertEqual(action, Action.ENROLL)
|
self.assertEqual(action, Action.ENROLL)
|
||||||
@ -52,7 +37,7 @@ class TestSourceFlowManager(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||||
)
|
)
|
||||||
action, _ = flow_manager.get_action()
|
action, _ = flow_manager.get_action()
|
||||||
self.assertEqual(action, Action.AUTH)
|
self.assertEqual(action, Action.AUTH)
|
||||||
@ -65,7 +50,7 @@ class TestSourceFlowManager(TestCase):
|
|||||||
)
|
)
|
||||||
user = User.objects.create(username="foo", email="foo@bar.baz")
|
user = User.objects.create(username="foo", email="foo@bar.baz")
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
self.source, self.get_request(user), self.identifier, {}
|
self.source, get_request("/", user=user), self.identifier, {}
|
||||||
)
|
)
|
||||||
action, _ = flow_manager.get_action()
|
action, _ = flow_manager.get_action()
|
||||||
self.assertEqual(action, Action.LINK)
|
self.assertEqual(action, Action.LINK)
|
||||||
@ -78,7 +63,7 @@ class TestSourceFlowManager(TestCase):
|
|||||||
|
|
||||||
# Without email, deny
|
# Without email, deny
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||||
)
|
)
|
||||||
action, _ = flow_manager.get_action()
|
action, _ = flow_manager.get_action()
|
||||||
self.assertEqual(action, Action.DENY)
|
self.assertEqual(action, Action.DENY)
|
||||||
@ -86,7 +71,7 @@ class TestSourceFlowManager(TestCase):
|
|||||||
# With email
|
# With email
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
self.source,
|
self.source,
|
||||||
self.get_request(AnonymousUser()),
|
get_request("/", user=AnonymousUser()),
|
||||||
self.identifier,
|
self.identifier,
|
||||||
{"email": "foo@bar.baz"},
|
{"email": "foo@bar.baz"},
|
||||||
)
|
)
|
||||||
@ -101,7 +86,7 @@ class TestSourceFlowManager(TestCase):
|
|||||||
|
|
||||||
# Without username, deny
|
# Without username, deny
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||||
)
|
)
|
||||||
action, _ = flow_manager.get_action()
|
action, _ = flow_manager.get_action()
|
||||||
self.assertEqual(action, Action.DENY)
|
self.assertEqual(action, Action.DENY)
|
||||||
@ -109,7 +94,7 @@ class TestSourceFlowManager(TestCase):
|
|||||||
# With username
|
# With username
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
self.source,
|
self.source,
|
||||||
self.get_request(AnonymousUser()),
|
get_request("/", user=AnonymousUser()),
|
||||||
self.identifier,
|
self.identifier,
|
||||||
{"username": "foo"},
|
{"username": "foo"},
|
||||||
)
|
)
|
||||||
@ -125,7 +110,7 @@ class TestSourceFlowManager(TestCase):
|
|||||||
# With non-existent username, enroll
|
# With non-existent username, enroll
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
self.source,
|
self.source,
|
||||||
self.get_request(AnonymousUser()),
|
get_request("/", user=AnonymousUser()),
|
||||||
self.identifier,
|
self.identifier,
|
||||||
{
|
{
|
||||||
"username": "bar",
|
"username": "bar",
|
||||||
@ -137,7 +122,7 @@ class TestSourceFlowManager(TestCase):
|
|||||||
# With username
|
# With username
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
self.source,
|
self.source,
|
||||||
self.get_request(AnonymousUser()),
|
get_request("/", user=AnonymousUser()),
|
||||||
self.identifier,
|
self.identifier,
|
||||||
{"username": "foo"},
|
{"username": "foo"},
|
||||||
)
|
)
|
||||||
@ -151,7 +136,7 @@ class TestSourceFlowManager(TestCase):
|
|||||||
|
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
self.source,
|
self.source,
|
||||||
self.get_request(AnonymousUser()),
|
get_request("/", user=AnonymousUser()),
|
||||||
self.identifier,
|
self.identifier,
|
||||||
{"username": "foo"},
|
{"username": "foo"},
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""Test token API"""
|
"""Test token API"""
|
||||||
|
from json import loads
|
||||||
|
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
@ -13,7 +15,8 @@ class TestTokenAPI(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.get(username="akadmin")
|
self.user = User.objects.create(username="testuser")
|
||||||
|
self.admin = User.objects.get(username="akadmin")
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_token_create(self):
|
def test_token_create(self):
|
||||||
@ -27,6 +30,14 @@ class TestTokenAPI(APITestCase):
|
|||||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||||
self.assertEqual(token.expiring, True)
|
self.assertEqual(token.expiring, True)
|
||||||
|
|
||||||
|
def test_token_create_invalid(self):
|
||||||
|
"""Test token creation endpoint (invalid data)"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:token-list"),
|
||||||
|
{"identifier": "test-token", "intent": TokenIntents.INTENT_RECOVERY},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_token_create_non_expiring(self):
|
def test_token_create_non_expiring(self):
|
||||||
"""Test token creation endpoint"""
|
"""Test token creation endpoint"""
|
||||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = False
|
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = False
|
||||||
@ -47,3 +58,29 @@ class TestTokenAPI(APITestCase):
|
|||||||
clean_expired_models.delay().get()
|
clean_expired_models.delay().get()
|
||||||
token.refresh_from_db()
|
token.refresh_from_db()
|
||||||
self.assertNotEqual(key, token.key)
|
self.assertNotEqual(key, token.key)
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
"""Test Token List (Test normal authentication)"""
|
||||||
|
token_should: Token = Token.objects.create(
|
||||||
|
identifier="test", expiring=False, user=self.user
|
||||||
|
)
|
||||||
|
Token.objects.create(identifier="test-2", expiring=False, user=get_anonymous_user())
|
||||||
|
response = self.client.get(reverse(("authentik_api:token-list")))
|
||||||
|
body = loads(response.content)
|
||||||
|
self.assertEqual(len(body["results"]), 1)
|
||||||
|
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
||||||
|
|
||||||
|
def test_list_admin(self):
|
||||||
|
"""Test Token List (Test with admin auth)"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
token_should: Token = Token.objects.create(
|
||||||
|
identifier="test", expiring=False, user=self.user
|
||||||
|
)
|
||||||
|
token_should_not: Token = Token.objects.create(
|
||||||
|
identifier="test-2", expiring=False, user=get_anonymous_user()
|
||||||
|
)
|
||||||
|
response = self.client.get(reverse(("authentik_api:token-list")))
|
||||||
|
body = loads(response.content)
|
||||||
|
self.assertEqual(len(body["results"]), 2)
|
||||||
|
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
||||||
|
self.assertEqual(body["results"][1]["identifier"], token_should_not.identifier)
|
||||||
|
40
authentik/core/tests/test_token_auth.py
Normal file
40
authentik/core/tests/test_token_auth.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"""Test token auth"""
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik.core.auth import TokenBackend
|
||||||
|
from authentik.core.models import Token, TokenIntents, User
|
||||||
|
from authentik.flows.planner import FlowPlan
|
||||||
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
|
from authentik.lib.tests.utils import get_request
|
||||||
|
|
||||||
|
|
||||||
|
class TestTokenAuth(TestCase):
|
||||||
|
"""Test token auth"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = User.objects.create(username="test-user")
|
||||||
|
self.token = Token.objects.create(
|
||||||
|
expiring=False, user=self.user, intent=TokenIntents.INTENT_APP_PASSWORD
|
||||||
|
)
|
||||||
|
# To test with session we need to create a request and pass it through all middlewares
|
||||||
|
self.request = get_request("/")
|
||||||
|
self.request.session[SESSION_KEY_PLAN] = FlowPlan("test")
|
||||||
|
|
||||||
|
def test_token_auth(self):
|
||||||
|
"""Test auth with token"""
|
||||||
|
self.assertEqual(
|
||||||
|
TokenBackend().authenticate(self.request, "test-user", self.token.key), self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_token_auth_none(self):
|
||||||
|
"""Test auth with token (non-existent user)"""
|
||||||
|
self.assertIsNone(
|
||||||
|
TokenBackend().authenticate(self.request, "test-user-foo", self.token.key), self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_token_auth_invalid(self):
|
||||||
|
"""Test auth with token (invalid token)"""
|
||||||
|
self.assertIsNone(
|
||||||
|
TokenBackend().authenticate(self.request, "test-user", self.token.key + "foo"),
|
||||||
|
self.user,
|
||||||
|
)
|
@ -2,7 +2,7 @@
|
|||||||
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.flows.models import Flow, 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
|
||||||
@ -15,6 +15,34 @@ class TestUsersAPI(APITestCase):
|
|||||||
self.admin = User.objects.get(username="akadmin")
|
self.admin = User.objects.get(username="akadmin")
|
||||||
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)
|
||||||
@ -105,3 +133,39 @@ class TestUsersAPI(APITestCase):
|
|||||||
+ f"?email_stage={stage.pk}"
|
+ f"?email_stage={stage.pk}"
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 204)
|
self.assertEqual(response.status_code, 204)
|
||||||
|
|
||||||
|
def test_service_account(self):
|
||||||
|
"""Service account creation"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.post(reverse("authentik_api:user-service-account"))
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-service-account"),
|
||||||
|
data={
|
||||||
|
"name": "test-sa",
|
||||||
|
"create_group": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(User.objects.filter(username="test-sa").exists())
|
||||||
|
|
||||||
|
def test_service_account_invalid(self):
|
||||||
|
"""Service account creation (twice with same name, expect error)"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-service-account"),
|
||||||
|
data={
|
||||||
|
"name": "test-sa",
|
||||||
|
"create_group": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(User.objects.filter(username="test-sa").exists())
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-service-account"),
|
||||||
|
data={
|
||||||
|
"name": "test-sa",
|
||||||
|
"create_group": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
@ -12,7 +12,7 @@ from authentik.core.views.session import EndSessionView
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"",
|
"",
|
||||||
login_required(RedirectView.as_view(pattern_name="authentik_core:if-admin")),
|
login_required(RedirectView.as_view(pattern_name="authentik_core:if-user")),
|
||||||
name="root-redirect",
|
name="root-redirect",
|
||||||
),
|
),
|
||||||
# Impersonation
|
# Impersonation
|
||||||
@ -32,6 +32,11 @@ urlpatterns = [
|
|||||||
ensure_csrf_cookie(TemplateView.as_view(template_name="if/admin.html")),
|
ensure_csrf_cookie(TemplateView.as_view(template_name="if/admin.html")),
|
||||||
name="if-admin",
|
name="if-admin",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"if/user/",
|
||||||
|
ensure_csrf_cookie(TemplateView.as_view(template_name="if/user.html")),
|
||||||
|
name="if-user",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"if/flow/<slug:flow_slug>/",
|
"if/flow/<slug:flow_slug>/",
|
||||||
ensure_csrf_cookie(FlowInterfaceView.as_view()),
|
ensure_csrf_cookie(FlowInterfaceView.as_view()),
|
||||||
|
@ -28,7 +28,7 @@ class ImpersonateInitView(View):
|
|||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||||
|
|
||||||
return redirect("authentik_core:if-admin")
|
return redirect("authentik_core:if-user")
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateEndView(View):
|
class ImpersonateEndView(View):
|
||||||
@ -41,7 +41,7 @@ class ImpersonateEndView(View):
|
|||||||
or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session
|
or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||||
):
|
):
|
||||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||||
return redirect("authentik_core:if-admin")
|
return redirect("authentik_core:if-user")
|
||||||
|
|
||||||
original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -99,6 +99,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,13 +135,13 @@ 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__isnull=False)
|
||||||
serializer_class = CertificateKeyPairSerializer
|
serializer_class = CertificateKeyPairSerializer
|
||||||
filterset_class = CertificateKeyPairFilter
|
filterset_class = CertificateKeyPairFilter
|
||||||
|
|
||||||
|
@ -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,6 @@ 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")
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
@ -78,9 +79,7 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
|||||||
@property
|
@property
|
||||||
def kid(self):
|
def kid(self):
|
||||||
"""Get Key ID used for JWKS"""
|
"""Get Key ID used for JWKS"""
|
||||||
return "{0}".format(
|
return md5(self.key_data.encode("utf-8")).hexdigest() if self.key_data else "" # nosec
|
||||||
md5(self.key_data.encode("utf-8")).hexdigest() if self.key_data else "" # nosec
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Certificate-Key Pair {self.name}"
|
return f"Certificate-Key Pair {self.name}"
|
||||||
|
@ -10,7 +10,7 @@ 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.flows.models import Flow
|
||||||
from authentik.providers.oauth2.generators import generate_client_secret
|
from authentik.lib.generators import generate_key
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
|
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ class TestCrypto(TestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
client_secret=generate_client_secret(),
|
client_secret=generate_key(),
|
||||||
authorization_flow=Flow.objects.first(),
|
authorization_flow=Flow.objects.first(),
|
||||||
redirect_uris="http://localhost",
|
redirect_uris="http://localhost",
|
||||||
rsa_key=CertificateKeyPair.objects.first(),
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
"""Notification API Views"""
|
"""Notification API Views"""
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
@ -53,3 +58,18 @@ class NotificationViewSet(
|
|||||||
]
|
]
|
||||||
permission_classes = [OwnerPermissions]
|
permission_classes = [OwnerPermissions]
|
||||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=OpenApiTypes.NONE,
|
||||||
|
responses={
|
||||||
|
204: OpenApiResponse(description="Marked tasks as read successfully."),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=False, methods=["post"])
|
||||||
|
def mark_all_seen(self, request: Request) -> Response:
|
||||||
|
"""Mark all the user's notifications as seen"""
|
||||||
|
notifications = Notification.objects.filter(user=request.user)
|
||||||
|
for notification in notifications:
|
||||||
|
notification.seen = True
|
||||||
|
Notification.objects.bulk_update(notifications, ["seen"])
|
||||||
|
return Response({}, status=204)
|
||||||
|
28
authentik/events/api/notification_mapping.py
Normal file
28
authentik/events/api/notification_mapping.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""NotificationWebhookMapping API Views"""
|
||||||
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
from authentik.events.models import NotificationWebhookMapping
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationWebhookMappingSerializer(ModelSerializer):
|
||||||
|
"""NotificationWebhookMapping Serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = NotificationWebhookMapping
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"name",
|
||||||
|
"expression",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationWebhookMappingViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
"""NotificationWebhookMapping Viewset"""
|
||||||
|
|
||||||
|
queryset = NotificationWebhookMapping.objects.all()
|
||||||
|
serializer_class = NotificationWebhookMappingSerializer
|
||||||
|
filterset_fields = ["name"]
|
||||||
|
ordering = ["name"]
|
@ -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
|
||||||
@ -38,6 +49,7 @@ class NotificationTransportSerializer(ModelSerializer):
|
|||||||
"mode",
|
"mode",
|
||||||
"mode_verbose",
|
"mode_verbose",
|
||||||
"webhook_url",
|
"webhook_url",
|
||||||
|
"webhook_mapping",
|
||||||
"send_once",
|
"send_once",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
"""authentik events app"""
|
"""authentik events app"""
|
||||||
from datetime import timedelta
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db import ProgrammingError
|
|
||||||
from django.utils.timezone import now
|
|
||||||
|
|
||||||
|
|
||||||
class AuthentikEventsConfig(AppConfig):
|
class AuthentikEventsConfig(AppConfig):
|
||||||
@ -16,12 +13,3 @@ class AuthentikEventsConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import_module("authentik.events.signals")
|
import_module("authentik.events.signals")
|
||||||
try:
|
|
||||||
from authentik.events.models import Event
|
|
||||||
|
|
||||||
date_from = now() - timedelta(days=1)
|
|
||||||
|
|
||||||
for event in Event.objects.filter(created__gte=date_from):
|
|
||||||
event._set_prom_metrics()
|
|
||||||
except ProgrammingError:
|
|
||||||
pass
|
|
||||||
|
@ -0,0 +1,831 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-10 16:01
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
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
|
||||||
|
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=[django.core.validators.URLValidator()]),
|
||||||
|
),
|
||||||
|
]
|
46
authentik/events/migrations/0018_auto_20210911_2217.py
Normal file
46
authentik/events/migrations/0018_auto_20210911_2217.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 3.2.6 on 2021-09-11 22:17
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0028_alter_token_intent"),
|
||||||
|
("authentik_events", "0017_alter_event_action"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2021-10-04 15:31
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, 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=[django.core.validators.URLValidator()]),
|
||||||
|
),
|
||||||
|
]
|
@ -2,25 +2,26 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from inspect import getmodule, stack
|
from inspect import getmodule, stack
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
from typing import Optional, Union
|
from typing import TYPE_CHECKING, Optional, Type, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.validators import URLValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.http.request import QueryDict
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from prometheus_client import Gauge
|
from requests import RequestException
|
||||||
from requests import RequestException, post
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
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 ExpiringModel, Group, 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.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.lib.utils.http import get_client_ip
|
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
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
@ -28,11 +29,8 @@ from authentik.tenants.models import Tenant
|
|||||||
from authentik.tenants.utils import DEFAULT_TENANT
|
from authentik.tenants.utils import DEFAULT_TENANT
|
||||||
|
|
||||||
LOGGER = get_logger("authentik.events")
|
LOGGER = get_logger("authentik.events")
|
||||||
GAUGE_EVENTS = Gauge(
|
if TYPE_CHECKING:
|
||||||
"authentik_events",
|
from rest_framework.serializers import Serializer
|
||||||
"Events in authentik",
|
|
||||||
["action", "user_username", "app", "client_ip"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def default_event_duration():
|
def default_event_duration():
|
||||||
@ -143,8 +141,9 @@ class Event(ExpiringModel):
|
|||||||
`user` arguments optionally overrides user from requests."""
|
`user` arguments optionally overrides user from requests."""
|
||||||
if request:
|
if request:
|
||||||
self.context["http_request"] = {
|
self.context["http_request"] = {
|
||||||
"path": request.get_full_path(),
|
"path": request.path,
|
||||||
"method": request.method,
|
"method": request.method,
|
||||||
|
"args": QueryDict(request.META.get("QUERY_STRING", "")),
|
||||||
}
|
}
|
||||||
if hasattr(request, "tenant"):
|
if hasattr(request, "tenant"):
|
||||||
tenant: Tenant = request.tenant
|
tenant: Tenant = request.tenant
|
||||||
@ -182,14 +181,6 @@ class Event(ExpiringModel):
|
|||||||
return
|
return
|
||||||
self.context["geo"] = city
|
self.context["geo"] = city
|
||||||
|
|
||||||
def _set_prom_metrics(self):
|
|
||||||
GAUGE_EVENTS.labels(
|
|
||||||
action=self.action,
|
|
||||||
user_username=self.user.get("username"),
|
|
||||||
app=self.app,
|
|
||||||
client_ip=self.client_ip,
|
|
||||||
).set(self.created.timestamp())
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self._state.adding:
|
if self._state.adding:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
@ -200,7 +191,6 @@ class Event(ExpiringModel):
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
)
|
)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
self._set_prom_metrics()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def summary(self) -> str:
|
def summary(self) -> str:
|
||||||
@ -234,7 +224,10 @@ 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=[URLValidator()])
|
||||||
|
webhook_mapping = models.ForeignKey(
|
||||||
|
"NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
|
||||||
|
)
|
||||||
send_once = models.BooleanField(
|
send_once = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
@ -254,15 +247,22 @@ class NotificationTransport(models.Model):
|
|||||||
|
|
||||||
def send_webhook(self, notification: "Notification") -> list[str]:
|
def send_webhook(self, notification: "Notification") -> list[str]:
|
||||||
"""Send notification to generic webhook"""
|
"""Send notification to generic webhook"""
|
||||||
|
default_body = {
|
||||||
|
"body": notification.body,
|
||||||
|
"severity": notification.severity,
|
||||||
|
"user_email": notification.user.email,
|
||||||
|
"user_username": notification.user.username,
|
||||||
|
}
|
||||||
|
if self.webhook_mapping:
|
||||||
|
default_body = self.webhook_mapping.evaluate(
|
||||||
|
user=notification.user,
|
||||||
|
request=None,
|
||||||
|
notification=notification,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
response = post(
|
response = get_http_session().post(
|
||||||
self.webhook_url,
|
self.webhook_url,
|
||||||
json={
|
json=default_body,
|
||||||
"body": notification.body,
|
|
||||||
"severity": notification.severity,
|
|
||||||
"user_email": notification.user.email,
|
|
||||||
"user_username": notification.user.username,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except RequestException as exc:
|
except RequestException as exc:
|
||||||
@ -312,7 +312,7 @@ class NotificationTransport(models.Model):
|
|||||||
if notification.event:
|
if notification.event:
|
||||||
body["attachments"][0]["title"] = notification.event.action
|
body["attachments"][0]["title"] = notification.event.action
|
||||||
try:
|
try:
|
||||||
response = post(self.webhook_url, json=body)
|
response = get_http_session().post(self.webhook_url, json=body)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except RequestException as exc:
|
except RequestException as exc:
|
||||||
text = exc.response.text if exc.response else str(exc)
|
text = exc.response.text if exc.response else str(exc)
|
||||||
@ -429,3 +429,25 @@ class NotificationRule(PolicyBindingModel):
|
|||||||
|
|
||||||
verbose_name = _("Notification Rule")
|
verbose_name = _("Notification Rule")
|
||||||
verbose_name_plural = _("Notification Rules")
|
verbose_name_plural = _("Notification Rules")
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationWebhookMapping(PropertyMapping):
|
||||||
|
"""Modify the schema and layout of the webhook being sent"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def component(self) -> str:
|
||||||
|
return "ak-property-mapping-notification-form"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serializer(self) -> Type["Serializer"]:
|
||||||
|
from authentik.events.api.notification_mapping import NotificationWebhookMappingSerializer
|
||||||
|
|
||||||
|
return NotificationWebhookMappingSerializer
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Notification Webhook Mapping {self.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
verbose_name = _("Notification Webhook Mapping")
|
||||||
|
verbose_name_plural = _("Notification Webhook Mappings")
|
||||||
|
@ -3,14 +3,16 @@ from dataclasses import dataclass, field
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from timeit import default_timer
|
from timeit import default_timer
|
||||||
from traceback import format_tb
|
|
||||||
from typing import Any, Optional
|
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
|
||||||
|
|
||||||
GAUGE_TASKS = Gauge(
|
GAUGE_TASKS = Gauge(
|
||||||
"authentik_system_tasks",
|
"authentik_system_tasks",
|
||||||
@ -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
|
||||||
@ -41,8 +46,7 @@ class TaskResult:
|
|||||||
|
|
||||||
def with_error(self, exc: Exception) -> "TaskResult":
|
def with_error(self, exc: Exception) -> "TaskResult":
|
||||||
"""Since errors might not always be pickle-able, set the traceback"""
|
"""Since errors might not always be pickle-able, set the traceback"""
|
||||||
self.messages.extend(format_tb(exc.__traceback__))
|
self.messages.extend(exception_to_string(exc).splitlines())
|
||||||
self.messages.append(str(exc))
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
@ -77,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"""
|
||||||
@ -108,6 +112,30 @@ class TaskInfo:
|
|||||||
cache.set(key, self, timeout=timeout_hours * 60 * 60)
|
cache.set(key, self, timeout=timeout_hours * 60 * 60)
|
||||||
|
|
||||||
|
|
||||||
|
def prefill_task():
|
||||||
|
"""Ensure a task's details are always in cache, so it can always be triggered via API"""
|
||||||
|
|
||||||
|
def inner_wrap(func):
|
||||||
|
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
|
||||||
|
|
||||||
|
return inner_wrap
|
||||||
|
|
||||||
|
|
||||||
class MonitoredTask(Task):
|
class MonitoredTask(Task):
|
||||||
"""Task which can save its state to the cache"""
|
"""Task which can save its state to the cache"""
|
||||||
|
|
||||||
@ -174,9 +202,7 @@ class MonitoredTask(Task):
|
|||||||
).save(self.result_timeout_hours)
|
).save(self.result_timeout_hours)
|
||||||
Event.new(
|
Event.new(
|
||||||
EventAction.SYSTEM_TASK_EXCEPTION,
|
EventAction.SYSTEM_TASK_EXCEPTION,
|
||||||
message=(
|
message=(f"Task {self.__name__} encountered an error: {exception_to_string(exc)}"),
|
||||||
f"Task {self.__name__} encountered an error: " "\n".join(self._result.messages)
|
|
||||||
),
|
|
||||||
).save()
|
).save()
|
||||||
return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
|
return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
|
||||||
|
|
||||||
|
@ -12,9 +12,10 @@ 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
|
||||||
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.user_write.signals import user_write
|
from authentik.stages.user_write.signals import user_write
|
||||||
|
|
||||||
|
|
||||||
@ -46,7 +47,13 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
|||||||
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
||||||
if PLAN_CONTEXT_SOURCE in flow_plan.context:
|
if PLAN_CONTEXT_SOURCE in flow_plan.context:
|
||||||
# Login request came from an external source, save it in the context
|
# Login request came from an external source, save it in the context
|
||||||
thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE]
|
thread.kwargs[PLAN_CONTEXT_SOURCE] = flow_plan.context[PLAN_CONTEXT_SOURCE]
|
||||||
|
if PLAN_CONTEXT_METHOD in flow_plan.context:
|
||||||
|
thread.kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD]
|
||||||
|
# Save the login method used
|
||||||
|
thread.kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(
|
||||||
|
PLAN_CONTEXT_METHOD_ARGS, {}
|
||||||
|
)
|
||||||
thread.user = user
|
thread.user = user
|
||||||
thread.run()
|
thread.run()
|
||||||
|
|
||||||
|
@ -98,7 +98,9 @@ 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:
|
||||||
|
@ -4,15 +4,21 @@ 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.models import User
|
||||||
from authentik.events.models import Event, EventAction
|
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:
|
||||||
user = User.objects.get(username="akadmin")
|
self.user = User.objects.get(username="akadmin")
|
||||||
self.client.force_login(user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_top_n(self):
|
def test_top_n(self):
|
||||||
"""Test top_per_user"""
|
"""Test top_per_user"""
|
||||||
@ -30,3 +36,34 @@ class TestEventsAPI(APITestCase):
|
|||||||
reverse("authentik_api:event-actions"),
|
reverse("authentik_api:event-actions"),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_notifications(self):
|
||||||
|
"""Test notifications"""
|
||||||
|
notification = Notification.objects.create(
|
||||||
|
user=self.user, severity=NotificationSeverity.ALERT, body="", seen=False
|
||||||
|
)
|
||||||
|
self.client.post(
|
||||||
|
reverse("authentik_api:notification-mark-all-seen"),
|
||||||
|
)
|
||||||
|
notification.refresh_from_db()
|
||||||
|
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)
|
||||||
|
@ -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):
|
||||||
|
@ -7,10 +7,10 @@ from django.http.response import HttpResponseBadRequest, JsonResponse
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import BooleanField, FileField, ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -20,14 +20,19 @@ from structlog.stdlib import get_logger
|
|||||||
|
|
||||||
from authentik.api.decorators import permission_required
|
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 CacheSerializer, LinkSerializer
|
from authentik.core.api.utils import (
|
||||||
|
CacheSerializer,
|
||||||
|
FilePathSerializer,
|
||||||
|
FileUploadSerializer,
|
||||||
|
LinkSerializer,
|
||||||
|
)
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||||
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()
|
||||||
@ -103,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"]
|
||||||
|
|
||||||
@ -147,7 +153,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request={"multipart/form-data": inline_serializer("SetIcon", fields={"file": FileField()})},
|
request={"multipart/form-data": FileUploadSerializer},
|
||||||
responses={
|
responses={
|
||||||
204: OpenApiResponse(description="Successfully imported flow"),
|
204: OpenApiResponse(description="Successfully imported flow"),
|
||||||
400: OpenApiResponse(description="Bad request"),
|
400: OpenApiResponse(description="Bad request"),
|
||||||
@ -259,13 +265,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
@permission_required("authentik_flows.change_flow")
|
@permission_required("authentik_flows.change_flow")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request={
|
request={
|
||||||
"multipart/form-data": inline_serializer(
|
"multipart/form-data": FileUploadSerializer,
|
||||||
"SetIcon",
|
|
||||||
fields={
|
|
||||||
"file": FileField(required=False),
|
|
||||||
"clear": BooleanField(default=False),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
responses={
|
responses={
|
||||||
200: OpenApiResponse(description="Success"),
|
200: OpenApiResponse(description="Success"),
|
||||||
@ -301,7 +301,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
|
|
||||||
@permission_required("authentik_core.change_application")
|
@permission_required("authentik_core.change_application")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=inline_serializer("SetIconURL", fields={"url": CharField()}),
|
request=FilePathSerializer,
|
||||||
responses={
|
responses={
|
||||||
200: OpenApiResponse(description="Success"),
|
200: OpenApiResponse(description="Success"),
|
||||||
400: OpenApiResponse(description="Bad request"),
|
400: OpenApiResponse(description="Bad request"),
|
||||||
@ -334,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()
|
||||||
@ -86,9 +84,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()
|
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
|
||||||
|
@ -11,7 +11,7 @@ class Command(BaseCommand): # pragma: no cover
|
|||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
"""Apply all flows in order, abort when one fails to import"""
|
"""Apply all flows in order, abort when one fails to import"""
|
||||||
for flow_path in options.get("flows", []):
|
for flow_path in options.get("flows", []):
|
||||||
with open(flow_path, "r") as flow_file:
|
with open(flow_path, "r", encoding="utf8") as flow_file:
|
||||||
importer = FlowImporter(flow_file.read())
|
importer = FlowImporter(flow_file.read())
|
||||||
valid = importer.validate()
|
valid = importer.validate()
|
||||||
if not valid:
|
if not valid:
|
||||||
|
@ -31,6 +31,7 @@ class FlowPlanProcess(PROCESS_CLASS): # pragma: no cover
|
|||||||
self.request = RequestFactory().get("/")
|
self.request = RequestFactory().get("/")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
"""Execute 1000 flow plans"""
|
||||||
print(f"Proc {self.index} Running")
|
print(f"Proc {self.index} Running")
|
||||||
|
|
||||||
def test_inner():
|
def test_inner():
|
||||||
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -6,7 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|||||||
|
|
||||||
from authentik.flows.models import FlowDesignation
|
from authentik.flows.models import FlowDesignation
|
||||||
from authentik.stages.identification.models import UserFields
|
from authentik.stages.identification.models import UserFields
|
||||||
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
|
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||||
|
|
||||||
|
|
||||||
def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
@ -26,7 +26,7 @@ def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSc
|
|||||||
|
|
||||||
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
|
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
|
||||||
name="default-authentication-password",
|
name="default-authentication-password",
|
||||||
defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP]},
|
defaults={"backends": [BACKEND_INBUILT, BACKEND_LDAP, BACKEND_APP_PASSWORD]},
|
||||||
)
|
)
|
||||||
|
|
||||||
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
||||||
|
@ -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.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 3.2.6 on 2021-08-30 14:49
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_flows", "0023_alter_flow_background"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="compatibility_mode",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Enable compatibility mode, increases compatibility with password managers on mobile devices.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -125,7 +125,7 @@ class Flow(SerializerModel, PolicyBindingModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
compatibility_mode = models.BooleanField(
|
compatibility_mode = models.BooleanField(
|
||||||
default=True,
|
default=False,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Enable compatibility mode, increases compatibility with "
|
"Enable compatibility mode, increases compatibility with "
|
||||||
"password managers on mobile devices."
|
"password managers on mobile devices."
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user