Compare commits
2082 Commits
version/20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
9201fc1834 | |||
5385feb428 | |||
db557401aa | |||
c824af5bc3 | |||
1faba11a57 | |||
f0c72e8536 | |||
91f91b08e5 | |||
8faa909c32 | |||
49142fa80b | |||
2a6fccd22a | |||
1d10afa209 | |||
4b7c3c38cd | |||
440cacbafe | |||
b33bff92ee | |||
caed306346 | |||
d0eb6af7e9 | |||
ec5ed67f6c | |||
59b899ddff | |||
85784f796c | |||
4c0e19cbea | |||
b42eb9464f | |||
6559fdee15 | |||
3455bf3d27 | |||
ff2baf502b | |||
3b182ca223 | |||
8da8890a8e | |||
23023ec727 | |||
7d84a71a01 | |||
192001f193 | |||
63734682d2 | |||
a0cd2d55f8 | |||
a72c7adfc0 | |||
e88e02ec85 | |||
f7661c8bbd | |||
9add8479ca | |||
4c39e08dd4 | |||
44ce2ebece | |||
f5a8859d00 | |||
9ef0e8bc5f | |||
60eeafd111 | |||
6f3d6efa22 | |||
8d3275817b | |||
ca40d31dac | |||
438aac8879 | |||
2dfa6c2c82 | |||
c11435780d | |||
ee54328589 | |||
817d538b8f | |||
210775776f | |||
2a4ce75bc4 | |||
b26111fb42 | |||
e30103aa9f | |||
dc9203789e | |||
d70ce2776f | |||
ad7d65e903 | |||
67d54c5209 | |||
bb244b8338 | |||
fa04883ac1 | |||
6739ded5a9 | |||
9a7e5d934e | |||
6dc6d19d2d | |||
36cbc44ed6 | |||
0c591a50e3 | |||
7ee655a318 | |||
8447e9b9c2 | |||
09f92e5bad | |||
f9a419107a | |||
8f0572d11e | |||
7ebf793953 | |||
63783ee77b | |||
eba339ba27 | |||
0adb5a79f6 | |||
fa81adf254 | |||
558c7bba2a | |||
8cd1a42fb9 | |||
8cf0e78aa0 | |||
3f69a57013 | |||
f7f12cab10 | |||
cacaa378c8 | |||
33fe85eb96 | |||
a9744cbf48 | |||
b91d8a676c | |||
f19cd1c003 | |||
65341cecd0 | |||
c0cb891078 | |||
fc1c1a849a | |||
5a81ae956f | |||
0cac034512 | |||
5666995a15 | |||
8d3059e4f3 | |||
a90dc34494 | |||
2c6d82593e | |||
34bcc2df1a | |||
c00f2907ea | |||
b4d528a789 | |||
d9172cb296 | |||
bee36cde59 | |||
d4e7d9d64a | |||
7b0265207a | |||
7c076579fd | |||
7171706d7f | |||
9cd46ecbeb | |||
5f09ba675d | |||
630b926e2a | |||
9c6be60ad9 | |||
a0397fdcf4 | |||
59e13e8026 | |||
374b51e956 | |||
8faa1bf865 | |||
fc75867218 | |||
6d94c2c925 | |||
eb51dd1379 | |||
13a4559c37 | |||
4fcf7285d7 | |||
0ba9f25155 | |||
453c751c7f | |||
d1eaaef254 | |||
3eb466ff4b | |||
9f2529c886 | |||
fb25b28976 | |||
612163b82f | |||
3c43690a96 | |||
dd74565c7b | |||
fb69f67f47 | |||
18b48684eb | |||
098b0aef6e | |||
4ed8171130 | |||
335131affc | |||
bba17a8a67 | |||
082df0ec51 | |||
1883402b3d | |||
88a8b7d2fa | |||
987f03c4be | |||
1b3aacfa1d | |||
a03dde8a90 | |||
5f04a187ea | |||
2b68363452 | |||
3a994ab2a4 | |||
d7713357f4 | |||
e7c03fdb14 | |||
6105956847 | |||
89028f175a | |||
f121098957 | |||
4ff32af343 | |||
972868c15c | |||
0bc57f571b | |||
9de5b6f93e | |||
acf1ded1d4 | |||
a286f999e2 | |||
4b6c1da51d | |||
a81d5a3d41 | |||
4d17111233 | |||
64cb9812e0 | |||
ed037b2e3a | |||
d2be6a8e3a | |||
a9667eb0f4 | |||
7f3988f3c9 | |||
4c095a6f2a | |||
c10b5c3c8c | |||
9d920580a1 | |||
34ef4af799 | |||
5da47b69dd | |||
0e0dd2437b | |||
e42386b150 | |||
f21f81022e | |||
e73a468921 | |||
c0ac053380 | |||
4e670295d1 | |||
8d7d8d613c | |||
4d632a8679 | |||
ef219198d4 | |||
cc744dc581 | |||
47006fc9d2 | |||
ada53362d5 | |||
a03e48c5ce | |||
816b0c7d83 | |||
a6398f46da | |||
56babb2649 | |||
0edf4296c4 | |||
b8fdda50ec | |||
d25a051eae | |||
4a9b788703 | |||
d4ef321ac2 | |||
80c1dbdfbb | |||
b0af062d74 | |||
b4e75218f5 | |||
ab1840dd66 | |||
482491e93c | |||
2ca991ba3d | |||
b20c384f5a | |||
9ce8edbcd6 | |||
cb5b2148a3 | |||
d5702c6282 | |||
61a876b582 | |||
8c9748e4a0 | |||
6460245d5e | |||
b7979ad48e | |||
cbd95848e7 | |||
4704de937a | |||
394d8e99a4 | |||
a26f25ccd6 | |||
94257e0f50 | |||
b2a42a68a4 | |||
7895d59da3 | |||
b54c60d7af | |||
6bab3bf68e | |||
fdc09c658a | |||
a690a02f99 | |||
0e912fd647 | |||
27af330932 | |||
7187d28905 | |||
ca832b6090 | |||
53bd6bf06e | |||
813f271bdd | |||
63dc8fe7dc | |||
383f4e4dcf | |||
2896652fef | |||
cfe2648b62 | |||
8d49705c87 | |||
c99e6d8f2c | |||
0996bb500c | |||
3d4a45c93f | |||
0642af0b78 | |||
dce623dd7c | |||
646d174dd2 | |||
b8fdb82adc | |||
75d6cd1674 | |||
5c91658484 | |||
ebb44c992b | |||
233bb35ebe | |||
f60d0b9753 | |||
7e95c756b9 | |||
be26b92927 | |||
dd3ed1bfb9 | |||
6f56a61a64 | |||
2dee8034d3 | |||
d9d42020cc | |||
90298a2b6c | |||
7c17e7d52f | |||
fbb3ca98c1 | |||
220d21c3e0 | |||
84e74bc21e | |||
ec15060c84 | |||
334898ae23 | |||
b43df2ae27 | |||
a52638d898 | |||
5bc893b890 | |||
fe5d9e4cd2 | |||
a7442e0043 | |||
8103bbf9af | |||
056b90b590 | |||
70221e3d14 | |||
d570feffac | |||
3d52266773 | |||
7bdecd2ee6 | |||
a500ff28ac | |||
263bcae050 | |||
8691a79204 | |||
3b0b6dcf29 | |||
11f7935155 | |||
450a26d1b5 | |||
3e42c1bad4 | |||
5abbb7657b | |||
75b0fb3393 | |||
538c2ca4d3 | |||
5080840ed9 | |||
eded9bfb2d | |||
b3a43ae37c | |||
dc78746825 | |||
3c6828cbba | |||
26646264dc | |||
f7ecfdd4b6 | |||
967c80069b | |||
f8b0c071b7 | |||
221ab47410 | |||
ffe162214f | |||
ad9d8d26ed | |||
35402ada17 | |||
086a44bdbd | |||
6494a0352f | |||
ca1fb737a8 | |||
9e91a0a85d | |||
4e68fe2fea | |||
a36eab81eb | |||
215b2a3224 | |||
4c3f8e446f | |||
4b9922e5b1 | |||
6324521424 | |||
d6b18f2833 | |||
333e58ce2f | |||
699d3ca067 | |||
296779ddf1 | |||
8669f498f1 | |||
4de2ac3248 | |||
eb4dce91c3 | |||
c64a99345b | |||
2e174a1be5 | |||
11ef500475 | |||
d4fd6153c8 | |||
85b6bfbe5f | |||
5ddd138c97 | |||
5644d5f3f7 | |||
be06adcb59 | |||
4da350ebfc | |||
f391c33bdf | |||
18f450bd49 | |||
ee36b7f3eb | |||
f56d619243 | |||
a9a62bbfc8 | |||
ddd785898b | |||
8ba45a5f6a | |||
7d41e6227b | |||
1363226697 | |||
25910bb577 | |||
62e54a3a51 | |||
5f5b4c962b | |||
4a9a19eacb | |||
d4abf5621e | |||
1cb71b5217 | |||
a884f23855 | |||
421b003218 | |||
25a4310bb1 | |||
e897307548 | |||
0fd959c5c0 | |||
ce7d18798f | |||
be3b034cb8 | |||
9f674442d3 | |||
c21793943d | |||
ec67b60219 | |||
2fe553785e | |||
fd1d38f844 | |||
4d755dc0f6 | |||
30c65f9e61 | |||
3554406aa5 | |||
5eeaac1ad9 | |||
5a172abdb9 | |||
8f861d8ecb | |||
f9fdcd2d07 | |||
ed58f21a21 | |||
45af8eb4be | |||
88573105a0 | |||
f9469e3f99 | |||
26d92d9259 | |||
9cb0d37d51 | |||
5a25e1524a | |||
9e1a518689 | |||
cf5771dad3 | |||
db5aafed36 | |||
4b0324220a | |||
0183d2c880 | |||
c1fe18a261 | |||
ab2299ba1e | |||
2678b381b9 | |||
d3ef7920cb | |||
860269acf0 | |||
d2bd177b8f | |||
32cc03832a | |||
948d2cbdca | |||
22026f0755 | |||
a7a7b5aacb | |||
03d5b9e7e9 | |||
30c7e6c94c | |||
1ba96586f7 | |||
607f632515 | |||
58b46fbfcd | |||
9b53e26ab0 | |||
832d3175aa | |||
ebea8369d6 | |||
a8508aac99 | |||
59df02b3b8 | |||
f00657f217 | |||
110bc762a1 | |||
f35e5f79aa | |||
3f32109706 | |||
0f042f2e4a | |||
34d1eb140b | |||
62f67aabe3 | |||
82c3eaa0f9 | |||
31ede2ae1d | |||
54c672256f | |||
5f47d46b6f | |||
3f23bc0b85 | |||
366142382b | |||
ddbe0aaf13 | |||
75320bf579 | |||
15d8988569 | |||
84930b4924 | |||
1ede972222 | |||
cd1d1b4402 | |||
79caba45cc | |||
c101357051 | |||
9bebb82bbf | |||
d95d2ca7fe | |||
c0a883f76f | |||
eb6cfd22a7 | |||
254249e38b | |||
da28bb7d3c | |||
391c1ff911 | |||
1d475d0982 | |||
f92fa61101 | |||
ccca397a77 | |||
162fd26f32 | |||
1d7a235766 | |||
01a8deb77f | |||
cba770a551 | |||
c67afc4084 | |||
4ed30fa61e | |||
db16a0ffbe | |||
99ec355710 | |||
9e1882cebd | |||
80912cace0 | |||
0882894dc3 | |||
c1582147d7 | |||
ab8b37a899 | |||
9077eff34d | |||
2399fa456b | |||
c8c69a9a56 | |||
1258f3bba2 | |||
5488120e84 | |||
0b4ac54363 | |||
1a1434bfda | |||
1328c3e62c | |||
1800b62cd6 | |||
32fa4c9fcb | |||
15f0045a00 | |||
ac2211d9da | |||
cbd5b0dbfd | |||
8e4896d261 | |||
9481df619a | |||
d283a5236c | |||
6add88654e | |||
e4486b98fc | |||
778065f468 | |||
70794d79dd | |||
6e5ac4bffc | |||
4bab42fb58 | |||
c97823fe49 | |||
a3bb5d89cc | |||
f4f9f525d7 | |||
555525ea9d | |||
e455e20312 | |||
4c14e88a25 | |||
7561ea15de | |||
8242b09394 | |||
6f0fa731c0 | |||
576bb013ed | |||
aefedfb836 | |||
4295ddb671 | |||
9b9c0fe663 | |||
5a58f6ee64 | |||
da83c3af53 | |||
e84b17d550 | |||
b4fb0190a3 | |||
bb52b95e5b | |||
a2b5d667af | |||
2df9c0479d | |||
5c673dc7bb | |||
da2dd7daf4 | |||
f2a80030d7 | |||
918183f472 | |||
9da439623b | |||
957bb1c5ef | |||
677d46d7fd | |||
5af7baf36c | |||
8b2ca822f5 | |||
2303a97bb9 | |||
8be04cc013 | |||
9b6e47e6b8 | |||
677621989a | |||
0d5125db76 | |||
ed88f6594c | |||
b1816f2101 | |||
fe60c26e11 | |||
cca33a74b6 | |||
f977bf61eb | |||
f8f8a9bbb9 | |||
7a44d5768a | |||
d9e4219d70 | |||
6db5df1b31 | |||
e64ca4ab04 | |||
0e59ed62f5 | |||
dfe3394d4e | |||
9d4fb8048c | |||
a7a517733e | |||
e2f0a76309 | |||
07267ac425 | |||
8fb7620004 | |||
2ef85c4447 | |||
c3174ac044 | |||
952b48541c | |||
a97ffce5f9 | |||
5d514bd8c4 | |||
128234324d | |||
2d1bc2efcc | |||
2a1af96838 | |||
a6674440e6 | |||
5861d41ad3 | |||
fcd9c58a73 | |||
4bf2878cf7 | |||
79d508a020 | |||
03916b0b25 | |||
263964865c | |||
21f92b4a65 | |||
e38d03b304 | |||
f2b540ed8a | |||
79ad356d90 | |||
e70490481d | |||
66ab9504e9 | |||
009173fe23 | |||
75a5335f0f | |||
7a9452c66a | |||
82a999f95d | |||
0c2e9234bf | |||
964a3276a1 | |||
5185b027dc | |||
d690296120 | |||
9252a1f9d3 | |||
fc6742a17e | |||
31546da796 | |||
4a6c46a5c9 | |||
20262f3f4b | |||
dea61ef35e | |||
edda644e28 | |||
ee13ec1dca | |||
39bea1d5d0 | |||
453dcd790f | |||
bb70e6c81d | |||
4ff9db9d7e | |||
8b2e70d15d | |||
8e2f929933 | |||
ae2d86096b | |||
849c347e8c | |||
c974298836 | |||
b46eb7198b | |||
37db6764ab | |||
633296503d | |||
508cec2fd5 | |||
7a93614e4b | |||
4f319eaa4f | |||
86a8d00b3f | |||
5fe8c1f3d7 | |||
be91d893fb | |||
1fc6aa5a02 | |||
2256baced5 | |||
f2af904aeb | |||
030f612c38 | |||
d84ff2bbca | |||
4be238018b | |||
71c6313c46 | |||
f7daa7723d | |||
1ff35eef4c | |||
743bb3e98f | |||
83c4d5393c | |||
99008252f8 | |||
4cf00ed5cf | |||
8689444954 | |||
4210f692ff | |||
85a3578092 | |||
6b05d44d1f | |||
49b221ed68 | |||
67b43c223c | |||
5f9dc4395a | |||
bb8af2f19b | |||
996bd05ba6 | |||
ac03f5a97d | |||
a1a64e25ee | |||
53851efacb | |||
afea262e14 | |||
53f92f01da | |||
a267686098 | |||
9ee06b7d1f | |||
f53343141e | |||
62250f4ec6 | |||
485329130b | |||
6891c239e2 | |||
993c6472db | |||
123b0b2f05 | |||
487b1e4f34 | |||
b308cfa8d7 | |||
839884c65c | |||
dc93f5d4c9 | |||
735af9aaad | |||
9c52ee585f | |||
4c5f01020e | |||
fc315eb8da | |||
b90d8b14d6 | |||
1af49c930c | |||
624ae67b50 | |||
cd2fb49f9b | |||
3da531ede3 | |||
e3e4b2f818 | |||
98391da0d0 | |||
1555aed02f | |||
7a01529511 | |||
bc3e6b3962 | |||
7cbd5174f0 | |||
788cd401f6 | |||
bec8c8fe0a | |||
3184a64482 | |||
c7a83e6182 | |||
933919c647 | |||
7d3841e85f | |||
21e54d803f | |||
883af97148 | |||
3184019996 | |||
c0edaaf821 | |||
74ff9d04dd | |||
969902f503 | |||
04372e21dd | |||
0c53650216 | |||
8e028c2feb | |||
d75a864f0e | |||
81f3b133f6 | |||
b887916f5b | |||
2a354aa64f | |||
d9724e6885 | |||
d092e8e4bc | |||
e5b8975459 | |||
4f4784f4d8 | |||
51194cbf42 | |||
4d5a619cc0 | |||
2314340823 | |||
7c6b2c843b | |||
0c2b32da31 | |||
9ad4c736f1 | |||
0c0b9ca84a | |||
4154b62565 | |||
5a07d4ec66 | |||
64b758c8fa | |||
a0e29d42a6 | |||
0bbea79c64 | |||
467ad29656 | |||
d2fc1226f8 | |||
5c50a18b6f | |||
75505a2077 | |||
6d7525b5a1 | |||
4ca7ba427a | |||
740fafa86d | |||
4b80f52e11 | |||
7ae2bdc35f | |||
34473903dd | |||
86a4a7dcee | |||
73fe866cb6 | |||
8b95e9f97a | |||
a3eb72d160 | |||
b418db6ecf | |||
6cb1ab1d2b | |||
ae09dac720 | |||
44c9ad19a7 | |||
554272a927 | |||
acf2af8f66 | |||
b45a442447 | |||
75a720ead1 | |||
615ce287ce | |||
aa8d97249a | |||
2390df17f1 | |||
c022052539 | |||
13c050e2a6 | |||
ef371b3750 | |||
bb1f79347b | |||
6ed0d6d124 | |||
4ed60fe36b | |||
ca9fa79095 | |||
a2408cefcf | |||
145eaa5de3 | |||
1991c930f2 | |||
736f84b670 | |||
d4d5c2675b | |||
be232e2b77 | |||
42389188ad | |||
1f6af8c221 | |||
f4955e3e62 | |||
a8ef3096c1 | |||
14f76b2575 | |||
50065d37b9 | |||
a54670fb91 | |||
51fda51cbf | |||
53d0205e86 | |||
0f56d00959 | |||
b7a6fccdf9 | |||
522f49f48c | |||
e685f11514 | |||
1841b9b4c6 | |||
40e37a5c2c | |||
ac838645a9 | |||
be40d67c4d | |||
700cc06f45 | |||
260a7aac63 | |||
37df054f4c | |||
a3df414f24 | |||
dcaa8d6322 | |||
e03dd70f2f | |||
ceb894039e | |||
a77616e942 | |||
47601a767b | |||
c7a825c393 | |||
181c55aef1 | |||
631b1fcc29 | |||
54f170650a | |||
3bdb551e74 | |||
96b2631ec4 | |||
4fffa6d2cc | |||
e46c70e13d | |||
7d4e7f84f4 | |||
d49640ca9b | |||
ed2cf44471 | |||
5b1d15276a | |||
d9275a3350 | |||
2e81dddc1d | |||
abc73deda0 | |||
becec6b7d8 | |||
ab516f782b | |||
d7b3c545aa | |||
81550d9d1d | |||
72e5768c2f | |||
11cf5fc472 | |||
fedb81571d | |||
37528e1bba | |||
97ef2a6f5f | |||
cc1509cf57 | |||
0dfecc6ae2 | |||
c1e4d78672 | |||
0ab427b5bb | |||
a9f095d1d9 | |||
de17207c68 | |||
d9675695fe | |||
ec7f372fa9 | |||
8a675152e6 | |||
228fe01f92 | |||
b9547ece49 | |||
6e9bc143bd | |||
8cd4bf1be8 | |||
76660e4666 | |||
73b2e2cb82 | |||
d741d6dcf1 | |||
2575fa6db7 | |||
7512c57a2e | |||
e6e2dfd757 | |||
920d1f1b0e | |||
680d4fc20d | |||
4d3b25ea66 | |||
5106c0d0c1 | |||
fd09ade054 | |||
01629fe9e3 | |||
5be97e98e4 | |||
b1fd801ceb | |||
62a939b91d | |||
257ac04be4 | |||
ec5e6c14a2 | |||
1e1d9f1bdd | |||
da1ea51dad | |||
6ee3b8d644 | |||
6155c69b7c | |||
136d40d919 | |||
bb1bb9e22a | |||
05e84b63a2 | |||
7ab55f7afa | |||
f5ec5245c5 | |||
4f4f954693 | |||
c57fbcfd89 | |||
025fc3fe96 | |||
4d079522c4 | |||
08acc7ba41 | |||
7bdd32506e | |||
6283fedcd9 | |||
7a0badc81b | |||
1e134aa446 | |||
27bc5489c5 | |||
2dca45917c | |||
66a4338b48 | |||
a4dfc7e068 | |||
f98a9bed9f | |||
5d1bf4a0af | |||
34635ab928 | |||
fabe1130c1 | |||
8feda9c2b1 | |||
074928cac1 | |||
2308f90270 | |||
13adca0763 | |||
50ded723d1 | |||
e9064509fe | |||
6fdf3ad3e5 | |||
fb60cefb72 | |||
61f7db314a | |||
ef7952cab3 | |||
7e5d8624c8 | |||
2c54be85be | |||
2f8dbe9b97 | |||
cebe44403c | |||
7261017e13 | |||
0b3d33f428 | |||
6f0cbd5fa6 | |||
fb94aefd2f | |||
c4c8390eff | |||
8c2e4478fd | |||
94029ee612 | |||
8db49f9eca | |||
7bd25d90f4 | |||
133528ee90 | |||
578bd8fcb3 | |||
4c2ef95253 | |||
702a59222d | |||
48e2121a75 | |||
61249786ff | |||
008af4ccce | |||
02e3010efe | |||
aca4795e0c | |||
ff0febfecd | |||
4daad4b514 | |||
677bcaadd7 | |||
c6e9ecdd37 | |||
c9ecad6262 | |||
e545b3b401 | |||
fec96ea013 | |||
1ac1c50b67 | |||
d2f189c1d0 | |||
fb33906637 | |||
6d3a94f24f | |||
84f594e658 | |||
1486bd5ab2 | |||
2c00f4da2d | |||
c10a23220b | |||
f20243d545 | |||
903c6422ad | |||
f5ab955536 | |||
3a861f0497 | |||
744f250d05 | |||
83d435bd3b | |||
945cdfe212 | |||
fcc0963fab | |||
2ab4fcd757 | |||
bfe31b15ad | |||
49c4b43f32 | |||
19b1f3a8c1 | |||
80f218a6bf | |||
61aaa90226 | |||
7fdda5a387 | |||
94597fd2ad | |||
09808883f4 | |||
81ecb85a55 | |||
21bfaa3927 | |||
1c9c7be1c0 | |||
5a11dc567e | |||
4a1acd377b | |||
c5b84a91d1 | |||
e77ecda3b8 | |||
4e317c10c5 | |||
eb05a3ddb8 | |||
a22d6a0924 | |||
3f0d67779a | |||
0a937ae8e9 | |||
f8d94f3039 | |||
6bb261ac62 | |||
45f2c5bae7 | |||
5d8c1aa0b0 | |||
0101368369 | |||
4854f81592 | |||
4bed6e02e5 | |||
908f123d0e | |||
256dd24a1e | |||
d4284407f9 | |||
80da5dfc52 | |||
b6edf990e0 | |||
a66dcf9382 | |||
9095a840d5 | |||
72259f6479 | |||
0973c74b9d | |||
c7ed4f7ac1 | |||
3d577cf15e | |||
5474a32573 | |||
a5940b88e3 | |||
ff15716012 | |||
c040b13b29 | |||
4915e980c5 | |||
df362dd9ea | |||
d4e4f93cb4 | |||
3af0de6a00 | |||
4f24d61290 | |||
4c5c4dcf2c | |||
660b5cb6c6 | |||
6ff1ea73a9 | |||
3de224690a | |||
d4624b510a | |||
8856d762d0 | |||
5d1cbf14d1 | |||
6d5207f644 | |||
3b6497cd51 | |||
ff7320b0f8 | |||
e5a393c534 | |||
bb4be944dc | |||
21efee8f44 | |||
f61549a60f | |||
0a7bafd1b2 | |||
b3987c5fa0 | |||
0da043a9fe | |||
f336f204cb | |||
3bfcf18492 | |||
dfafe8b43d | |||
b5d43b15f8 | |||
2ccab75021 | |||
9070df6c26 | |||
a1c8ad55ad | |||
872c05c690 | |||
a9528dc1b5 | |||
0e59ade1f2 | |||
5ac49c695d | |||
3a30ecbe76 | |||
1f838bb2aa | |||
cc42830e23 | |||
593eb959ca | |||
5bb6785ad6 | |||
535c11a729 | |||
a0fa8d8524 | |||
c14025c579 | |||
8bc3db7c90 | |||
eaad564e23 | |||
511a94975b | |||
015810a2fd | |||
e70e6b84c2 | |||
d0b9c9a26f | |||
3e403fa348 | |||
48f4a971ef | |||
6314be14ad | |||
1a072c6c39 | |||
ef2eed0bdf | |||
91227b1e96 | |||
67d68629da | |||
e875db8f66 | |||
055a76393d | |||
0754821628 | |||
fca88d9896 | |||
dfe0404c51 | |||
fa61696b46 | |||
e5773738f4 | |||
cac8539d79 | |||
cf600f6f26 | |||
e194715c3e | |||
787f02d5dc | |||
a0ed01a610 | |||
02ba493759 | |||
a7fea5434d | |||
4fb783e953 | |||
affbf85699 | |||
0d92112a3f | |||
b1ad3ec9db | |||
c0601baca6 | |||
057c5c5e9a | |||
05429ab848 | |||
b66d51a699 | |||
f834bc0ff2 | |||
93fd883d7a | |||
7e080d4d68 | |||
3e3ca22d04 | |||
e741caa6b3 | |||
4343246a41 | |||
3f6f83b4b6 | |||
c63e1c9b87 | |||
f44cf06d22 | |||
3f609b8601 | |||
edd89b44a4 | |||
3e58748862 | |||
7088a6b0e6 | |||
6c880e0e62 | |||
cb1e70be7f | |||
6ba150f737 | |||
131769ea73 | |||
e68adbb30d | |||
f1eef09099 | |||
5ab3c7fa9f | |||
d0cec39a0f | |||
e15f53a39a | |||
25fb995663 | |||
eac658c64f | |||
15e2032493 | |||
c87f6cd9d9 | |||
e758995458 | |||
20c284a188 | |||
b0936ea8f3 | |||
bfc0f4a413 | |||
1a9a90cf6a | |||
00f1a6fa48 | |||
33754a06d2 | |||
69b838e1cf | |||
d5e04a2301 | |||
fbf251280f | |||
eaadf62f01 | |||
8c33e7a7c1 | |||
a7d9a80a28 | |||
2ea5dce8d3 | |||
14bf01efe4 | |||
67b24a60e4 | |||
e6775297cb | |||
4e4e2b36b6 | |||
3189c56fc3 | |||
5b5ea47b7a | |||
caa382f898 | |||
2d63488197 | |||
c1c8e4c8d4 | |||
a0e451c5e5 | |||
eaba8006e6 | |||
39ff202f8c | |||
654e0d6245 | |||
ec04443493 | |||
d247c262af | |||
dff49b2bef | |||
50666a76fb | |||
b51a7f9746 | |||
001dfd9f6c | |||
5e4fbeeb25 | |||
2c910bf6ca | |||
9b11319e81 | |||
40dc4b3fb8 | |||
0e37b98968 | |||
7e132eb014 | |||
49dfb4756e | |||
814758e2aa | |||
5c42dac5e2 | |||
88603fa4f7 | |||
0232c4e162 | |||
11753c1fe1 | |||
f5cc6c67ec | |||
8b8ed3527a | |||
1aa0274e7c | |||
ecd33ca0c1 | |||
e93be0de9a | |||
a5adc4f8ed | |||
a6baed9753 | |||
ceaf832e63 | |||
a6b0b14685 | |||
f679250edd | |||
acc4de2235 | |||
56a8276dbf | |||
6dfe6edbef | |||
6af4bd0d9a | |||
7ee7f6bd6a | |||
f8b8334010 | |||
d4b65dc4b4 | |||
e4bbd3b1c0 | |||
87de5e625d | |||
efbe51673e | |||
a95bea53ea | |||
6021fc0f52 | |||
1415b68ff4 | |||
be6853ac52 | |||
7fd6be5abb | |||
91d6f572a5 | |||
016a9ce34e | |||
8adb95af7f | |||
1dc54775d8 | |||
370ef716b5 | |||
16e56ad9ca | |||
b5b5a9eed3 | |||
8b22e7bcc3 | |||
d48b5b9511 | |||
0eccaa3f1e | |||
67d550a80d | |||
ebb5711c32 | |||
79ec872232 | |||
4284e14ff7 | |||
92a09779d0 | |||
14c621631d | |||
c55f503b9b | |||
a908cad976 | |||
c2586557d8 | |||
01c80a82e2 | |||
0d47654651 | |||
1183095833 | |||
c281b11bdc | |||
61fe45a58c | |||
d43aab479c | |||
7f8383427a | |||
a06d6cf33d | |||
5b7cb205c9 | |||
293a932d20 | |||
fff901ff03 | |||
f47c936295 | |||
88d5aec618 | |||
96ae68cf09 | |||
63b3434b6f | |||
947ecec02b | |||
1c2b452406 | |||
47777529ac | |||
949095c376 | |||
4b112c2799 | |||
291a2516b1 | |||
4dcfd021e2 | |||
ca50848db3 | |||
0bb3e3c558 | |||
e4b25809ab | |||
7bf932f8e2 | |||
99d04528b0 | |||
e48d172036 | |||
c2388137a8 | |||
650e2cbc38 | |||
b32800ea71 | |||
e1c0c0b20c | |||
fe39e39dcd | |||
883f213b03 | |||
538996f617 | |||
2f4c92deb9 | |||
ef335ec083 | |||
07b09df3fe | |||
e70e031a1f | |||
c7ba183dc0 | |||
3ed23a37ea | |||
3d724db0e3 | |||
2997542114 | |||
84b18fff96 | |||
1dce408c72 | |||
e5ff47bf14 | |||
b53bf331c3 | |||
90e9a8b34c | |||
845f842783 | |||
7397849c60 | |||
6dd46b5fc5 | |||
89ca79ed10 | |||
713bef895c | |||
925115e9ce | |||
42f5cf8c93 | |||
82cc1d536a | |||
08af2fd46b | |||
70e3b27a4d | |||
6a411d7960 | |||
33567b56d7 | |||
0c1954aeb7 | |||
f4a6c70e98 | |||
5f198e7fe4 | |||
d172d32817 | |||
af3fb5c2cd | |||
885efb526e | |||
3bfb8b2cb2 | |||
9fc5ff4b77 | |||
dd8b579dd6 | |||
e12cbd8711 | |||
62d35f8f8c | |||
49be504c13 | |||
edad55e51d | |||
38086fa8bb | |||
c4f9a3e9a7 | |||
930df791bd | |||
9a6086634c | |||
b68e65355a | |||
72d33a91dd | |||
7067e3d69a | |||
4db370d24e | |||
41e7b9b73f | |||
7f47f93e4e | |||
89abd44b76 | |||
14c7d8c4f4 | |||
525976a81b | |||
64a2126ea4 | |||
994c5882ab | |||
ad64d51e85 | |||
a184a7518a | |||
943fd80920 | |||
01bb18b8c4 | |||
94baaaa5a5 | |||
40b164ce94 | |||
1d7c7801e7 | |||
0db0a12ef3 | |||
8008aba450 | |||
eaeab27004 | |||
111fbf119b | |||
300ad88447 | |||
92cc0c9c64 | |||
18ff803370 | |||
819af78e2b | |||
6338785ce1 | |||
973e151dff | |||
fae6d83f27 | |||
ed84fe0b8d | |||
1ee603403e | |||
7db7b7cc4d | |||
68a98cd86c | |||
e758db5727 | |||
4d7d700afa | |||
f9a5add01d | |||
2986b56389 | |||
58f79b525d | |||
0a1c0dae05 | |||
e18ef8dab6 | |||
3cacc59bec | |||
4eea46d399 | |||
11e25617bd | |||
4817126811 | |||
0181361efa | |||
8ff8e1d5f7 | |||
19d5902a92 | |||
71dffb21a9 | |||
bd283c506d | |||
ef564e5f1a | |||
2543224c7c | |||
077eee9310 | |||
d894eeaa67 | |||
452bfb39bf | |||
6b6702521f | |||
c07b8d95d0 | |||
bf347730b3 | |||
ececfc3a30 | |||
b76546de0c | |||
424d490a60 | |||
127dd85214 | |||
10570ac7f8 | |||
dc5667b0b8 | |||
ec9cacb610 | |||
0027dbc0e5 | |||
c15e4b24a1 | |||
b6f518ffe6 | |||
4e476fd4e9 | |||
03503363e5 | |||
22d6621b02 | |||
0023df64c8 | |||
59a259e43a | |||
c6f39f5eb4 | |||
e3c0aad48a | |||
91dd33cee6 | |||
5a2c367e89 | |||
3b05c9cb1a | |||
6e53f1689d | |||
e3be0f2550 | |||
294f2243c1 | |||
7b1373e8d6 | |||
e70b486f20 | |||
b90174f153 | |||
7d7acd8494 | |||
4d9d7c5efb | |||
d614b3608d | |||
beb2715fa7 | |||
5769ff45b5 | |||
9d6f79558f | |||
41d5bff9d3 | |||
ec84ba9b6d | |||
042a62f99e | |||
907f02cfee | |||
53fe412bf9 | |||
ef9e177fe9 | |||
28e675596b | |||
9b7f57cc75 | |||
935a8f4d58 | |||
01fcbb325b | |||
7d3d17acb9 | |||
e434321f7c | |||
ebd476be14 | |||
31ba543c62 | |||
a101d48b5a | |||
4c166dcf52 | |||
47b1f025e1 | |||
8f44c792ac | |||
e57b6f2347 | |||
275d0dfd03 | |||
f18cbace7a | |||
212220554f | |||
a596392bc3 | |||
3e22740eac | |||
d18a691f63 | |||
3cd5e68bc1 | |||
c741c13132 | |||
924f6f104a | |||
454594025b | |||
e72097292c | |||
ab17a12184 | |||
776f3f69a5 | |||
8560c7150a | |||
301386fb4a | |||
68e8b6990b | |||
4f800c4758 | |||
90c31c2214 | |||
50e3d317b2 | |||
3eed7bb010 | |||
0ef8edc9f1 | |||
a6373ebb33 | |||
bf8ce55eea | |||
61b4fcb5f3 | |||
81275e3bd1 | |||
7988bf7748 | |||
00d8eec360 | |||
82150c8e84 | |||
1dbd749a74 | |||
a96479f16c | |||
5d5fb1f37e | |||
b6f4d6a5eb | |||
8ab5c04c2c | |||
386944117e | |||
9154b9b85d | |||
fc19372709 | |||
e5d9c6537c | |||
bf5cbac314 | |||
5cca637a3d | |||
5bfb8b454b | |||
4d96437972 | |||
d03b0b8152 | |||
c249b55ff5 | |||
1e1876b34c | |||
a27493ad1b | |||
95b1ab820e | |||
5cf9f0002b | |||
fc7a452b0c | |||
25ee0e4b45 | |||
46f12e62e8 | |||
4245dea25a | |||
908db3df81 | |||
ef4f9aa437 | |||
902dd83c67 | |||
1c4b78b5f4 | |||
d854d819d1 | |||
f246da6b73 | |||
4a56b5e827 | |||
53b10e64f8 | |||
27e4c7027c | |||
410d1b97cd | |||
f93f7e635b | |||
74eba04735 | |||
01bdaffe36 | |||
f6b556713a | |||
abe38bb16a | |||
f2b8d45999 | |||
3f61dff1cb | |||
b19da6d774 | |||
7c55616e29 | |||
952a7f07c1 | |||
6510b97c1e | |||
19b707a0fb | |||
320a600349 | |||
10110deae5 | |||
884c546f32 | |||
abec906677 | |||
22d1dd801c | |||
03891cbe09 | |||
3c5157dfd4 | |||
d241e8d51d | |||
7ba15884ed | |||
47356915b1 | |||
2520c92b78 | |||
e7e0e6d213 | |||
ca0250e19f | |||
cf4c7c1bcb | |||
670af8789a | |||
5c5634830f | |||
b6b0edb7ad | |||
45440abc80 | |||
9c42b75567 | |||
e9a477c1eb | |||
fa60655a5d | |||
5d729b4878 | |||
8692f7233f | |||
457e17fec3 | |||
87e99625e6 | |||
6f32eeea43 | |||
dfcf8b2d40 | |||
846006f2e3 | |||
f557b2129f | |||
6dc2003e34 | |||
0149c89003 | |||
f458cae954 | |||
f01d117ce6 | |||
2bde43e5dc | |||
84cc0b5490 | |||
2f3026084e | |||
89696edbee | |||
c1f0833c09 | |||
c77f804b77 | |||
8e83209631 | |||
2e48e0cc2f | |||
e72f0ab160 | |||
a3c681cc44 | |||
5b3a9e29fb | |||
15803dc67d | |||
ff37e064c9 | |||
ef8e922e2a | |||
34b11524f1 | |||
9e2492be5c | |||
b3ba083ff0 | |||
22a8603892 | |||
d83d058a4b | |||
ec3fd4a3ab | |||
0764668b14 | |||
16b6c17305 | |||
e60509697a | |||
85364af9e9 | |||
cf4b4030aa | |||
74dc025869 | |||
cabdc53553 | |||
29e9f399bd | |||
dad43017a0 | |||
7fb939f97b | |||
88859b1c26 | |||
c78236a2a2 | |||
ba55538a34 | |||
f742c73e24 | |||
ca314c262c | |||
b932b6c963 | |||
3c048a1921 | |||
8a60a7e26f | |||
f10b57ba0b | |||
e53114a645 | |||
2e50532518 | |||
1936ddfecb | |||
4afef46cb8 | |||
92b4244e81 | |||
dfbf7027bc | |||
eca2ef20d0 | |||
cac5c7b3ea | |||
37ee555c8e | |||
f910da0f8a | |||
fc9d270992 | |||
dcbc3d788a | |||
4658018a90 | |||
577b7ee515 | |||
621773c1ea | |||
3da526f20e | |||
052e465041 | |||
c843f18743 | |||
80d0b14bb8 | |||
68637cf7cf | |||
82acba26af | |||
ff8a812823 | |||
7f5fed2aea | |||
a5c30fd9c7 | |||
ef23a0da52 | |||
ba527e7141 | |||
8edc254ab5 | |||
42627d21b0 | |||
2479b157d0 | |||
602573f83f | |||
20c33fa011 | |||
8599d9efe0 | |||
8e6fcfe350 | |||
558aa45201 | |||
e9910732bc | |||
246dd4b062 | |||
4425f8d183 | |||
c410bb8c36 | |||
44f62a4773 | |||
b6ff04694f | |||
d4ce0e8e41 | |||
362d72da8c | |||
88d0f8d8a8 | |||
61097b9400 | |||
7a73ddfb60 | |||
d66f13c249 | |||
8cc3cb6a42 | |||
4c5537ddfe | |||
a95779157d | |||
70256727fd | |||
ac6afb2b82 | |||
2ea7bd86e8 | |||
95bce9c9e7 | |||
71a22c2a34 | |||
f3eb85877d | |||
273f5211a0 | |||
db06428ab9 | |||
109d8e48d4 | |||
2ca115285c | |||
f5459645a5 | |||
14c159500d | |||
03da87991f | |||
e38ee9c580 | |||
3bf53b2db1 | |||
f33190caa5 | |||
741822424a | |||
0ca6fbb224 | |||
f72b652b24 | |||
0a2c1eb419 | |||
eb9593a847 | |||
7c71c52791 | |||
59493c02c4 | |||
83089b47d3 | |||
103e723d8c | |||
7d6e88061f | |||
f8aab40e3e | |||
5123bc1316 | |||
30e8408e85 | |||
bb34474101 | |||
a105760123 | |||
f410a77010 | |||
6ff8fdcc49 | |||
50ca3dc772 | |||
2a09fc0ae2 | |||
fbb6756488 | |||
f45fb2eac0 | |||
7b8cde17e6 | |||
186634fc67 | |||
c84b1b7997 | |||
6e83467481 | |||
72db17f23b | |||
ee4e176039 | |||
e18e681c2b | |||
10fe67e08d | |||
fc1db83be7 | |||
3740e65906 | |||
30386cd899 | |||
64a10e9a46 | |||
77d6242cce | |||
9a86dcaec3 | |||
0b00768b84 | |||
d162c79373 | |||
05db352a0f | |||
5bf3d7fe02 | |||
1ae1cbebf4 | |||
8c16dfc478 | |||
c6a3286e4c | |||
44cfd7e5b0 | |||
210d4c5058 | |||
6b39d616b1 | |||
32ace1bece | |||
54f893b84f | |||
b5685ec072 | |||
5854833240 | |||
4b2437a6f1 | |||
2981ac7b10 | |||
59a51c859a | |||
47bab6c182 | |||
4e6714fffe | |||
aa6b595545 | |||
0131b1f6cc | |||
9f53c359dd | |||
28e4dba3e8 | |||
2afd46e1df | |||
f5991b19be | |||
5cc75cb25c | |||
68c1df2d39 | |||
c83724f45c | |||
5f91c150df | |||
0bfe999442 | |||
58440b16c4 | |||
57757a2ff5 | |||
2993f506a7 | |||
e4841d54a1 | |||
4f05dcec89 | |||
ede6bcd31e | |||
728c8e994d | |||
5290b64415 | |||
fec6de1ba2 | |||
69678dcfa6 | |||
4911a243ff | |||
70316b37da | |||
307cb94e3b | |||
ace53a8fa5 | |||
0544dc3f83 | |||
708ff300a3 | |||
4e63f0f215 | |||
141481df3a | |||
29241cc287 | |||
e81e97d404 | |||
a5182e5c24 | |||
cf5ff6e160 | |||
f2b3a2ec91 | |||
69780c67a9 | |||
ac9cf590bc | |||
cb6edcb198 | |||
8eecc28c3c | |||
10b16bc36a | |||
2fe88cfea9 | |||
caab396b56 | |||
5f0f4284a2 | |||
c11be2284d | |||
aa321196d7 | |||
ff03db61a8 | |||
f3b3ce6572 | |||
09b02e1aec | |||
451a9aaf01 | |||
eaee7cb562 | |||
a010c91a52 | |||
709194330f | |||
5914bbf173 | |||
5e9166f859 | |||
35b8ef6592 | |||
772a939f17 | |||
24971801cf | |||
43aebe8cb2 | |||
19cfc87c84 | |||
f920f183c8 | |||
97f979c81e | |||
e61411d396 | |||
c4f985f542 | |||
302dee7ab2 | |||
83c12ad483 | |||
4224fd5c6f | |||
597ce1eb42 | |||
5ef385f0bb | |||
cda4be3d47 | |||
8cdf22fc94 | |||
6efc7578ef | |||
4e2457560d | |||
2ddf122d27 | |||
a24651437a | |||
30bb7acb17 | |||
7859145138 | |||
8a8aafec81 | |||
deebdf2bcc | |||
4982c4abcb | |||
1486f90077 | |||
f4988bc45e | |||
8abc9cc031 | |||
534689895c | |||
8a0dd6be24 | |||
65d2eed82d | |||
e450e7b107 | |||
552ddda909 | |||
bafeff7306 | |||
6791436302 | |||
7eda794070 | |||
e3129c1067 | |||
ff481ba6e7 | |||
a106bad2db | |||
3a1c311d02 | |||
6465333f4f | |||
b761659227 | |||
9321c355f8 | |||
86c8e79ea1 | |||
8916b1f8ab | |||
41fcf2aba6 | |||
87e72b08a9 | |||
b2fcd42e3c | |||
fc1b47a80f | |||
af14e3502e | |||
a2faa5ceb5 | |||
63a19a1381 | |||
b472dcb7e7 | |||
6303909031 | |||
4bdc06865b | |||
2ee48cd039 | |||
893d5f452b | |||
340a9bc8ee | |||
cb3d9f83f1 | |||
4ba55aa8e9 | |||
bab6f501ec | |||
7327939684 | |||
ffb0135f06 | |||
ee0ddc3d17 | |||
5dd979d66c | |||
a9bd34f3c5 | |||
db316b59c5 | |||
6209714f87 | |||
1ed2bddba7 | |||
26b35c9b7b | |||
86a9271f75 | |||
402ed9bd20 | |||
68a0684569 | |||
bd2e453218 | |||
1f31c63e57 | |||
480410efa2 | |||
e9bfee52ed | |||
326b574d54 | |||
0a7abcf2ad | |||
9e5019881e | |||
8071750681 | |||
f2f0931904 | |||
a91204e5b9 | |||
b14c22cbff | |||
b3e40c6aed | |||
873aa4bb22 | |||
c1ea78c422 | |||
3c8bbc2621 | |||
42a9979d91 | |||
b7f94df4d9 | |||
4143d3fe28 | |||
f95c06b76f | |||
e3e9178ccc | |||
b694816e7b | |||
e046000f36 | |||
edb5caae9b | |||
02d27651f3 | |||
44cd4d847d | |||
472256794d | |||
cbb6887983 | |||
317e9ec605 | |||
ada2a16412 | |||
61f6b0f122 | |||
6a3f7e45cf | |||
2b78c4ba86 | |||
680ef641fb | |||
2b5504ff63 | |||
f8a6aa3250 | |||
6c23fc4b2b | |||
639c2f5c2e | |||
e44632f9a0 | |||
3f2ce34468 | |||
426cef998f | |||
8ddb62ed0f | |||
572f6d4ea0 | |||
8db68410c6 | |||
caa3c3de32 | |||
23b5ca761a | |||
f1b9021e3e | |||
99c62af89e | |||
8ae50814fe | |||
2e2b491ec7 | |||
ac432e78e2 | |||
83ac42ac43 | |||
4bd1cd127b | |||
2eb5a5cc76 | |||
75051687e6 | |||
7e316b5fc2 | |||
5594ad0b36 | |||
ea097afeae | |||
b77b4b5c80 | |||
f8dc7f48f2 | |||
692e75b057 | |||
02771683a6 | |||
40404ff41d | |||
fdd5211253 | |||
85a417d22e | |||
66c530ea06 | |||
347c3793fc | |||
cf78c89830 | |||
20c738c384 | |||
4f54ce6afb | |||
f0d7edb963 | |||
e42ad8db93 | |||
e917e756cc | |||
b4963bec76 | |||
0d23796989 | |||
d0ceafe79e | |||
f2023a7af2 | |||
31d597005f | |||
62dc86be7b | |||
7aa8e35f87 | |||
60b95271eb | |||
382b0e8941 | |||
3b068610b9 | |||
9a8f62f42e | |||
632e3cf7dc | |||
e7144649d5 | |||
dd8909c9b2 | |||
e6818c1f6a | |||
10c4e3c717 | |||
b8425867c8 | |||
a05da8cdbf | |||
c3aeefa653 | |||
62c840df21 | |||
45d1db8880 | |||
b34f30f1dd | |||
7a54e84eb4 | |||
917eef96fb | |||
9a393848b2 | |||
a6abeb50c6 | |||
39acb044fb | |||
7d2f622f4b | |||
a2b38caf64 | |||
1193b9fd22 | |||
e3a5ef1907 | |||
e597bb4542 | |||
c31df2b3f9 | |||
3f2637cffa | |||
3b6d9bec0a | |||
b184210610 | |||
d2010808ee | |||
f5b185dd06 | |||
ae161c1ba9 | |||
109283b189 | |||
235d283def | |||
96a86b3298 | |||
db9ea8603c | |||
8b7f698c7b | |||
813c13ce45 | |||
629a0e1a4d | |||
d1e2c018a3 | |||
1e86844823 | |||
b58875d4c7 | |||
03e0eecb1d | |||
7aa61d86e4 | |||
0e6a799e6d | |||
bc6afdf94f | |||
80364b04a9 | |||
0948e0ee1c | |||
5c54de66fc | |||
937edc73bc | |||
2c0d8d8943 | |||
059ccdd592 | |||
0ec0d3f1aa | |||
0a0eee138a | |||
3ed4c38101 | |||
de8cf65503 | |||
121b36f35f | |||
363aed2a47 | |||
ef994e0084 | |||
e1ef196283 | |||
f81ffd54f3 | |||
f9bfae9190 | |||
0d686465a4 | |||
e13b4a561f | |||
f6417f95e5 | |||
9c6bf5f4ae | |||
d2d7acb50e | |||
c7681dde32 | |||
8cf9661e08 | |||
2dbd76cf90 | |||
28d39f4d80 | |||
760428aa18 | |||
49bbac7441 | |||
0b8cfd437b | |||
b69aaf9417 | |||
758d1bdfd4 | |||
ab501ca971 | |||
9657741a3d | |||
29b7368f42 | |||
75724b6f8d | |||
7c9f821bfd | |||
5b9e6bed6c | |||
6113d7d768 | |||
0e3602d7eb | |||
2b94e9a687 | |||
6ed7d842e4 | |||
8794c840cf | |||
9c9c00755a | |||
6703c0a5d1 | |||
060f19ce06 | |||
b2d2e7cbc8 | |||
91fd792f88 | |||
2d9cd28221 | |||
aa64cf898f | |||
27d109c1fe | |||
1b4a14f3ee | |||
9835785864 | |||
d785998c5a | |||
8ba9553220 | |||
6eb132c48b | |||
b523cd064b | |||
355b832cc3 | |||
8f5af464a2 | |||
fb70769358 | |||
ad06778c34 | |||
bcb4451fb7 | |||
110d558572 | |||
e32d4f0095 | |||
0e413acd61 | |||
d3397c349f | |||
fb18a10e61 | |||
9bb0d04aeb | |||
666cf77b04 | |||
90ca1b8e5a | |||
f1e95b8816 | |||
dad8547212 | |||
a957e1fc45 | |||
39e3f02503 | |||
2b999e922c | |||
4224134a19 | |||
eda260dddd | |||
8a1dd521e1 | |||
1c5e91de1d | |||
4b1744fad0 | |||
f17b83010d | |||
12ddf9e73c | |||
0b3b300333 | |||
23f1a19765 | |||
b27e998615 | |||
2b928146a8 | |||
a94b0504b7 | |||
4fcbfa7709 | |||
986e01db20 | |||
9092d1189b | |||
605ed94ba2 | |||
4cbeeb9a0c | |||
993dee6aad | |||
c663deb659 | |||
61621e7d60 | |||
0ee9b07172 | |||
431ba6b4ef | |||
146818793e | |||
0ce663bce4 | |||
923ba4fb42 | |||
bb6eed0db1 | |||
d1bd8f333b | |||
2ac9f5426d | |||
8d1fd48003 | |||
241cb01ec6 | |||
65b4139997 | |||
1431be8c44 | |||
049fceeeee | |||
e6638afa3c | |||
465898c7d0 | |||
c363b1cfde | |||
b30ffd1318 | |||
fe0d3a64c8 | |||
ae9f1c1063 | |||
ea63d384fd | |||
c28d75754d | |||
518b691e00 | |||
cd845be45d | |||
a813d8e05e | |||
75f850f4d2 | |||
c84265c6f0 | |||
a477ea29cd | |||
f6aa85e340 | |||
0aeedb3ad8 | |||
4b29f238b5 | |||
34157db06a | |||
84b9e66a97 | |||
e831e4fb94 | |||
956922820b | |||
b0fac9c9f1 | |||
f4db09cd59 | |||
047030f901 | |||
638e8d741f | |||
425b87a6d0 | |||
e7dc763612 | |||
a80cc94da9 | |||
547dd3cb7a | |||
95739a934c | |||
d12e24017e | |||
e4a0345231 | |||
078633c2af | |||
4b8b800648 | |||
6f9ed001a1 | |||
e4095dfffe | |||
d5341c2284 | |||
357bd65028 | |||
867fb0dac0 | |||
2666aa2c73 | |||
f0e9bafa35 | |||
0d739f5c1a | |||
e08077c73a | |||
7cf8a31057 | |||
c43049a981 | |||
1a9ace6f9d | |||
b8d86bc482 | |||
f7044e41c6 | |||
fa59fec17a | |||
e29afa289e | |||
4d4193a586 | |||
59343ff441 | |||
cab564152d | |||
97b814ab33 | |||
88516ba2ca | |||
f069cfb643 | |||
4ce3c2341c | |||
77e42d60cb | |||
cacb919c6f | |||
2a3b049b01 | |||
e4a5e86c93 | |||
3a51bcd890 | |||
c28f68400d | |||
5d50fc281a | |||
9f7d1466e9 | |||
c815d24806 | |||
d1200a7e40 | |||
edd4f9ceae | |||
1cfe81887b | |||
bb5e0ebab1 | |||
dfda76d896 | |||
8fc5114ce4 | |||
e7b4363d21 | |||
53905d1a89 | |||
0ad1392632 | |||
6db1c914ee | |||
00324f922d | |||
8a24ddad28 | |||
0f85fe3c29 | |||
1f05eaa420 | |||
84e126a32c | |||
9ae69866bd | |||
56576a7f44 | |||
7f0295ba53 | |||
5553b3ff36 | |||
6f969525fe | |||
bac12246fb | |||
b53ef6e529 | |||
39c62afb93 | |||
c98bdbacc5 | |||
1e8d45dc15 | |||
202b057ce9 | |||
d5d8641b37 | |||
9dd37689e3 | |||
cc0832f487 | |||
b515bf7d2e | |||
34fbf3941b | |||
e73606b54d | |||
0a413fe21a | |||
d1b9f1e6b8 | |||
e5a6e128e4 | |||
9295d1ed0b | |||
5d479a6c8f | |||
4a773b2b4f | |||
8003d67844 | |||
58baf97e2d | |||
51783c1cbb | |||
94290c7e36 | |||
123ff7ad1f | |||
8f3e863cce | |||
3d6c459349 | |||
6a583bae49 | |||
78e5879d9a | |||
fdcac2a9ed | |||
e81715caef | |||
ab2b13938e | |||
5c97a3aef3 | |||
e6963c543d | |||
9ca15983a2 | |||
99ef94b7aa | |||
133bedafba | |||
c3faa61ed9 | |||
da74304221 | |||
ed6659a46d | |||
0abb1f94a4 | |||
c7e299e0bf | |||
8a6590bac8 | |||
ed717dcfa2 | |||
b6df42f580 | |||
2ea85bd0c4 | |||
68fa8105e1 | |||
79db0ce4c1 | |||
5e23b11764 | |||
c4e029ffe2 | |||
61b5b36192 | |||
c6cc1b1728 | |||
77dd652160 | |||
1144944adb | |||
7751be284e | |||
74382c6287 | |||
011babbbd9 | |||
3c01a1dd7b | |||
6e832be2de | |||
46017f2f86 | |||
da50eb0369 | |||
b996e3cee7 | |||
12735cc14c | |||
4d36699b78 | |||
8110d2861b | |||
1cc60f572d | |||
90151a13ae | |||
f958aa6930 | |||
13fbac30a2 | |||
4f4cdf16f1 | |||
7d75599627 | |||
924a13e832 | |||
ae83c35dfd | |||
e9102f4e28 | |||
9b8c1cbea5 | |||
6424bf98da | |||
74fb0f9e2a | |||
4380f37a77 | |||
17fccd44e6 | |||
217a8b5610 | |||
2cef220a3e | |||
5a8c66d325 | |||
8de13d3f67 | |||
5c22bedbaf | |||
8a0f993f0b | |||
abcf515a69 | |||
894f704c27 | |||
7798292aa8 | |||
3005ca17bd | |||
909461e533 | |||
df838a4023 | |||
0f86b62dd3 | |||
a40c3aeb68 | |||
4080738ded | |||
4a89be3048 | |||
e587c53e18 | |||
023b97aa69 | |||
51365dba74 | |||
0d3705685e | |||
738e4d5c74 | |||
b14b9cb0dd | |||
2a21ebf7b0 | |||
5bc1301043 | |||
e0e4bf6972 | |||
337677ad12 | |||
3712d5aee2 | |||
dd82d55725 | |||
8d766efecb | |||
9ac3b29418 | |||
5000c5b061 | |||
b362d2af03 | |||
bcd42fce13 | |||
6deddd038f | |||
3b47cb64da | |||
cf5e70c759 | |||
20bc38a54b | |||
672a4ab1f4 | |||
47dd667261 | |||
d1ac69789b | |||
08abf81c6d | |||
76bd987e6f | |||
5374352411 | |||
08eff4cc5d | |||
c87a9f9489 | |||
8f6d700aa8 | |||
c6843b026c | |||
3769c33ef0 | |||
8982afaf44 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.10.2
|
||||
current_version = 2022.6.3
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
@ -17,7 +17,7 @@ values =
|
||||
beta
|
||||
stable
|
||||
|
||||
[bumpversion:file:website/docs/installation/docker-compose.md]
|
||||
[bumpversion:file:pyproject.toml]
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
@ -30,7 +30,3 @@ values =
|
||||
[bumpversion:file:internal/constants/constants.go]
|
||||
|
||||
[bumpversion:file:web/src/constants.ts]
|
||||
|
||||
[bumpversion:file:website/docs/outposts/manual-deploy-docker-compose.md]
|
||||
|
||||
[bumpversion:file:website/docs/outposts/manual-deploy-kubernetes.md]
|
||||
|
49
.github/actions/docker-setup/action.yml
vendored
Normal file
49
.github/actions/docker-setup/action.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
name: 'Prepare docker environment variables'
|
||||
description: 'Prepare docker environment variables'
|
||||
|
||||
outputs:
|
||||
shouldBuild:
|
||||
description: "Whether to build image or not"
|
||||
value: ${{ steps.ev.outputs.shouldBuild }}
|
||||
branchName:
|
||||
description: "Branch name"
|
||||
value: ${{ steps.ev.outputs.branchName }}
|
||||
branchNameContainer:
|
||||
description: "Branch name (for containers)"
|
||||
value: ${{ steps.ev.outputs.branchNameContainer }}
|
||||
timestamp:
|
||||
description: "Timestamp"
|
||||
value: ${{ steps.ev.outputs.timestamp }}
|
||||
sha:
|
||||
description: "sha"
|
||||
value: ${{ steps.ev.outputs.sha }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Generate config
|
||||
id: ev
|
||||
shell: python
|
||||
run: |
|
||||
"""Helper script to get the actual branch name, docker safe"""
|
||||
import os
|
||||
from time import time
|
||||
|
||||
env_pr_branch = "GITHUB_HEAD_REF"
|
||||
default_branch = "GITHUB_REF"
|
||||
sha = "GITHUB_SHA"
|
||||
|
||||
branch_name = os.environ[default_branch]
|
||||
if os.environ.get(env_pr_branch, "") != "":
|
||||
branch_name = os.environ[env_pr_branch]
|
||||
|
||||
should_build = str(os.environ.get("DOCKER_USERNAME", "") != "").lower()
|
||||
|
||||
print("##[set-output name=branchName]%s" % branch_name)
|
||||
print(
|
||||
"##[set-output name=branchNameContainer]%s"
|
||||
% branch_name.replace("refs/heads/", "").replace("/", "-")
|
||||
)
|
||||
print("##[set-output name=timestamp]%s" % int(time()))
|
||||
print("##[set-output name=sha]%s" % os.environ[sha])
|
||||
print("##[set-output name=shouldBuild]%s" % should_build)
|
45
.github/actions/setup/action.yml
vendored
Normal file
45
.github/actions/setup/action.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: 'Setup authentik testing environemnt'
|
||||
description: 'Setup authentik testing environemnt'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install poetry
|
||||
shell: bash
|
||||
run: |
|
||||
pipx install poetry || true
|
||||
sudo apt update
|
||||
sudo apt install -y libxmlsec1-dev pkg-config gettext
|
||||
- name: Setup python and restore poetry
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: 'poetry'
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.1.0
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Setup dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
docker-compose -f .github/actions/setup/docker-compose.yml up -d
|
||||
poetry env use python3.10
|
||||
poetry install
|
||||
npm install -g pyright@1.1.136
|
||||
- name: Generate config
|
||||
shell: poetry run python {0}
|
||||
run: |
|
||||
from authentik.lib.generators import generate_id
|
||||
from yaml import safe_dump
|
||||
|
||||
with open("local.env.yml", "w") as _config:
|
||||
safe_dump(
|
||||
{
|
||||
"log_level": "debug",
|
||||
"secret_key": generate_id(),
|
||||
},
|
||||
_config,
|
||||
default_flow_style=False,
|
||||
)
|
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@ -1,7 +1,7 @@
|
||||
<!--
|
||||
👋 Hello there! Welcome.
|
||||
|
||||
Please check the [Contributing guidelines](https://github.com/goauthentik/authentik/blob/master/CONTRIBUTING.md#how-can-i-contribute).
|
||||
Please check the [Contributing guidelines](https://github.com/goauthentik/authentik/blob/main/CONTRIBUTING.md#how-can-i-contribute).
|
||||
-->
|
||||
|
||||
# Details
|
||||
|
4
.github/stale.yml
vendored
4
.github/stale.yml
vendored
@ -7,6 +7,10 @@ exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- pr_wanted
|
||||
- enhancement
|
||||
- bug/confirmed
|
||||
- enhancement/confirmed
|
||||
- question
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
|
322
.github/workflows/ci-main.yml
vendored
322
.github/workflows/ci-main.yml
vendored
@ -3,14 +3,14 @@ name: authentik-ci-main
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- next
|
||||
- version-*
|
||||
paths-ignore:
|
||||
- website
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
env:
|
||||
POSTGRES_DB: authentik
|
||||
@ -18,201 +18,90 @@ env:
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
|
||||
jobs:
|
||||
lint-pylint:
|
||||
lint:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- pylint
|
||||
- black
|
||||
- isort
|
||||
- bandit
|
||||
- pyright
|
||||
- pending-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 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
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run job
|
||||
run: poetry run make ci-${{ matrix.job }}
|
||||
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
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run migrations
|
||||
run: pipenv run python -m lifecycle.migrate
|
||||
run: poetry run python -m lifecycle.migrate
|
||||
test-migrations-from-stable:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
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: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: checkout stable
|
||||
run: |
|
||||
# Copy current, latest config to local
|
||||
cp authentik/lib/default.yml local.env.yml
|
||||
cp -R .github ..
|
||||
cp -R scripts ..
|
||||
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
|
||||
rm -rf .github/ scripts/
|
||||
mv ../.github ../scripts .
|
||||
- name: Setup authentik env (ensure stable deps are installed)
|
||||
uses: ./.github/actions/setup
|
||||
- name: run migrations to stable
|
||||
run: pipenv run python -m lifecycle.migrate
|
||||
run: poetry 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
|
||||
git reset --hard HEAD
|
||||
git clean -d -fx .
|
||||
git checkout $GITHUB_SHA
|
||||
poetry install
|
||||
- name: Setup authentik env (ensure latest deps are installed)
|
||||
uses: ./.github/actions/setup
|
||||
- name: migrate to latest
|
||||
run: pipenv run python -m lifecycle.migrate
|
||||
run: poetry 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: actions/checkout@v3
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
- name: run unittest
|
||||
run: |
|
||||
pipenv run make test
|
||||
pipenv run coverage xml
|
||||
poetry run make test
|
||||
poetry run coverage xml
|
||||
- name: run testspace
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
testspace [unittest]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
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: actions/checkout@v3
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
@ -220,93 +109,121 @@ jobs:
|
||||
uses: helm/kind-action@v1.2.0
|
||||
- name: run integration
|
||||
run: |
|
||||
pipenv run make test-integration
|
||||
pipenv run coverage xml
|
||||
poetry run make test-integration
|
||||
poetry run coverage xml
|
||||
- name: run testspace
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
testspace [integration]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v2
|
||||
test-e2e:
|
||||
uses: codecov/codecov-action@v3
|
||||
test-e2e-provider:
|
||||
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: actions/checkout@v3
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- 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 }}
|
||||
- name: Setup authentik env
|
||||
run: |
|
||||
scripts/ci_prepare.sh
|
||||
docker-compose -f tests/e2e/docker-compose.yml up -d
|
||||
- id: cache-web
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v3
|
||||
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'
|
||||
working-directory: web
|
||||
run: |
|
||||
cd web
|
||||
npm i
|
||||
npm ci
|
||||
npm run build
|
||||
- name: run e2e
|
||||
run: |
|
||||
pipenv run make test-e2e
|
||||
pipenv run coverage xml
|
||||
poetry run make test-e2e-provider
|
||||
poetry run coverage xml
|
||||
- name: run testspace
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
testspace [e2e]unittest.xml --link=codecov
|
||||
testspace [e2e-provider]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v2
|
||||
build:
|
||||
uses: codecov/codecov-action@v3
|
||||
test-e2e-rest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
- name: Setup authentik env
|
||||
run: |
|
||||
docker-compose -f tests/e2e/docker-compose.yml up -d
|
||||
- id: cache-web
|
||||
uses: actions/cache@v3
|
||||
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'
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
- name: run e2e
|
||||
run: |
|
||||
poetry run make test-e2e-rest
|
||||
poetry run coverage xml
|
||||
- name: run testspace
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
testspace [e2e-rest]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v3
|
||||
ci-core-mark:
|
||||
needs:
|
||||
- lint-pylint
|
||||
- lint-black
|
||||
- lint-isort
|
||||
- lint-bandit
|
||||
- lint-pyright
|
||||
- lint
|
||||
- test-migrations
|
||||
- test-migrations-from-stable
|
||||
- test-unittest
|
||||
- test-integration
|
||||
- test-e2e
|
||||
- test-e2e-rest
|
||||
- test-e2e-provider
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: echo mark
|
||||
build:
|
||||
needs: ci-core-mark
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- 'linux/amd64'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: prepare variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||
run: |
|
||||
python ./scripts/gh_env.py
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
uses: ./.github/actions/docker-setup
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
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
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||
tags: |
|
||||
@ -314,3 +231,4 @@ jobs:
|
||||
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 }}
|
||||
platforms: ${{ matrix.arch }}
|
||||
|
105
.github/workflows/ci-outpost.yml
vendored
105
.github/workflows/ci-outpost.yml
vendored
@ -3,61 +3,85 @@ name: authentik-ci-outpost
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- next
|
||||
- version-*
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint-golint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.16.3'
|
||||
- name: Run linter
|
||||
go-version: "^1.17"
|
||||
- name: Prepare and generate API
|
||||
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:
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
test-unittest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "^1.17"
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: Go unittests
|
||||
run: |
|
||||
go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./...
|
||||
ci-outpost-mark:
|
||||
needs:
|
||||
- lint-golint
|
||||
- test-unittest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo mark
|
||||
build:
|
||||
timeout-minutes: 120
|
||||
needs:
|
||||
- ci-outpost-mark
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
type:
|
||||
- proxy
|
||||
- ldap
|
||||
arch:
|
||||
- 'linux/amd64'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: prepare variables
|
||||
id: ev
|
||||
uses: ./.github/actions/docker-setup
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||
run: |
|
||||
python ./scripts/gh_env.py
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: Building Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||
tags: |
|
||||
@ -67,3 +91,44 @@ jobs:
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||
platforms: ${{ matrix.arch }}
|
||||
build-outpost-binary:
|
||||
timeout-minutes: 120
|
||||
needs:
|
||||
- ci-outpost-mark
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
type:
|
||||
- proxy
|
||||
- ldap
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "^1.17"
|
||||
- uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
npm run build-proxy
|
||||
- name: Build outpost
|
||||
run: |
|
||||
set -x
|
||||
export GOOS=${{ matrix.goos }}
|
||||
export GOARCH=${{ matrix.goarch }}
|
||||
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
path: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
|
76
.github/workflows/ci-web.yml
vendored
76
.github/workflows/ci-web.yml
vendored
@ -3,87 +3,85 @@ name: authentik-ci-web
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- next
|
||||
- version-*
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint-eslint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: |
|
||||
cd web
|
||||
npm install
|
||||
- working-directory: web/
|
||||
run: npm ci
|
||||
- name: Generate API
|
||||
run: make gen-web
|
||||
run: make gen-client-web
|
||||
- name: Eslint
|
||||
run: |
|
||||
cd web
|
||||
npm run lint
|
||||
working-directory: web/
|
||||
run: npm run lint
|
||||
lint-prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: |
|
||||
cd web
|
||||
npm install
|
||||
- working-directory: web/
|
||||
run: npm ci
|
||||
- name: Generate API
|
||||
run: make gen-web
|
||||
run: make gen-client-web
|
||||
- name: prettier
|
||||
run: |
|
||||
cd web
|
||||
npm run prettier-check
|
||||
working-directory: web/
|
||||
run: npm run prettier-check
|
||||
lint-lit-analyse:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: |
|
||||
cd web
|
||||
npm install
|
||||
- working-directory: web/
|
||||
run: npm ci
|
||||
- name: Generate API
|
||||
run: make gen-web
|
||||
run: make gen-client-web
|
||||
- name: lit-analyse
|
||||
run: |
|
||||
cd web
|
||||
npm run lit-analyse
|
||||
build:
|
||||
working-directory: web/
|
||||
run: npm run lit-analyse
|
||||
ci-web-mark:
|
||||
needs:
|
||||
- lint-eslint
|
||||
- lint-prettier
|
||||
- lint-lit-analyse
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- run: echo mark
|
||||
build:
|
||||
needs:
|
||||
- ci-web-mark
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: |
|
||||
cd web
|
||||
npm install
|
||||
- working-directory: web/
|
||||
run: npm ci
|
||||
- name: Generate API
|
||||
run: make gen-web
|
||||
run: make gen-client-web
|
||||
- name: build
|
||||
run: |
|
||||
cd web
|
||||
npm run build
|
||||
working-directory: web/
|
||||
run: npm run build
|
||||
|
33
.github/workflows/ci-website.yml
vendored
Normal file
33
.github/workflows/ci-website.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
name: authentik-ci-website
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
- version-*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint-prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: website/package-lock.json
|
||||
- working-directory: website/
|
||||
run: npm ci
|
||||
- name: prettier
|
||||
working-directory: website/
|
||||
run: npm run prettier-check
|
||||
ci-website-mark:
|
||||
needs:
|
||||
- lint-prettier
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo mark
|
12
.github/workflows/codeql-analysis.yml
vendored
12
.github/workflows/codeql-analysis.yml
vendored
@ -2,10 +2,10 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, '*', next, version* ]
|
||||
branches: [ main, '*', next, version* ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '30 6 * * 5'
|
||||
|
||||
@ -28,11 +28,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@ -43,7 +43,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
2
.github/workflows/ghcr-retention.yml
vendored
2
.github/workflows/ghcr-retention.yml
vendored
@ -19,4 +19,4 @@ jobs:
|
||||
org-name: goauthentik
|
||||
untagged-only: false
|
||||
token: ${{ secrets.GHCR_CLEANUP_TOKEN }}
|
||||
skip-tags: gh-next,gh-master
|
||||
skip-tags: gh-next,gh-main
|
||||
|
166
.github/workflows/release-publish.yml
vendored
166
.github/workflows/release-publish.yml
vendored
@ -9,134 +9,119 @@ jobs:
|
||||
build-server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Building Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.10.2,
|
||||
beryju/authentik:2022.6.3,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.10.2,
|
||||
ghcr.io/goauthentik/server:2022.6.3,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.10.2', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik:latest
|
||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||
docker push beryju/authentik:stable
|
||||
docker pull ghcr.io/goauthentik/server:latest
|
||||
docker tag ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:stable
|
||||
docker push ghcr.io/goauthentik/server:stable
|
||||
build-proxy:
|
||||
build-outpost:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
type:
|
||||
- proxy
|
||||
- ldap
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "^1.15"
|
||||
go-version: "^1.17"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Building Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-proxy:2021.10.2,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.10.2,
|
||||
ghcr.io/goauthentik/proxy:latest
|
||||
file: proxy.Dockerfile
|
||||
beryju/authentik-${{ matrix.type }}:2022.6.3,
|
||||
beryju/authentik-${{ matrix.type }}:latest,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:2022.6.3,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:latest
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.10.2', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-proxy:latest
|
||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||
docker push beryju/authentik-proxy:stable
|
||||
docker pull ghcr.io/goauthentik/proxy:latest
|
||||
docker tag ghcr.io/goauthentik/proxy:latest ghcr.io/goauthentik/proxy:stable
|
||||
docker push ghcr.io/goauthentik/proxy:stable
|
||||
build-ldap:
|
||||
build-outpost-binary:
|
||||
timeout-minutes: 120
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
type:
|
||||
- proxy
|
||||
- ldap
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "^1.15"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@v1
|
||||
go-version: "^1.17"
|
||||
- uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Building Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-ldap:2021.10.2,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.10.2,
|
||||
ghcr.io/goauthentik/ldap:latest
|
||||
file: ldap.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.10.2', 'rc') }}
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
docker pull beryju/authentik-ldap:latest
|
||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||
docker push beryju/authentik-ldap:stable
|
||||
docker pull ghcr.io/goauthentik/ldap:latest
|
||||
docker tag ghcr.io/goauthentik/ldap:latest ghcr.io/goauthentik/ldap:stable
|
||||
docker push ghcr.io/goauthentik/ldap:stable
|
||||
npm ci
|
||||
npm run build-proxy
|
||||
- name: Build outpost
|
||||
run: |
|
||||
set -x
|
||||
export GOOS=${{ matrix.goos }}
|
||||
export GOARCH=${{ matrix.goarch }}
|
||||
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
|
||||
- name: Upload binaries to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
tag: ${{ github.ref }}
|
||||
test-release:
|
||||
needs:
|
||||
- build-server
|
||||
- build-proxy
|
||||
- build-ldap
|
||||
- build-outpost
|
||||
- build-outpost-binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
echo "PG_PASS=$(openssl rand -base64 32)" >> .env
|
||||
@ -147,20 +132,17 @@ jobs:
|
||||
docker-compose run -u root server test
|
||||
sentry-release:
|
||||
needs:
|
||||
- test-release
|
||||
- build-server
|
||||
- build-outpost
|
||||
- build-outpost-binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Build web api client and web ui
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get static files from docker image
|
||||
run: |
|
||||
export NODE_ENV=production
|
||||
cd web
|
||||
npm i
|
||||
npm run build
|
||||
docker pull ghcr.io/goauthentik/server:latest
|
||||
container=$(docker container create ghcr.io/goauthentik/server:latest)
|
||||
docker cp ${container}:web/ .
|
||||
- name: Create a Sentry.io release
|
||||
uses: getsentry/action-release@v1
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
@ -170,7 +152,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.10.2
|
||||
version: authentik@2022.6.3
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
url_prefix: '~/static/dist'
|
||||
|
5
.github/workflows/release-tag.yml
vendored
5
.github/workflows/release-tag.yml
vendored
@ -10,11 +10,12 @@ jobs:
|
||||
name: Create Release from Tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Pre-release test
|
||||
run: |
|
||||
echo "PG_PASS=$(openssl rand -base64 32)" >> .env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
|
||||
docker buildx install
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t testing:latest \
|
||||
@ -26,7 +27,7 @@ jobs:
|
||||
docker-compose run -u root server test
|
||||
- name: Extract version number
|
||||
id: get_version
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
32
.github/workflows/translation-compile.yml
vendored
32
.github/workflows/translation-compile.yml
vendored
@ -1,11 +1,12 @@
|
||||
name: authentik-backend-translate-compile
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- '/locale/'
|
||||
pull_request:
|
||||
paths:
|
||||
- '/locale/'
|
||||
schedule:
|
||||
- cron: "0 */2 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@ -17,30 +18,19 @@ 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
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run compile
|
||||
run: pipenv run ./manage.py compilemessages
|
||||
run: poetry run ./manage.py compilemessages
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: compile-backend-translation
|
||||
commit-message: "core: compile backend translations"
|
||||
title: "core: compile backend translations"
|
||||
body: "core: compile backend translations"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
|
21
.github/workflows/web-api-publish.yml
vendored
21
.github/workflows/web-api-publish.yml
vendored
@ -1,39 +1,42 @@
|
||||
name: authentik-web-api-publish
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'schema.yml'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Generate API Client
|
||||
run: make gen-web
|
||||
run: make gen-client-web
|
||||
- name: Publish package
|
||||
working-directory: gen-ts-api/
|
||||
run: |
|
||||
cd web-api/
|
||||
npm i
|
||||
npm ci
|
||||
npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
- name: Upgrade /web
|
||||
working-directory: web/
|
||||
run: |
|
||||
cd web/
|
||||
export VERSION=`node -e 'console.log(require("../web-api/package.json").version)'`
|
||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
id: cpr
|
||||
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"
|
||||
body: "web: Update Web API Client version"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -66,7 +66,9 @@ coverage.xml
|
||||
unittest.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
# Have to include binary mo files as they are annoying to compile at build time
|
||||
# since a full postgres and redis instance are required
|
||||
# *.mo
|
||||
|
||||
# Django stuff:
|
||||
|
||||
@ -200,5 +202,4 @@ media/
|
||||
*mmdb
|
||||
|
||||
.idea/
|
||||
/api/
|
||||
/web-api/
|
||||
/gen-*/
|
||||
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -1,5 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"akadmin",
|
||||
"asgi",
|
||||
"authentik",
|
||||
"authn",
|
||||
@ -10,7 +11,10 @@
|
||||
"plex",
|
||||
"saml",
|
||||
"totp",
|
||||
"webauthn"
|
||||
"webauthn",
|
||||
"traefik",
|
||||
"passwordless",
|
||||
"kubernetes"
|
||||
],
|
||||
"python.linting.pylintEnabled": true,
|
||||
"todo-tree.tree.showCountsInTree": true,
|
||||
|
@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
hello@beryju.org.
|
||||
hello@goauthentik.io.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
86
Dockerfile
86
Dockerfile
@ -1,45 +1,43 @@
|
||||
# Stage 1: Lock python dependencies
|
||||
FROM docker.io/python:3.9-bullseye as locker
|
||||
# Stage 1: Build website
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/node:18 as website-builder
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -r --dev-only > requirements-dev.txt
|
||||
|
||||
# Stage 2: Build website
|
||||
FROM docker.io/node:16 as website-builder
|
||||
|
||||
COPY ./website /static/
|
||||
COPY ./website /work/website/
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build-docs-only
|
||||
WORKDIR /work/website
|
||||
RUN npm ci && npm run build-docs-only
|
||||
|
||||
# Stage 3: Build webui
|
||||
FROM docker.io/node:16 as web-builder
|
||||
# Stage 2: Build webui
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/node:18 as web-builder
|
||||
|
||||
COPY ./web /static/
|
||||
COPY ./web /work/web/
|
||||
COPY ./website /work/website/
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build
|
||||
WORKDIR /work/web
|
||||
RUN npm ci && npm run build
|
||||
|
||||
# Stage 3: Poetry to requirements.txt export
|
||||
FROM docker.io/python:3.10.4-slim-bullseye AS poetry-locker
|
||||
|
||||
WORKDIR /work
|
||||
COPY ./pyproject.toml /work
|
||||
COPY ./poetry.lock /work
|
||||
|
||||
RUN pip install --no-cache-dir poetry && \
|
||||
poetry export -f requirements.txt --output requirements.txt && \
|
||||
poetry export -f requirements.txt --dev --output requirements-dev.txt
|
||||
|
||||
# Stage 4: Build go proxy
|
||||
FROM docker.io/golang:1.17.2-bullseye AS builder
|
||||
FROM docker.io/golang:1.18.3-bullseye AS builder
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
COPY --from=web-builder /static/robots.txt /work/web/robots.txt
|
||||
COPY --from=web-builder /static/security.txt /work/web/security.txt
|
||||
COPY --from=web-builder /static/dist/ /work/web/dist/
|
||||
COPY --from=web-builder /static/authentik/ /work/web/authentik/
|
||||
COPY --from=website-builder /static/help/ /work/website/help/
|
||||
COPY --from=web-builder /work/web/robots.txt /work/web/robots.txt
|
||||
COPY --from=web-builder /work/web/security.txt /work/web/security.txt
|
||||
|
||||
COPY ./cmd /work/cmd
|
||||
COPY ./web/static.go /work/web/static.go
|
||||
COPY ./website/static.go /work/website/static.go
|
||||
COPY ./internal /work/internal
|
||||
COPY ./go.mod /work/go.mod
|
||||
COPY ./go.sum /work/go.sum
|
||||
@ -47,29 +45,36 @@ COPY ./go.sum /work/go.sum
|
||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
||||
|
||||
# Stage 5: Run
|
||||
FROM docker.io/python:3.9-bullseye
|
||||
FROM docker.io/python:3.10.4-slim-bullseye
|
||||
|
||||
LABEL org.opencontainers.image.url https://goauthentik.io
|
||||
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
|
||||
LABEL org.opencontainers.image.source https://github.com/goauthentik/authentik
|
||||
|
||||
WORKDIR /
|
||||
COPY --from=locker /app/requirements.txt /
|
||||
COPY --from=locker /app/requirements-dev.txt /
|
||||
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
COPY --from=poetry-locker /work/requirements.txt /
|
||||
COPY --from=poetry-locker /work/requirements-dev.txt /
|
||||
|
||||
RUN apt-get update && \
|
||||
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 - && \
|
||||
echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
||||
apt-get update && \
|
||||
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 && \
|
||||
apt-get remove --purge -y build-essential git && \
|
||||
# Required for installing pip packages
|
||||
apt-get install -y --no-install-recommends build-essential pkg-config libxmlsec1-dev && \
|
||||
# Required for runtime
|
||||
apt-get install -y --no-install-recommends libxmlsec1-openssl libmaxminddb0 && \
|
||||
# Required for bootstrap & healtcheck
|
||||
apt-get install -y --no-install-recommends curl runit && \
|
||||
pip install --no-cache-dir -r /requirements.txt && \
|
||||
apt-get remove --purge -y build-essential pkg-config libxmlsec1-dev && \
|
||||
apt-get autoremove --purge -y && \
|
||||
apt-get clean && \
|
||||
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
||||
mkdir /backups && \
|
||||
chown authentik:authentik /backups
|
||||
mkdir -p /certs /media && \
|
||||
mkdir -p /authentik/.ssh && \
|
||||
chown authentik:authentik /certs /media /authentik/.ssh
|
||||
|
||||
COPY ./authentik/ /authentik
|
||||
COPY ./pyproject.toml /
|
||||
@ -78,6 +83,9 @@ COPY ./tests /tests
|
||||
COPY ./manage.py /
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
COPY --from=builder /work/authentik /authentik-proxy
|
||||
COPY --from=web-builder /work/web/dist/ /web/dist/
|
||||
COPY --from=web-builder /work/web/authentik/ /web/authentik/
|
||||
COPY --from=website-builder /work/website/help/ /website/help/
|
||||
|
||||
USER authentik
|
||||
|
||||
|
139
Makefile
139
Makefile
@ -4,16 +4,31 @@ UID = $(shell id -u)
|
||||
GID = $(shell id -g)
|
||||
NPM_VERSION = $(shell python -m scripts.npm_version)
|
||||
|
||||
all: lint-fix lint test gen
|
||||
all: lint-fix lint test gen web
|
||||
|
||||
test-integration:
|
||||
coverage run manage.py test -v 3 tests/integration
|
||||
coverage run manage.py test tests/integration
|
||||
|
||||
test-e2e:
|
||||
coverage run manage.py test --failfast -v 3 tests/e2e
|
||||
test-e2e-provider:
|
||||
coverage run manage.py test tests/e2e/test_provider*
|
||||
|
||||
test-e2e-rest:
|
||||
coverage run manage.py test tests/e2e/test_flows* tests/e2e/test_source*
|
||||
|
||||
test-go:
|
||||
go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
test-docker:
|
||||
echo "PG_PASS=$(openssl rand -base64 32)" >> .env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
|
||||
docker-compose pull -q
|
||||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
docker-compose run -u root server test
|
||||
rm -f .env
|
||||
|
||||
test:
|
||||
coverage run manage.py test -v 3 authentik
|
||||
coverage run manage.py test authentik
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
@ -32,51 +47,127 @@ lint-fix:
|
||||
lint:
|
||||
bandit -r authentik tests lifecycle -x node_modules
|
||||
pylint authentik tests lifecycle
|
||||
golangci-lint run -v
|
||||
|
||||
i18n-extract:
|
||||
i18n-extract: i18n-extract-core web-extract
|
||||
|
||||
i18n-extract-core:
|
||||
./manage.py makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en
|
||||
cd web && npm run extract
|
||||
|
||||
gen-build:
|
||||
./manage.py spectacular --file schema.yml
|
||||
AUTHENTIK_DEBUG=true ./manage.py spectacular --file schema.yml
|
||||
|
||||
gen-clean:
|
||||
rm -rf web/api/src/
|
||||
rm -rf api/
|
||||
|
||||
gen-web:
|
||||
gen-client-web:
|
||||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
openapitools/openapi-generator-cli generate \
|
||||
openapitools/openapi-generator-cli:v6.0.0 generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/web-api \
|
||||
-o /local/gen-ts-api \
|
||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=@goauthentik/api,npmVersion=${NPM_VERSION}
|
||||
mkdir -p web/node_modules/@goauthentik/api
|
||||
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
|
||||
\cp -fv scripts/web_api_readme.md gen-ts-api/README.md
|
||||
cd gen-ts-api && npm i
|
||||
\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api
|
||||
|
||||
gen-outpost:
|
||||
gen-client-go:
|
||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml
|
||||
mkdir -p templates
|
||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O templates/README.mustache
|
||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O templates/go.mod.mustache
|
||||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
openapitools/openapi-generator-cli generate \
|
||||
--git-host goauthentik.io \
|
||||
--git-repo-id outpost \
|
||||
--git-user-id api \
|
||||
openapitools/openapi-generator-cli:v6.0.0 generate \
|
||||
-i /local/schema.yml \
|
||||
-g go \
|
||||
-o /local/api \
|
||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false
|
||||
rm -f api/go.mod api/go.sum
|
||||
-o /local/gen-go-api \
|
||||
-c /local/config.yaml
|
||||
go mod edit -replace goauthentik.io/api/v3=./gen-go-api
|
||||
rm -rf config.yaml ./templates/
|
||||
|
||||
gen: gen-build gen-clean gen-web
|
||||
gen: gen-build gen-clean gen-client-web
|
||||
|
||||
migrate:
|
||||
python -m lifecycle.migrate
|
||||
|
||||
run:
|
||||
go run -v cmd/server/main.go
|
||||
|
||||
#########################
|
||||
## Web
|
||||
#########################
|
||||
|
||||
web-build: web-install
|
||||
cd web && npm run build
|
||||
|
||||
web: web-lint-fix web-lint web-extract
|
||||
|
||||
web-install:
|
||||
cd web && npm ci
|
||||
|
||||
web-watch:
|
||||
cd web && npm run watch
|
||||
|
||||
web-lint-fix:
|
||||
cd web && npm run prettier
|
||||
|
||||
web-lint:
|
||||
cd web && npm run lint
|
||||
cd web && npm run lit-analyse
|
||||
|
||||
web-extract:
|
||||
cd web && npm run extract
|
||||
|
||||
#########################
|
||||
## Website
|
||||
#########################
|
||||
|
||||
website: website-lint-fix
|
||||
|
||||
website-install:
|
||||
cd website && npm ci
|
||||
|
||||
website-lint-fix:
|
||||
cd website && npm run prettier
|
||||
|
||||
website-watch:
|
||||
cd website && npm run watch
|
||||
|
||||
# These targets are use by GitHub actions to allow usage of matrix
|
||||
# which makes the YAML File a lot smaller
|
||||
|
||||
ci--meta-debug:
|
||||
python -V
|
||||
node --version
|
||||
|
||||
ci-pylint: ci--meta-debug
|
||||
pylint authentik tests lifecycle
|
||||
|
||||
ci-black: ci--meta-debug
|
||||
black --check authentik tests lifecycle
|
||||
|
||||
ci-isort: ci--meta-debug
|
||||
isort --check authentik tests lifecycle
|
||||
|
||||
ci-bandit: ci--meta-debug
|
||||
bandit -r authentik tests lifecycle
|
||||
|
||||
ci-pyright: ci--meta-debug
|
||||
pyright e2e lifecycle
|
||||
|
||||
ci-pending-migrations: ci--meta-debug
|
||||
./manage.py makemigrations --check
|
||||
|
||||
install: web-install website-install
|
||||
poetry install
|
||||
|
||||
a: install
|
||||
tmux \
|
||||
new-session 'make run' \; \
|
||||
split-window 'make web-watch'
|
||||
|
64
Pipfile
64
Pipfile
@ -1,64 +0,0 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[packages]
|
||||
boto3 = "*"
|
||||
celery = "*"
|
||||
channels = "*"
|
||||
channels-redis = "*"
|
||||
dacite = "*"
|
||||
defusedxml = "*"
|
||||
django = "*"
|
||||
django-dbbackup = { git = 'https://github.com/django-dbbackup/django-dbbackup.git', ref = '9d1909c30a3271c8c9c8450add30d6e0b996e145' }
|
||||
django-filter = "*"
|
||||
django-guardian = "*"
|
||||
django-model-utils = "*"
|
||||
django-otp = "*"
|
||||
django-prometheus = "*"
|
||||
django-redis = "*"
|
||||
django-storages = "*"
|
||||
djangorestframework = "*"
|
||||
djangorestframework-guardian = "*"
|
||||
docker = "*"
|
||||
drf-spectacular = "*"
|
||||
facebook-sdk = "*"
|
||||
geoip2 = "*"
|
||||
gunicorn = "*"
|
||||
kubernetes = "==v19.15.0"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
packaging = "*"
|
||||
psycopg2-binary = "*"
|
||||
pycryptodome = "*"
|
||||
pyjwt = "*"
|
||||
pyyaml = "*"
|
||||
requests-oauthlib = "*"
|
||||
sentry-sdk = "*"
|
||||
service_identity = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
twisted = "==21.7.0"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
uvicorn = {extras = ["standard"],version = "*"}
|
||||
webauthn = "*"
|
||||
xmlsec = "*"
|
||||
duo-client = "*"
|
||||
ua-parser = "*"
|
||||
deepmerge = "*"
|
||||
colorama = "*"
|
||||
codespell = "*"
|
||||
|
||||
[dev-packages]
|
||||
bandit = "*"
|
||||
black = "==21.9b0"
|
||||
bump2version = "*"
|
||||
colorama = "*"
|
||||
coverage = {extras = ["toml"],version = "*"}
|
||||
pylint = "*"
|
||||
pylint-django = "*"
|
||||
pytest = "*"
|
||||
pytest-django = "*"
|
||||
selenium = "*"
|
||||
requests-mock = "*"
|
2349
Pipfile.lock
generated
2349
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
22
README.md
22
README.md
@ -9,7 +9,7 @@
|
||||
[](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://goauthentik.testspace.com/)
|
||||
[](https://goauthentik.testspace.com/)
|
||||

|
||||

|
||||
[](https://www.transifex.com/beryjuorg/authentik/)
|
||||
@ -38,3 +38,23 @@ See [Development Documentation](https://goauthentik.io/developer-docs/?utm_sourc
|
||||
## Security
|
||||
|
||||
See [SECURITY.md](SECURITY.md)
|
||||
|
||||
## Sponsors
|
||||
|
||||
This project is proudly sponsored by:
|
||||
|
||||
<p>
|
||||
<a href="https://www.digitalocean.com/?utm_medium=opensource&utm_source=goauthentik.io">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
DigitalOcean provides development and testing resources for authentik.
|
||||
|
||||
<p>
|
||||
<a href="https://www.netlify.com">
|
||||
<img src="https://www.netlify.com/img/global/badges/netlify-color-accent.svg" alt="Deploys by Netlify" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Netlify hosts the [goauthentik.io](https://goauthentik.io) site.
|
||||
|
@ -6,9 +6,9 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | ------------------ |
|
||||
| 2021.8.x | :white_check_mark: |
|
||||
| 2021.9.x | :white_check_mark: |
|
||||
| 2022.4.x | :white_check_mark: |
|
||||
| 2022.5.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, send an email to [security@beryju.org](mailto:security@beryju.org)
|
||||
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||
|
@ -1,3 +1,22 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.10.2"
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2022.6.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
def get_build_hash(fallback: Optional[str] = None) -> str:
|
||||
"""Get build hash"""
|
||||
build_hash = environ.get(ENV_GIT_HASH_KEY, fallback if fallback else "")
|
||||
if build_hash == "" and fallback:
|
||||
return fallback
|
||||
return build_hash
|
||||
|
||||
|
||||
def get_full_version() -> str:
|
||||
"""Get full version, with build hash appended"""
|
||||
version = __version__
|
||||
if (build_hash := get_build_hash()) != "":
|
||||
version += "." + build_hash
|
||||
return version
|
||||
|
@ -1,13 +1,6 @@
|
||||
"""authentik administration metrics"""
|
||||
import time
|
||||
from collections import Counter
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db.models import Count, ExpressionWrapper, F
|
||||
from django.db.models.fields import DurationField
|
||||
from django.db.models.functions import ExtractHour
|
||||
from django.utils.timezone import now
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.fields import IntegerField, SerializerMethodField
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.request import Request
|
||||
@ -15,31 +8,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
|
||||
def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
|
||||
"""Get event count by hour in the last day, fill with zeros"""
|
||||
date_from = now() - timedelta(days=1)
|
||||
result = (
|
||||
Event.objects.filter(created__gte=date_from, **filter_kwargs)
|
||||
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
|
||||
.annotate(age_hours=ExtractHour("age"))
|
||||
.values("age_hours")
|
||||
.annotate(count=Count("pk"))
|
||||
.order_by("age_hours")
|
||||
)
|
||||
data = Counter({int(d["age_hours"]): d["count"] for d in result})
|
||||
results = []
|
||||
_now = now()
|
||||
for hour in range(0, -24, -1):
|
||||
results.append(
|
||||
{
|
||||
"x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
|
||||
"y_cord": data[hour * -1],
|
||||
}
|
||||
)
|
||||
return results
|
||||
from authentik.events.models import EventAction
|
||||
|
||||
|
||||
class CoordinateSerializer(PassiveSerializer):
|
||||
@ -58,12 +27,22 @@ class LoginMetricsSerializer(PassiveSerializer):
|
||||
@extend_schema_field(CoordinateSerializer(many=True))
|
||||
def get_logins_per_1h(self, _):
|
||||
"""Get successful logins per hour for the last 24 hours"""
|
||||
return get_events_per_1h(action=EventAction.LOGIN)
|
||||
user = self.context["user"]
|
||||
return (
|
||||
get_objects_for_user(user, "authentik_events.view_event")
|
||||
.filter(action=EventAction.LOGIN)
|
||||
.get_events_per_hour()
|
||||
)
|
||||
|
||||
@extend_schema_field(CoordinateSerializer(many=True))
|
||||
def get_logins_failed_per_1h(self, _):
|
||||
"""Get failed logins per hour for the last 24 hours"""
|
||||
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
|
||||
user = self.context["user"]
|
||||
return (
|
||||
get_objects_for_user(user, "authentik_events.view_event")
|
||||
.filter(action=EventAction.LOGIN_FAILED)
|
||||
.get_events_per_hour()
|
||||
)
|
||||
|
||||
|
||||
class AdministrationMetricsViewSet(APIView):
|
||||
@ -75,4 +54,5 @@ class AdministrationMetricsViewSet(APIView):
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Login Metrics per 1h"""
|
||||
serializer = LoginMetricsSerializer(True)
|
||||
serializer.context["user"] = request.user
|
||||
return Response(serializer.data)
|
||||
|
@ -86,7 +86,7 @@ class SystemSerializer(PassiveSerializer):
|
||||
def get_embedded_outpost_host(self, request: Request) -> str:
|
||||
"""Get the FQDN configured on the embedded outpost"""
|
||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||
if not outposts.exists():
|
||||
if not outposts.exists(): # pragma: no cover
|
||||
return ""
|
||||
return outposts.first().config.authentik_host
|
||||
|
||||
|
@ -12,10 +12,13 @@ from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class TaskSerializer(PassiveSerializer):
|
||||
"""Serialize TaskInfo and TaskResult"""
|
||||
@ -36,7 +39,7 @@ class TaskSerializer(PassiveSerializer):
|
||||
are pickled in cache. In that case, just delete the info"""
|
||||
try:
|
||||
return super().to_representation(instance)
|
||||
except AttributeError:
|
||||
except AttributeError: # pragma: no cover
|
||||
if isinstance(self.instance, list):
|
||||
for inst in self.instance:
|
||||
inst.delete()
|
||||
@ -89,13 +92,15 @@ class TaskViewSet(ViewSet):
|
||||
try:
|
||||
task_module = import_module(task.task_call_module)
|
||||
task_func = getattr(task_module, task.task_call_func)
|
||||
LOGGER.debug("Running task", task=task_func)
|
||||
task_func.delay(*task.task_call_args, **task.task_call_kwargs)
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Successfully re-scheduled Task %(name)s!" % {"name": task.task_name}),
|
||||
)
|
||||
return Response(status=204)
|
||||
except ImportError: # pragma: no cover
|
||||
except (ImportError, AttributeError): # pragma: no cover
|
||||
LOGGER.warning("Failed to run task, remove state", task=task)
|
||||
# if we get an import error, the module path has probably changed
|
||||
task.delete()
|
||||
return Response(status=500)
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""authentik administration overview"""
|
||||
from os import environ
|
||||
|
||||
from django.core.cache import cache
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from packaging.version import parse
|
||||
@ -10,7 +8,7 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
|
||||
@ -25,7 +23,7 @@ class VersionSerializer(PassiveSerializer):
|
||||
|
||||
def get_build_hash(self, _) -> str:
|
||||
"""Get build hash, if version is not latest or released"""
|
||||
return environ.get(ENV_GIT_HASH_KEY, "")
|
||||
return get_build_hash()
|
||||
|
||||
def get_version_current(self, _) -> str:
|
||||
"""Get current version"""
|
||||
|
@ -23,6 +23,6 @@ class WorkerView(APIView):
|
||||
"""Get currently connected worker count."""
|
||||
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:
|
||||
if settings.DEBUG: # pragma: no cover
|
||||
count += 1
|
||||
return Response({"count": count})
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""authentik admin app config"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
@ -10,6 +12,4 @@ class AuthentikAdminConfig(AppConfig):
|
||||
verbose_name = "authentik Admin"
|
||||
|
||||
def ready(self):
|
||||
from authentik.admin.tasks import clear_update_notifications
|
||||
|
||||
clear_update_notifications.delay()
|
||||
import_module("authentik.admin.signals")
|
||||
|
@ -1,10 +1,12 @@
|
||||
"""authentik admin settings"""
|
||||
from celery.schedules import crontab
|
||||
|
||||
from authentik.lib.utils.time import fqdn_rand
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"admin_latest_version": {
|
||||
"task": "authentik.admin.tasks.update_latest_version",
|
||||
"schedule": crontab(minute="*/60"), # Run every hour
|
||||
"schedule": crontab(minute=fqdn_rand("admin_latest_version"), hour="*"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
}
|
||||
}
|
||||
|
23
authentik/admin/signals.py
Normal file
23
authentik/admin/signals.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""admin signals"""
|
||||
from django.dispatch import receiver
|
||||
|
||||
from authentik.admin.api.tasks import TaskInfo
|
||||
from authentik.admin.api.workers import GAUGE_WORKERS
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.root.monitoring import monitoring_set
|
||||
|
||||
|
||||
@receiver(monitoring_set)
|
||||
# pylint: disable=unused-argument
|
||||
def monitoring_set_workers(sender, **kwargs):
|
||||
"""Set worker gauge"""
|
||||
count = len(CELERY_APP.control.ping(timeout=0.5))
|
||||
GAUGE_WORKERS.set(count)
|
||||
|
||||
|
||||
@receiver(monitoring_set)
|
||||
# pylint: disable=unused-argument
|
||||
def monitoring_set_tasks(sender, **kwargs):
|
||||
"""Set task gauges"""
|
||||
for task in TaskInfo.all().values():
|
||||
task.set_prom_metrics()
|
@ -1,6 +1,5 @@
|
||||
"""authentik admin tasks"""
|
||||
import re
|
||||
from os import environ
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.core.validators import URLValidator
|
||||
@ -9,7 +8,7 @@ from prometheus_client import Info
|
||||
from requests import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.events.models import Event, EventAction, Notification
|
||||
from authentik.events.monitored_tasks import (
|
||||
MonitoredTask,
|
||||
@ -27,6 +26,7 @@ VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours
|
||||
# Chop of the first ^ because we want to search the entire string
|
||||
URL_FINDER = URLValidator.regex.pattern[1:]
|
||||
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
||||
LOCAL_VERSION = parse(__version__)
|
||||
|
||||
|
||||
def _set_prom_info():
|
||||
@ -35,7 +35,7 @@ def _set_prom_info():
|
||||
{
|
||||
"version": __version__,
|
||||
"latest": cache.get(VERSION_CACHE_KEY, ""),
|
||||
"build_hash": environ.get(ENV_GIT_HASH_KEY, ""),
|
||||
"build_hash": get_build_hash(),
|
||||
}
|
||||
)
|
||||
|
||||
@ -48,12 +48,12 @@ def clear_update_notifications():
|
||||
if "new_version" not in notification.event.context:
|
||||
continue
|
||||
notification_version = notification.event.context["new_version"]
|
||||
if notification_version == __version__:
|
||||
if LOCAL_VERSION >= parse(notification_version):
|
||||
notification.delete()
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
@prefill_task
|
||||
def update_latest_version(self: MonitoredTask):
|
||||
"""Update latest version info"""
|
||||
if CONFIG.y_bool("disable_update_check"):
|
||||
@ -74,8 +74,7 @@ def update_latest_version(self: MonitoredTask):
|
||||
_set_prom_info()
|
||||
# Check if upstream version is newer than what we're running,
|
||||
# and if no event exists yet, create one.
|
||||
local_version = parse(__version__)
|
||||
if local_version < parse(upstream_version):
|
||||
if LOCAL_VERSION < parse(upstream_version):
|
||||
# Event has already been created, don't create duplicate
|
||||
if Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE,
|
||||
|
@ -8,6 +8,7 @@ from authentik import __version__
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.tasks import clean_expired_models
|
||||
from authentik.events.monitored_tasks import TaskResultStatus
|
||||
from authentik.managed.tasks import managed_reconcile
|
||||
|
||||
|
||||
class TestAdminAPI(TestCase):
|
||||
@ -94,5 +95,7 @@ class TestAdminAPI(TestCase):
|
||||
|
||||
def test_system(self):
|
||||
"""Test system API"""
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
managed_reconcile() # pylint: disable=no-value-for-parameter
|
||||
response = self.client.get(reverse("authentik_api:admin_system"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -3,8 +3,13 @@ from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||
from authentik.admin.tasks import (
|
||||
VERSION_CACHE_KEY,
|
||||
clear_update_notifications,
|
||||
update_latest_version,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
RESPONSE_VALID = {
|
||||
"$schema": "https://version.goauthentik.io/schema.json",
|
||||
@ -21,7 +26,7 @@ class TestAdminTasks(TestCase):
|
||||
|
||||
def test_version_valid_response(self):
|
||||
"""Test Update checker with valid response"""
|
||||
with Mocker() as mocker:
|
||||
with Mocker() as mocker, CONFIG.patch("disable_update_check", False):
|
||||
mocker.get("https://version.goauthentik.io/version.json", json=RESPONSE_VALID)
|
||||
update_latest_version.delay().get()
|
||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999")
|
||||
@ -56,3 +61,23 @@ class TestAdminTasks(TestCase):
|
||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_version_disabled(self):
|
||||
"""Test Update checker while its disabled"""
|
||||
with CONFIG.patch("disable_update_check", True):
|
||||
update_latest_version.delay().get()
|
||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0")
|
||||
|
||||
def test_clear_update_notifications(self):
|
||||
"""Test clear of previous notification"""
|
||||
Event.objects.create(
|
||||
action=EventAction.UPDATE_AVAILABLE, context={"new_version": "99999999.9999999.9999999"}
|
||||
)
|
||||
Event.objects.create(action=EventAction.UPDATE_AVAILABLE, context={"new_version": "1.1.1"})
|
||||
Event.objects.create(action=EventAction.UPDATE_AVAILABLE, context={})
|
||||
clear_update_notifications()
|
||||
self.assertFalse(
|
||||
Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="1.1"
|
||||
).exists()
|
||||
)
|
||||
|
@ -1,7 +1,5 @@
|
||||
"""API Authentication"""
|
||||
from base64 import b64decode
|
||||
from binascii import Error
|
||||
from typing import Any, Optional, Union
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
@ -16,38 +14,36 @@ from authentik.outposts.models import Outpost
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def bearer_auth(raw_header: bytes) -> Optional[User]:
|
||||
"""raw_header in the Format of `Bearer dGVzdDp0ZXN0`"""
|
||||
auth_credentials = raw_header.decode()
|
||||
def validate_auth(header: bytes) -> str:
|
||||
"""Validate that the header is in a correct format,
|
||||
returns type and credentials"""
|
||||
auth_credentials = header.decode().strip()
|
||||
if auth_credentials == "" or " " not in auth_credentials:
|
||||
return None
|
||||
auth_type, _, auth_credentials = auth_credentials.partition(" ")
|
||||
if auth_type.lower() not in ["basic", "bearer"]:
|
||||
if auth_type.lower() != "bearer":
|
||||
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
|
||||
raise AuthenticationFailed("Unsupported authentication type")
|
||||
password = auth_credentials
|
||||
if auth_type.lower() == "basic":
|
||||
try:
|
||||
auth_credentials = b64decode(auth_credentials.encode()).decode()
|
||||
except (UnicodeDecodeError, Error):
|
||||
raise AuthenticationFailed("Malformed header")
|
||||
# Accept credentials with username and without
|
||||
if ":" in auth_credentials:
|
||||
_, _, password = auth_credentials.partition(":")
|
||||
else:
|
||||
password = auth_credentials
|
||||
if password == "": # nosec
|
||||
if auth_credentials == "": # nosec
|
||||
raise AuthenticationFailed("Malformed header")
|
||||
tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
|
||||
if not tokens.exists():
|
||||
user = token_secret_key(password)
|
||||
if not user:
|
||||
raise AuthenticationFailed("Token invalid/expired")
|
||||
return user
|
||||
return auth_credentials
|
||||
|
||||
|
||||
def bearer_auth(raw_header: bytes) -> Optional[User]:
|
||||
"""raw_header in the Format of `Bearer ....`"""
|
||||
auth_credentials = validate_auth(raw_header)
|
||||
if not auth_credentials:
|
||||
return None
|
||||
# first, check traditional tokens
|
||||
token = Token.filter_not_expired(key=auth_credentials, intent=TokenIntents.INTENT_API).first()
|
||||
if hasattr(LOCAL, "authentik"):
|
||||
LOCAL.authentik[KEY_AUTH_VIA] = "api_token"
|
||||
return tokens.first().user
|
||||
if token:
|
||||
return token.user
|
||||
user = token_secret_key(auth_credentials)
|
||||
if user:
|
||||
return user
|
||||
raise AuthenticationFailed("Token invalid/expired")
|
||||
|
||||
|
||||
def token_secret_key(value: str) -> Optional[User]:
|
||||
@ -69,7 +65,7 @@ def token_secret_key(value: str) -> Optional[User]:
|
||||
class TokenAuthentication(BaseAuthentication):
|
||||
"""Token-based authentication using HTTP Bearer authentication"""
|
||||
|
||||
def authenticate(self, request: Request) -> Union[tuple[User, Any], None]:
|
||||
def authenticate(self, request: Request) -> tuple[User, Any] | None:
|
||||
"""Token-based authentication using HTTP Bearer authentication"""
|
||||
auth = get_authorization_header(request)
|
||||
|
||||
|
@ -12,6 +12,8 @@ class OwnerFilter(BaseFilterBackend):
|
||||
owner_key = "user"
|
||||
|
||||
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
|
||||
if request.user.is_superuser:
|
||||
return queryset
|
||||
return queryset.filter(**{self.owner_key: request.user})
|
||||
|
||||
|
||||
|
@ -8,9 +8,6 @@ API Browser - {{ tenant.branding_title }}
|
||||
|
||||
{% block head %}
|
||||
<script type="module" src="{% static 'dist/rapidoc-min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<script>
|
||||
function getCookie(name) {
|
||||
let cookieValue = "";
|
||||
@ -30,20 +27,62 @@ function getCookie(name) {
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
const rapidocEl = document.querySelector('rapi-doc');
|
||||
rapidocEl.addEventListener('before-try', (e) => {
|
||||
e.detail.request.headers.append('X-CSRFToken', getCookie("authentik_csrf"));
|
||||
e.detail.request.headers.append('X-authentik-CSRF', getCookie("authentik_csrf"));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
img.logo {
|
||||
width: 100%;
|
||||
padding: 1rem 0.5rem 1.5rem 0.5rem;
|
||||
min-height: 48px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<rapi-doc
|
||||
spec-url="{{ path }}"
|
||||
heading-text="authentik"
|
||||
theme="dark"
|
||||
render-style="view"
|
||||
heading-text=""
|
||||
theme="light"
|
||||
render-style="read"
|
||||
default-schema-tab="schema"
|
||||
primary-color="#fd4b2d"
|
||||
nav-bg-color="#212427"
|
||||
bg-color="#000000"
|
||||
text-color="#000000"
|
||||
nav-text-color="#ffffff"
|
||||
nav-hover-bg-color="#3c3f42"
|
||||
nav-accent-color="#4f5255"
|
||||
nav-hover-text-color="#ffffff"
|
||||
use-path-in-nav-bar="true"
|
||||
nav-item-spacing="relaxed"
|
||||
allow-server-selection="false"
|
||||
show-header="false"
|
||||
allow-spec-url-load="false"
|
||||
allow-spec-file-load="false">
|
||||
<div slot="logo">
|
||||
<img src="{% static 'dist/assets/icons/icon.png' %}" style="width:50px; height:50px" />
|
||||
<div slot="nav-logo">
|
||||
<img class="logo" src="{% static 'dist/assets/icons/icon_left_brand.png' %}" />
|
||||
</div>
|
||||
</rapi-doc>
|
||||
<script>
|
||||
const rapidoc = document.querySelector("rapi-doc");
|
||||
const matcher = window.matchMedia("(prefers-color-scheme: light)");
|
||||
const changer = (ev) => {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
let bg, text = "";
|
||||
if (matcher.matches) {
|
||||
bg = style.getPropertyValue('--pf-global--BackgroundColor--light-300');
|
||||
text = style.getPropertyValue('--pf-global--Color--300');
|
||||
} else {
|
||||
bg = style.getPropertyValue('--ak-dark-background');
|
||||
text = style.getPropertyValue('--ak-dark-foreground');
|
||||
}
|
||||
rapidoc.attributes.getNamedItem("bg-color").value = bg.trim();
|
||||
rapidoc.attributes.getNamedItem("text-color").value = text.trim();
|
||||
rapidoc.requestUpdate();
|
||||
};
|
||||
matcher.addEventListener("change", changer);
|
||||
window.addEventListener("load", changer);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -14,12 +14,6 @@ from authentik.outposts.managed import OutpostManager
|
||||
class TestAPIAuth(TestCase):
|
||||
"""Test API Authentication"""
|
||||
|
||||
def test_valid_basic(self):
|
||||
"""Test valid token"""
|
||||
token = Token.objects.create(intent=TokenIntents.INTENT_API, user=get_anonymous_user())
|
||||
auth = b64encode(f":{token.key}".encode()).decode()
|
||||
self.assertEqual(bearer_auth(f"Basic {auth}".encode()), token.user)
|
||||
|
||||
def test_valid_bearer(self):
|
||||
"""Test valid token"""
|
||||
token = Token.objects.create(intent=TokenIntents.INTENT_API, user=get_anonymous_user())
|
||||
@ -30,16 +24,6 @@ class TestAPIAuth(TestCase):
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
bearer_auth("foo bar".encode())
|
||||
|
||||
def test_invalid_decode(self):
|
||||
"""Test invalid bas64"""
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
bearer_auth("Basic bar".encode())
|
||||
|
||||
def test_invalid_empty_password(self):
|
||||
"""Test invalid with empty password"""
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
bearer_auth("Basic :".encode())
|
||||
|
||||
def test_invalid_no_token(self):
|
||||
"""Test invalid with no token"""
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
|
29
authentik/api/tests/test_viewsets.py
Normal file
29
authentik/api/tests/test_viewsets.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""authentik API Modelviewset tests"""
|
||||
from typing import Callable
|
||||
|
||||
from django.test import TestCase
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.api.v3.urls import router
|
||||
|
||||
|
||||
class TestModelViewSets(TestCase):
|
||||
"""Test Viewset"""
|
||||
|
||||
|
||||
def viewset_tester_factory(test_viewset: type[ModelViewSet]) -> Callable:
|
||||
"""Test Viewset"""
|
||||
|
||||
def tester(self: TestModelViewSets):
|
||||
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
|
||||
filterset_class = getattr(test_viewset, "filterset_class", None)
|
||||
if not filterset_class:
|
||||
self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None))
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
for _, viewset, _ in router.registry:
|
||||
if not issubclass(viewset, (ModelViewSet, ReadOnlyModelViewSet)):
|
||||
continue
|
||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}", viewset_tester_factory(viewset))
|
@ -1,18 +0,0 @@
|
||||
"""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}"
|
@ -4,7 +4,5 @@ from django.urls import include, path
|
||||
from authentik.api.v3.urls import urlpatterns as v3_urls
|
||||
|
||||
urlpatterns = [
|
||||
# Remove in 2022.1
|
||||
path("v2beta/", include(v3_urls)),
|
||||
path("v3/", include(v3_urls)),
|
||||
]
|
||||
|
@ -1,11 +1,17 @@
|
||||
"""core Configs API"""
|
||||
from os import environ, path
|
||||
from os import path
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||
from rest_framework.fields import BooleanField, CharField, ChoiceField, IntegerField, ListField
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
ChoiceField,
|
||||
FloatField,
|
||||
IntegerField,
|
||||
ListField,
|
||||
)
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -21,16 +27,22 @@ class Capabilities(models.TextChoices):
|
||||
|
||||
CAN_SAVE_MEDIA = "can_save_media"
|
||||
CAN_GEO_IP = "can_geo_ip"
|
||||
CAN_BACKUP = "can_backup"
|
||||
CAN_IMPERSONATE = "can_impersonate"
|
||||
|
||||
|
||||
class ErrorReportingConfigSerializer(PassiveSerializer):
|
||||
"""Config for error reporting"""
|
||||
|
||||
enabled = BooleanField(read_only=True)
|
||||
environment = CharField(read_only=True)
|
||||
send_pii = BooleanField(read_only=True)
|
||||
traces_sample_rate = FloatField(read_only=True)
|
||||
|
||||
|
||||
class ConfigSerializer(PassiveSerializer):
|
||||
"""Serialize authentik Config into DRF Object"""
|
||||
|
||||
error_reporting_enabled = BooleanField(read_only=True)
|
||||
error_reporting_environment = CharField(read_only=True)
|
||||
error_reporting_send_pii = BooleanField(read_only=True)
|
||||
|
||||
error_reporting = ErrorReportingConfigSerializer(required=True)
|
||||
capabilities = ListField(child=ChoiceField(choices=Capabilities.choices))
|
||||
|
||||
cache_timeout = IntegerField(required=True)
|
||||
@ -52,13 +64,8 @@ class ConfigView(APIView):
|
||||
caps.append(Capabilities.CAN_SAVE_MEDIA)
|
||||
if GEOIP_READER.enabled:
|
||||
caps.append(Capabilities.CAN_GEO_IP)
|
||||
if SERVICE_HOST_ENV_NAME in environ:
|
||||
# Running in k8s, only s3 backup is supported
|
||||
if CONFIG.y("postgresql.s3_backup"):
|
||||
caps.append(Capabilities.CAN_BACKUP)
|
||||
else:
|
||||
# Running in compose, backup is always supported
|
||||
caps.append(Capabilities.CAN_BACKUP)
|
||||
if CONFIG.y_bool("impersonation"):
|
||||
caps.append(Capabilities.CAN_IMPERSONATE)
|
||||
return caps
|
||||
|
||||
@extend_schema(responses={200: ConfigSerializer(many=False)})
|
||||
@ -66,9 +73,12 @@ class ConfigView(APIView):
|
||||
"""Retrieve public configuration options"""
|
||||
config = ConfigSerializer(
|
||||
{
|
||||
"error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
|
||||
"error_reporting_environment": CONFIG.y("error_reporting.environment"),
|
||||
"error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"),
|
||||
"error_reporting": {
|
||||
"enabled": CONFIG.y("error_reporting.enabled") and not settings.DEBUG,
|
||||
"environment": CONFIG.y("error_reporting.environment"),
|
||||
"send_pii": CONFIG.y("error_reporting.send_pii"),
|
||||
"traces_sample_rate": float(CONFIG.y("error_reporting.sample_rate", 0.4)),
|
||||
},
|
||||
"capabilities": self.get_capabilities(),
|
||||
"cache_timeout": int(CONFIG.y("redis.cache_timeout")),
|
||||
"cache_timeout_flows": int(CONFIG.y("redis.cache_timeout_flows")),
|
||||
|
@ -22,11 +22,11 @@ from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSe
|
||||
from authentik.core.api.tokens import TokenViewSet
|
||||
from authentik.core.api.users import UserViewSet
|
||||
from authentik.crypto.api import CertificateKeyPairViewSet
|
||||
from authentik.events.api.event import EventViewSet
|
||||
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_transport import NotificationTransportViewSet
|
||||
from authentik.events.api.events import EventViewSet
|
||||
from authentik.events.api.notification_mappings import NotificationWebhookMappingViewSet
|
||||
from authentik.events.api.notification_rules import NotificationRuleViewSet
|
||||
from authentik.events.api.notification_transports import NotificationTransportViewSet
|
||||
from authentik.events.api.notifications import NotificationViewSet
|
||||
from authentik.flows.api.bindings import FlowStageBindingViewSet
|
||||
from authentik.flows.api.flows import FlowViewSet
|
||||
from authentik.flows.api.stages import StageViewSet
|
||||
@ -46,11 +46,7 @@ from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||
from authentik.policies.expression.api import ExpressionPolicyViewSet
|
||||
from authentik.policies.hibp.api import HaveIBeenPwendPolicyViewSet
|
||||
from authentik.policies.password.api import PasswordPolicyViewSet
|
||||
from authentik.policies.reputation.api import (
|
||||
IPReputationViewSet,
|
||||
ReputationPolicyViewSet,
|
||||
UserReputationViewSet,
|
||||
)
|
||||
from authentik.policies.reputation.api import ReputationPolicyViewSet, ReputationViewSet
|
||||
from authentik.providers.ldap.api import LDAPOutpostConfigViewSet, LDAPProviderViewSet
|
||||
from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet
|
||||
from authentik.providers.oauth2.api.scope import ScopeMappingViewSet
|
||||
@ -151,8 +147,7 @@ router.register("policies/event_matcher", EventMatcherPolicyViewSet)
|
||||
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
|
||||
router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)
|
||||
router.register("policies/password", PasswordPolicyViewSet)
|
||||
router.register("policies/reputation/users", UserReputationViewSet)
|
||||
router.register("policies/reputation/ips", IPReputationViewSet)
|
||||
router.register("policies/reputation/scores", ReputationViewSet)
|
||||
router.register("policies/reputation", ReputationPolicyViewSet)
|
||||
|
||||
router.register("providers/all", ProviderViewSet)
|
||||
|
@ -1,12 +1,15 @@
|
||||
"""Application API Views"""
|
||||
from typing import Optional
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.models import QuerySet
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.fields import ReadOnlyField, SerializerMethodField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -14,14 +17,16 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||
from structlog.stdlib import get_logger
|
||||
from structlog.testing import capture_logs
|
||||
|
||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
from authentik.admin.api.metrics import CoordinateSerializer
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
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.events.models import EventAction
|
||||
from authentik.events.utils import sanitize_dict
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.types import PolicyResult
|
||||
@ -38,11 +43,16 @@ def user_app_cache_key(user_pk: str) -> str:
|
||||
class ApplicationSerializer(ModelSerializer):
|
||||
"""Application Serializer"""
|
||||
|
||||
launch_url = ReadOnlyField(source="get_launch_url")
|
||||
provider_obj = ProviderSerializer(source="get_provider", required=False)
|
||||
launch_url = SerializerMethodField()
|
||||
provider_obj = ProviderSerializer(source="get_provider", required=False, read_only=True)
|
||||
|
||||
meta_icon = ReadOnlyField(source="get_meta_icon")
|
||||
|
||||
def get_launch_url(self, app: Application) -> Optional[str]:
|
||||
"""Allow formatting of launch URL"""
|
||||
user = self.context["request"].user
|
||||
return app.get_launch_url(user)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Application
|
||||
@ -53,11 +63,13 @@ class ApplicationSerializer(ModelSerializer):
|
||||
"provider",
|
||||
"provider_obj",
|
||||
"launch_url",
|
||||
"open_in_new_tab",
|
||||
"meta_launch_url",
|
||||
"meta_icon",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
"policy_engine_mode",
|
||||
"group",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"meta_icon": {"read_only": True},
|
||||
@ -75,8 +87,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
"meta_launch_url",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
"group",
|
||||
]
|
||||
lookup_field = "slug"
|
||||
filterset_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
|
||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
||||
@ -124,12 +138,19 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
return HttpResponseBadRequest("for_user must be numerical")
|
||||
engine = PolicyEngine(application, for_user, request)
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
result = engine.result
|
||||
with capture_logs() as logs:
|
||||
engine.build()
|
||||
result = engine.result
|
||||
response = PolicyTestResultSerializer(PolicyResult(False))
|
||||
if result.passing:
|
||||
response = PolicyTestResultSerializer(PolicyResult(True))
|
||||
if request.user.is_superuser:
|
||||
log_messages = []
|
||||
for log in logs:
|
||||
if log.get("process", "") == "PolicyProcess":
|
||||
continue
|
||||
log_messages.append(sanitize_dict(log))
|
||||
result.log_messages = log_messages
|
||||
response = PolicyTestResultSerializer(result)
|
||||
return Response(response.data)
|
||||
|
||||
@ -239,8 +260,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Metrics for application logins"""
|
||||
app = self.get_object()
|
||||
return Response(
|
||||
get_events_per_1h(
|
||||
get_objects_for_user(request.user, "authentik_events.view_event")
|
||||
.filter(
|
||||
action=EventAction.AUTHORIZE_APPLICATION,
|
||||
context__authorized_application__pk=app.pk.hex,
|
||||
)
|
||||
.get_events_per_hour()
|
||||
)
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""Groups API Viewset"""
|
||||
from json import loads
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django_filters.filters import ModelMultipleChoiceFilter
|
||||
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from rest_framework.fields import CharField, JSONField
|
||||
from rest_framework.serializers import ListSerializer, ModelSerializer
|
||||
from rest_framework.fields import CharField, IntegerField, JSONField
|
||||
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||
|
||||
@ -42,15 +44,20 @@ class GroupSerializer(ModelSerializer):
|
||||
users_obj = ListSerializer(
|
||||
child=GroupMemberSerializer(), read_only=True, source="users", required=False
|
||||
)
|
||||
parent_name = CharField(source="parent.name", read_only=True)
|
||||
|
||||
num_pk = IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Group
|
||||
fields = [
|
||||
"pk",
|
||||
"num_pk",
|
||||
"name",
|
||||
"is_superuser",
|
||||
"parent",
|
||||
"parent_name",
|
||||
"users",
|
||||
"attributes",
|
||||
"users_obj",
|
||||
@ -60,6 +67,13 @@ class GroupSerializer(ModelSerializer):
|
||||
class GroupFilter(FilterSet):
|
||||
"""Filter for groups"""
|
||||
|
||||
attributes = CharFilter(
|
||||
field_name="attributes",
|
||||
lookup_expr="",
|
||||
label="Attributes",
|
||||
method="filter_attributes",
|
||||
)
|
||||
|
||||
members_by_username = ModelMultipleChoiceFilter(
|
||||
field_name="users__username",
|
||||
to_field_name="username",
|
||||
@ -70,10 +84,28 @@ class GroupFilter(FilterSet):
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def filter_attributes(self, queryset, name, value):
|
||||
"""Filter attributes by query args"""
|
||||
try:
|
||||
value = loads(value)
|
||||
except ValueError:
|
||||
raise ValidationError(detail="filter: failed to parse JSON")
|
||||
if not isinstance(value, dict):
|
||||
raise ValidationError(detail="filter: value must be key:value mapping")
|
||||
qs = {}
|
||||
for key, _value in value.items():
|
||||
qs[f"attributes__{key}"] = _value
|
||||
try:
|
||||
_ = len(queryset.filter(**qs))
|
||||
return queryset.filter(**qs)
|
||||
except ValueError:
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Group
|
||||
fields = ["name", "is_superuser", "members_by_pk", "members_by_username"]
|
||||
fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"]
|
||||
|
||||
|
||||
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
|
@ -56,6 +56,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
|
||||
"component",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
]
|
||||
|
||||
|
||||
|
@ -43,6 +43,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"assigned_application_name",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
]
|
||||
|
||||
|
||||
|
@ -8,11 +8,11 @@ from rest_framework.decorators import action
|
||||
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, SerializerMethodField
|
||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField, SerializerMethodField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.models import Source, UserSourceConnection
|
||||
@ -26,6 +26,7 @@ LOGGER = get_logger()
|
||||
class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Source Serializer"""
|
||||
|
||||
managed = ReadOnlyField()
|
||||
component = SerializerMethodField()
|
||||
|
||||
def get_component(self, obj: Source) -> str:
|
||||
@ -48,8 +49,10 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"component",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
"policy_engine_mode",
|
||||
"user_matching_mode",
|
||||
"managed",
|
||||
]
|
||||
|
||||
|
||||
@ -65,6 +68,8 @@ class SourceViewSet(
|
||||
queryset = Source.objects.none()
|
||||
serializer_class = SourceSerializer
|
||||
lookup_field = "slug"
|
||||
search_fields = ["slug", "name"]
|
||||
filterset_fields = ["slug", "name", "managed"]
|
||||
|
||||
def get_queryset(self): # pragma: no cover
|
||||
return Source.objects.select_subclasses()
|
||||
@ -103,14 +108,14 @@ class SourceViewSet(
|
||||
)
|
||||
matching_sources: list[UserSettingSerializer] = []
|
||||
for source in _all_sources:
|
||||
user_settings = source.ui_user_settings
|
||||
user_settings = source.ui_user_settings()
|
||||
if not user_settings:
|
||||
continue
|
||||
policy_engine = PolicyEngine(source, request.user, request)
|
||||
policy_engine.build()
|
||||
if not policy_engine.passing:
|
||||
continue
|
||||
source_settings = source.ui_user_settings
|
||||
source_settings = source.ui_user_settings()
|
||||
source_settings.initial_data["object_uid"] = source.slug
|
||||
if not source_settings.is_valid():
|
||||
LOGGER.warning(source_settings.errors)
|
||||
@ -149,6 +154,6 @@ class UserSourceConnectionViewSet(
|
||||
|
||||
queryset = UserSourceConnection.objects.all()
|
||||
serializer_class = UserSourceConnectionSerializer
|
||||
permission_classes = [OwnerPermissions]
|
||||
permission_classes = [OwnerSuperuserPermissions]
|
||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
ordering = ["pk"]
|
||||
|
@ -1,10 +1,9 @@
|
||||
"""Tokens API Viewset"""
|
||||
from typing import Any
|
||||
|
||||
from django.http.response import Http404
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
||||
from guardian.shortcuts import assign_perm, get_anonymous_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
@ -21,13 +20,14 @@ from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import model_to_dict
|
||||
from authentik.managed.api import ManagedSerializer
|
||||
|
||||
|
||||
class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||
"""Token Serializer"""
|
||||
|
||||
user_obj = UserSerializer(required=False, source="user")
|
||||
user_obj = UserSerializer(required=False, source="user", read_only=True)
|
||||
|
||||
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||
"""Ensure only API or App password tokens are created."""
|
||||
@ -96,10 +96,12 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
def perform_create(self, serializer: TokenSerializer):
|
||||
if not self.request.user.is_superuser:
|
||||
return serializer.save(
|
||||
instance = serializer.save(
|
||||
user=self.request.user,
|
||||
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
|
||||
)
|
||||
assign_perm("authentik_core.view_token_key", self.request.user, instance)
|
||||
return instance
|
||||
return super().perform_create(serializer)
|
||||
|
||||
@permission_required("authentik_core.view_token_key")
|
||||
@ -109,12 +111,39 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
404: OpenApiResponse(description="Token not found or expired"),
|
||||
}
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["GET"])
|
||||
# pylint: disable=unused-argument
|
||||
def view_key(self, request: Request, identifier: str) -> Response:
|
||||
"""Return token key and log access"""
|
||||
token: Token = self.get_object()
|
||||
if token.is_expired:
|
||||
raise Http404
|
||||
Event.new(EventAction.SECRET_VIEW, secret=token).from_http(request) # noqa # nosec
|
||||
return Response(TokenViewSerializer({"key": token.key}).data)
|
||||
|
||||
@permission_required("authentik_core.set_token_key")
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
"TokenSetKey",
|
||||
{
|
||||
"key": CharField(),
|
||||
},
|
||||
),
|
||||
responses={
|
||||
204: OpenApiResponse(description="Successfully changed key"),
|
||||
400: OpenApiResponse(description="Missing key"),
|
||||
404: OpenApiResponse(description="Token not found or expired"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
# pylint: disable=unused-argument
|
||||
def set_key(self, request: Request, identifier: str) -> Response:
|
||||
"""Return token key and log access"""
|
||||
token: Token = self.get_object()
|
||||
key = request.POST.get("key")
|
||||
if not key:
|
||||
return Response(status=400)
|
||||
token.key = key
|
||||
token.save()
|
||||
Event.new(EventAction.MODEL_UPDATED, model=model_to_dict(token)).from_http(
|
||||
request
|
||||
) # noqa # nosec
|
||||
return Response(status=204)
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""User API Views"""
|
||||
from datetime import timedelta
|
||||
from json import loads
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
@ -16,14 +17,14 @@ from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
OpenApiResponse,
|
||||
extend_schema,
|
||||
extend_schema_field,
|
||||
inline_serializer,
|
||||
)
|
||||
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, DictField, JSONField, SerializerMethodField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import (
|
||||
@ -31,22 +32,22 @@ from rest_framework.serializers import (
|
||||
ListSerializer,
|
||||
ModelSerializer,
|
||||
PrimaryKeyRelatedField,
|
||||
Serializer,
|
||||
ValidationError,
|
||||
)
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
from authentik.admin.api.metrics import CoordinateSerializer
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.groups import GroupSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
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_KEY_IMPERSONATE_ORIGINAL_USER,
|
||||
SESSION_KEY_IMPERSONATE_USER,
|
||||
)
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL,
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME,
|
||||
USER_ATTRIBUTE_SA,
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
Group,
|
||||
@ -74,6 +75,7 @@ class UserSerializer(ModelSerializer):
|
||||
)
|
||||
groups_obj = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
|
||||
uid = CharField(read_only=True)
|
||||
username = CharField(max_length=150)
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -98,14 +100,13 @@ class UserSerializer(ModelSerializer):
|
||||
|
||||
|
||||
class UserSelfSerializer(ModelSerializer):
|
||||
"""User Serializer for information a user can retrieve about themselves and
|
||||
update about themselves"""
|
||||
"""User Serializer for information a user can retrieve about themselves"""
|
||||
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
avatar = CharField(read_only=True)
|
||||
groups = SerializerMethodField()
|
||||
uid = CharField(read_only=True)
|
||||
settings = DictField(source="attributes.settings", default=dict)
|
||||
settings = SerializerMethodField()
|
||||
|
||||
@extend_schema_field(
|
||||
ListSerializer(
|
||||
@ -123,21 +124,9 @@ class UserSelfSerializer(ModelSerializer):
|
||||
"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
|
||||
def get_settings(self, user: User) -> dict[str, Any]:
|
||||
"""Get user settings with tenant and group settings applied"""
|
||||
return user.group_attributes(self._context["request"]).get("settings", {})
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -179,19 +168,31 @@ class UserMetricsSerializer(PassiveSerializer):
|
||||
def get_logins_per_1h(self, _):
|
||||
"""Get successful logins per hour for the last 24 hours"""
|
||||
user = self.context["user"]
|
||||
return get_events_per_1h(action=EventAction.LOGIN, user__pk=user.pk)
|
||||
return (
|
||||
get_objects_for_user(user, "authentik_events.view_event")
|
||||
.filter(action=EventAction.LOGIN, user__pk=user.pk)
|
||||
.get_events_per_hour()
|
||||
)
|
||||
|
||||
@extend_schema_field(CoordinateSerializer(many=True))
|
||||
def get_logins_failed_per_1h(self, _):
|
||||
"""Get failed logins per hour for the last 24 hours"""
|
||||
user = self.context["user"]
|
||||
return get_events_per_1h(action=EventAction.LOGIN_FAILED, context__username=user.username)
|
||||
return (
|
||||
get_objects_for_user(user, "authentik_events.view_event")
|
||||
.filter(action=EventAction.LOGIN_FAILED, context__username=user.username)
|
||||
.get_events_per_hour()
|
||||
)
|
||||
|
||||
@extend_schema_field(CoordinateSerializer(many=True))
|
||||
def get_authorizations_per_1h(self, _):
|
||||
"""Get failed logins per hour for the last 24 hours"""
|
||||
user = self.context["user"]
|
||||
return get_events_per_1h(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
|
||||
return (
|
||||
get_objects_for_user(user, "authentik_events.view_event")
|
||||
.filter(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
|
||||
.get_events_per_hour()
|
||||
)
|
||||
|
||||
|
||||
class UsersFilter(FilterSet):
|
||||
@ -205,6 +206,7 @@ class UsersFilter(FilterSet):
|
||||
)
|
||||
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
||||
uuid = CharFilter(field_name="uuid")
|
||||
|
||||
groups_by_name = ModelMultipleChoiceFilter(
|
||||
field_name="ak_groups__name",
|
||||
@ -228,7 +230,11 @@ class UsersFilter(FilterSet):
|
||||
qs = {}
|
||||
for key, _value in value.items():
|
||||
qs[f"attributes__{key}"] = _value
|
||||
return queryset.filter(**qs)
|
||||
try:
|
||||
_ = len(queryset.filter(**qs))
|
||||
return queryset.filter(**qs)
|
||||
except ValueError:
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
@ -250,7 +256,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = User.objects.none()
|
||||
ordering = ["username"]
|
||||
serializer_class = UserSerializer
|
||||
search_fields = ["username", "name", "is_active", "email"]
|
||||
search_fields = ["username", "name", "is_active", "email", "uuid"]
|
||||
filterset_class = UsersFilter
|
||||
|
||||
def get_queryset(self): # pragma: no cover
|
||||
@ -309,7 +315,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
name=username,
|
||||
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
||||
)
|
||||
if create_group:
|
||||
if create_group and self.request.user.has_perm("authentik_core.add_group"):
|
||||
group = Group.objects.create(
|
||||
name=username,
|
||||
)
|
||||
@ -329,34 +335,46 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
# pylint: disable=invalid-name
|
||||
def me(self, request: Request) -> Response:
|
||||
"""Get information about current user"""
|
||||
context = {"request": request}
|
||||
serializer = SessionUserSerializer(
|
||||
data={"user": UserSelfSerializer(instance=request.user).data}
|
||||
data={"user": UserSelfSerializer(instance=request.user, context=context).data}
|
||||
)
|
||||
if SESSION_IMPERSONATE_USER in request._request.session:
|
||||
if SESSION_KEY_IMPERSONATE_USER in request._request.session:
|
||||
serializer.initial_data["original"] = UserSelfSerializer(
|
||||
instance=request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||
instance=request._request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER],
|
||||
context=context,
|
||||
).data
|
||||
self.request.session.save()
|
||||
return Response(serializer.initial_data)
|
||||
|
||||
@extend_schema(request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)})
|
||||
@action(
|
||||
methods=["PUT"],
|
||||
detail=False,
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
permission_classes=[IsAuthenticated],
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
"UserPasswordSetSerializer",
|
||||
{
|
||||
"password": CharField(required=True),
|
||||
},
|
||||
),
|
||||
responses={
|
||||
204: OpenApiResponse(description="Successfully changed password"),
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
},
|
||||
)
|
||||
def update_self(self, request: Request) -> Response:
|
||||
"""Allow users to change information on their own profile"""
|
||||
data = UserSelfSerializer(instance=User.objects.get(pk=request.user.pk), data=request.data)
|
||||
if not data.is_valid():
|
||||
return Response(data.errors, status=400)
|
||||
new_user = data.save()
|
||||
# If we're impersonating, we need to update that user object
|
||||
# since it caches the full object
|
||||
if SESSION_IMPERSONATE_USER in request.session:
|
||||
request.session[SESSION_IMPERSONATE_USER] = new_user
|
||||
return Response({"user": data.data})
|
||||
@action(detail=True, methods=["POST"])
|
||||
# pylint: disable=invalid-name, unused-argument
|
||||
def set_password(self, request: Request, pk: int) -> Response:
|
||||
"""Set password for user"""
|
||||
user: User = self.get_object()
|
||||
try:
|
||||
user.set_password(request.data.get("password"))
|
||||
user.save()
|
||||
except (ValidationError, IntegrityError) as exc:
|
||||
LOGGER.debug("Failed to set password", exc=exc)
|
||||
return Response(status=400)
|
||||
if user.pk == request.user.pk and SESSION_KEY_IMPERSONATE_USER not in self.request.session:
|
||||
LOGGER.debug("Updating session hash after password change")
|
||||
update_session_auth_hash(self.request, user)
|
||||
return Response(status=204)
|
||||
|
||||
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
||||
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
||||
@ -397,8 +415,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
],
|
||||
responses={
|
||||
"204": Serializer(),
|
||||
"404": Serializer(),
|
||||
"204": OpenApiResponse(description="Successfully sent recover email"),
|
||||
"404": OpenApiResponse(description="Bad request"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||
|
@ -41,6 +41,7 @@ class MetaNameSerializer(PassiveSerializer):
|
||||
|
||||
verbose_name = SerializerMethodField()
|
||||
verbose_name_plural = SerializerMethodField()
|
||||
meta_model_name = SerializerMethodField()
|
||||
|
||||
def get_verbose_name(self, obj: Model) -> str:
|
||||
"""Return object's verbose_name"""
|
||||
@ -50,6 +51,10 @@ class MetaNameSerializer(PassiveSerializer):
|
||||
"""Return object's plural verbose_name"""
|
||||
return obj._meta.verbose_name_plural
|
||||
|
||||
def get_meta_model_name(self, obj: Model) -> str:
|
||||
"""Return internal model name"""
|
||||
return f"{obj._meta.app_label}.{obj._meta.model_name}"
|
||||
|
||||
|
||||
class TypeCreateSerializer(PassiveSerializer):
|
||||
"""Types of an object that can be created"""
|
||||
|
@ -2,10 +2,6 @@
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db import ProgrammingError
|
||||
|
||||
from authentik.core.signals import GAUGE_MODELS
|
||||
from authentik.lib.utils.reflection import get_apps
|
||||
|
||||
|
||||
class AuthentikCoreConfig(AppConfig):
|
||||
@ -19,12 +15,3 @@ class AuthentikCoreConfig(AppConfig):
|
||||
def ready(self):
|
||||
import_module("authentik.core.signals")
|
||||
import_module("authentik.core.managed")
|
||||
try:
|
||||
for app in get_apps():
|
||||
for model in app.get_models():
|
||||
GAUGE_MODELS.labels(
|
||||
model_name=model._meta.model_name,
|
||||
app=model._meta.app_label,
|
||||
).set(model.objects.count())
|
||||
except ProgrammingError:
|
||||
pass
|
||||
|
@ -30,7 +30,7 @@ class InbuiltBackend(ModelBackend):
|
||||
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: FlowPlan = request.session.get(SESSION_KEY_PLAN, FlowPlan(""))
|
||||
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
|
||||
@ -49,11 +49,12 @@ class TokenBackend(InbuiltBackend):
|
||||
# difference between an existing and a nonexistent user (#20760).
|
||||
User().set_password(password)
|
||||
return None
|
||||
# pylint: disable=no-member
|
||||
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)
|
||||
self.set_method("token", request, token=token)
|
||||
return token.user
|
||||
|
@ -12,5 +12,6 @@ class CoreManager(ObjectManager):
|
||||
Source,
|
||||
"goauthentik.io/sources/inbuilt",
|
||||
name="authentik Built-in",
|
||||
slug="authentik-built-in",
|
||||
),
|
||||
]
|
||||
|
0
authentik/core/management/commands/__init__.py
Normal file
0
authentik/core/management/commands/__init__.py
Normal file
13
authentik/core/management/commands/bootstrap_tasks.py
Normal file
13
authentik/core/management/commands/bootstrap_tasks.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Run bootstrap tasks"""
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from authentik.root.celery import _get_startup_tasks
|
||||
|
||||
|
||||
class Command(BaseCommand): # pragma: no cover
|
||||
"""Run bootstrap tasks to ensure certain objects are created"""
|
||||
|
||||
def handle(self, **options):
|
||||
tasks = _get_startup_tasks()
|
||||
for task in tasks:
|
||||
task()
|
106
authentik/core/management/commands/shell.py
Normal file
106
authentik/core/management/commands/shell.py
Normal file
@ -0,0 +1,106 @@
|
||||
"""authentik shell command"""
|
||||
import code
|
||||
import platform
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.core.models import User
|
||||
from authentik.events.middleware import IGNORED_MODELS
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import model_to_dict
|
||||
|
||||
BANNER_TEXT = """### authentik shell ({authentik})
|
||||
### Node {node} | Arch {arch} | Python {python} """.format(
|
||||
node=platform.node(),
|
||||
python=platform.python_version(),
|
||||
arch=platform.machine(),
|
||||
authentik=__version__,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand): # pragma: no cover
|
||||
"""Start the Django shell with all authentik models already imported"""
|
||||
|
||||
django_models = {}
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--command",
|
||||
help="Python code to execute (instead of starting an interactive shell)",
|
||||
)
|
||||
|
||||
def get_namespace(self):
|
||||
"""Prepare namespace with all models"""
|
||||
namespace = {}
|
||||
|
||||
# Gather Django models and constants from each app
|
||||
for app in apps.get_app_configs():
|
||||
if not app.name.startswith("authentik"):
|
||||
continue
|
||||
|
||||
# Load models from each app
|
||||
for model in app.get_models():
|
||||
namespace[model.__name__] = model
|
||||
|
||||
return namespace
|
||||
|
||||
@staticmethod
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_handler(sender, instance: Model, created: bool, **_):
|
||||
"""Signal handler for all object's post_save"""
|
||||
if isinstance(instance, IGNORED_MODELS):
|
||||
return
|
||||
|
||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||
Event.new(action, model=model_to_dict(instance)).set_user(
|
||||
User(
|
||||
username="authentik-shell",
|
||||
pk=0,
|
||||
email="",
|
||||
)
|
||||
).save()
|
||||
|
||||
@staticmethod
|
||||
# pylint: disable=unused-argument
|
||||
def pre_delete_handler(sender, instance: Model, **_):
|
||||
"""Signal handler for all object's pre_delete"""
|
||||
if isinstance(instance, IGNORED_MODELS): # pragma: no cover
|
||||
return
|
||||
|
||||
Event.new(EventAction.MODEL_DELETED, model=model_to_dict(instance)).set_user(
|
||||
User(
|
||||
username="authentik-shell",
|
||||
pk=0,
|
||||
email="",
|
||||
)
|
||||
).save()
|
||||
|
||||
def handle(self, **options):
|
||||
namespace = self.get_namespace()
|
||||
|
||||
post_save.connect(Command.post_save_handler)
|
||||
pre_delete.connect(Command.pre_delete_handler)
|
||||
|
||||
# If Python code has been passed, execute it and exit.
|
||||
if options["command"]:
|
||||
# pylint: disable=exec-used
|
||||
exec(options["command"], namespace) # nosec # noqa
|
||||
return
|
||||
|
||||
# Try to enable tab-complete
|
||||
try:
|
||||
import readline
|
||||
import rlcompleter
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
else:
|
||||
readline.set_completer(rlcompleter.Completer(namespace).complete)
|
||||
readline.parse_and_bind("tab: complete")
|
||||
|
||||
# Run interactive shell
|
||||
code.interact(banner=BANNER_TEXT, local=namespace)
|
@ -5,14 +5,14 @@ from typing import Callable
|
||||
from uuid import uuid4
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from sentry_sdk.api import set_tag
|
||||
|
||||
SESSION_IMPERSONATE_USER = "authentik_impersonate_user"
|
||||
SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
|
||||
SESSION_KEY_IMPERSONATE_USER = "authentik/impersonate/user"
|
||||
SESSION_KEY_IMPERSONATE_ORIGINAL_USER = "authentik/impersonate/original_user"
|
||||
LOCAL = local()
|
||||
RESPONSE_HEADER_ID = "X-authentik-id"
|
||||
KEY_AUTH_VIA = "auth_via"
|
||||
KEY_USER = "user"
|
||||
INTERNAL_HEADER_PREFIX = "X-authentik-internal-"
|
||||
|
||||
|
||||
class ImpersonateMiddleware:
|
||||
@ -25,10 +25,10 @@ class ImpersonateMiddleware:
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
# No permission checks are done here, they need to be checked before
|
||||
# SESSION_IMPERSONATE_USER is set.
|
||||
# SESSION_KEY_IMPERSONATE_USER is set.
|
||||
|
||||
if SESSION_IMPERSONATE_USER in request.session:
|
||||
request.user = request.session[SESSION_IMPERSONATE_USER]
|
||||
if SESSION_KEY_IMPERSONATE_USER in request.session:
|
||||
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||
# Ensure that the user is active, otherwise nothing will work
|
||||
request.user.is_active = True
|
||||
|
||||
@ -51,11 +51,12 @@ class RequestIDMiddleware:
|
||||
"request_id": request_id,
|
||||
"host": request.get_host(),
|
||||
}
|
||||
set_tag("authentik.request_id", request_id)
|
||||
response = self.get_response(request)
|
||||
response[RESPONSE_HEADER_ID] = request.request_id
|
||||
if auth_via := LOCAL.authentik.get(KEY_AUTH_VIA, None):
|
||||
response[INTERNAL_HEADER_PREFIX + KEY_AUTH_VIA] = auth_via
|
||||
response[INTERNAL_HEADER_PREFIX + KEY_USER] = request.user.username
|
||||
setattr(response, "ak_context", {})
|
||||
response.ak_context.update(LOCAL.authentik)
|
||||
response.ak_context[KEY_USER] = request.user.username
|
||||
for key in list(LOCAL.authentik.keys()):
|
||||
del LOCAL.authentik[key]
|
||||
return response
|
||||
@ -66,4 +67,6 @@ def structlog_add_request_id(logger: Logger, method_name: str, event_dict: dict)
|
||||
"""If threadlocal has authentik defined, add request_id to log"""
|
||||
if hasattr(LOCAL, "authentik"):
|
||||
event_dict.update(LOCAL.authentik)
|
||||
if hasattr(LOCAL, "authentik_task"):
|
||||
event_dict.update(LOCAL.authentik_task)
|
||||
return event_dict
|
||||
|
@ -20,8 +20,15 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
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
|
||||
password = None
|
||||
if "TF_BUILD" in environ or settings.TEST:
|
||||
password = "akadmin" # noqa # nosec
|
||||
if "AK_ADMIN_PASS" in environ:
|
||||
password = environ["AK_ADMIN_PASS"]
|
||||
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
||||
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
||||
if password:
|
||||
akadmin.set_password(password, signal=False)
|
||||
else:
|
||||
akadmin.set_unusable_password()
|
||||
akadmin.save()
|
||||
|
@ -16,8 +16,15 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
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
|
||||
password = None
|
||||
if "TF_BUILD" in environ or settings.TEST:
|
||||
password = "akadmin" # noqa # nosec
|
||||
if "AK_ADMIN_PASS" in environ:
|
||||
password = environ["AK_ADMIN_PASS"]
|
||||
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
||||
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
||||
if password:
|
||||
akadmin.set_password(password, signal=False)
|
||||
else:
|
||||
akadmin.set_unusable_password()
|
||||
akadmin.save()
|
||||
|
@ -3,7 +3,6 @@
|
||||
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
|
||||
@ -12,10 +11,10 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.models import Count
|
||||
|
||||
import authentik.core.models
|
||||
import authentik.lib.models
|
||||
|
||||
|
||||
def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
|
||||
@ -37,22 +36,29 @@ def fix_duplicates(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
|
||||
|
||||
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
|
||||
from authentik.core.models import TokenIntents
|
||||
|
||||
User = apps.get_model("authentik_core", "User")
|
||||
Token = apps.get_model("authentik_core", "Token")
|
||||
|
||||
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:
|
||||
key = None
|
||||
if "AK_ADMIN_TOKEN" in environ:
|
||||
key = environ["AK_ADMIN_TOKEN"]
|
||||
if "AUTHENTIK_BOOTSTRAP_TOKEN" in environ:
|
||||
key = environ["AUTHENTIK_BOOTSTRAP_TOKEN"]
|
||||
if not key:
|
||||
return
|
||||
Token.objects.using(db_alias).create(
|
||||
identifier="authentik-boostrap-token",
|
||||
identifier="authentik-bootstrap-token",
|
||||
user=akadmin.first(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
expiring=False,
|
||||
key=environ["AK_ADMIN_TOKEN"],
|
||||
key=key,
|
||||
)
|
||||
|
||||
|
||||
@ -161,7 +167,7 @@ class Migration(migrations.Migration):
|
||||
model_name="application",
|
||||
name="meta_launch_url",
|
||||
field=models.TextField(
|
||||
blank=True, default="", validators=[django.core.validators.URLValidator()]
|
||||
blank=True, default="", validators=[authentik.lib.models.DomainlessURLValidator()]
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
|
18
authentik/core/migrations/0019_application_group.py
Normal file
18
authentik/core/migrations/0019_application_group.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.3 on 2022-04-02 19:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0018_auto_20210330_1345_squashed_0028_alter_token_intent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="group",
|
||||
field=models.TextField(blank=True, default=""),
|
||||
),
|
||||
]
|
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.0.5 on 2022-06-04 06:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0019_application_group"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="open_in_new_tab",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Open launch URL in a new browser tab or window."
|
||||
),
|
||||
),
|
||||
]
|
@ -12,7 +12,6 @@ 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
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-02 21:51
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.lib.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@ -17,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
validators=[django.core.validators.URLValidator()],
|
||||
validators=[authentik.lib.models.DomainlessURLValidator()],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -7,22 +7,29 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
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
|
||||
from authentik.core.models import TokenIntents
|
||||
|
||||
User = apps.get_model("authentik_core", "User")
|
||||
Token = apps.get_model("authentik_core", "Token")
|
||||
|
||||
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:
|
||||
key = None
|
||||
if "AK_ADMIN_TOKEN" in environ:
|
||||
key = environ["AK_ADMIN_TOKEN"]
|
||||
if "AUTHENTIK_BOOTSTRAP_TOKEN" in environ:
|
||||
key = environ["AUTHENTIK_BOOTSTRAP_TOKEN"]
|
||||
if not key:
|
||||
return
|
||||
Token.objects.using(db_alias).create(
|
||||
identifier="authentik-boostrap-token",
|
||||
identifier="authentik-bootstrap-token",
|
||||
user=akadmin.first(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
expiring=False,
|
||||
key=environ["AK_ADMIN_TOKEN"],
|
||||
key=key,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,20 +1,20 @@
|
||||
"""authentik core models"""
|
||||
from datetime import timedelta
|
||||
from hashlib import md5, sha256
|
||||
from typing import Any, Optional, Type
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urlencode
|
||||
from uuid import uuid4
|
||||
|
||||
from deepmerge import always_merger
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
from django.core import validators
|
||||
from django.db import models
|
||||
from django.db.models import Q, QuerySet, options
|
||||
from django.http import HttpRequest
|
||||
from django.templatetags.static import static
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.functional import SimpleLazyObject, cached_property
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -26,10 +26,9 @@ from structlog.stdlib import get_logger
|
||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||
from authentik.core.signals import password_changed
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.flows.models import Flow
|
||||
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, DomainlessURLValidator, SerializerModel
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.managed.models import ManagedModel
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
@ -37,9 +36,13 @@ from authentik.policies.models import PolicyBindingModel
|
||||
LOGGER = get_logger()
|
||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
||||
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
|
||||
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
|
||||
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
|
||||
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
|
||||
USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
|
||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
||||
|
||||
@ -59,7 +62,7 @@ def default_token_key():
|
||||
"""Default token key"""
|
||||
# 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)
|
||||
return generate_id(int(CONFIG.y("default_token_length")))
|
||||
|
||||
|
||||
class Group(models.Model):
|
||||
@ -81,6 +84,34 @@ class Group(models.Model):
|
||||
)
|
||||
attributes = models.JSONField(default=dict, blank=True)
|
||||
|
||||
@property
|
||||
def num_pk(self) -> int:
|
||||
"""Get a numerical, int32 ID for the group"""
|
||||
# int max is 2147483647 (10 digits) so 9 is the max usable
|
||||
# in the LDAP Outpost we use the last 5 chars so match here
|
||||
return int(str(self.pk.int)[:5])
|
||||
|
||||
def is_member(self, user: "User") -> bool:
|
||||
"""Recursively check if `user` is member of us, or any parent."""
|
||||
query = """
|
||||
WITH RECURSIVE parents AS (
|
||||
SELECT authentik_core_group.*, 0 AS relative_depth
|
||||
FROM authentik_core_group
|
||||
WHERE authentik_core_group.group_uuid = %s
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT authentik_core_group.*, parents.relative_depth - 1
|
||||
FROM authentik_core_group,parents
|
||||
WHERE authentik_core_group.parent_id = parents.group_uuid
|
||||
)
|
||||
SELECT group_uuid
|
||||
FROM parents
|
||||
GROUP BY group_uuid;
|
||||
"""
|
||||
groups = Group.objects.raw(query, [self.group_uuid])
|
||||
return user.ak_groups.filter(pk__in=[group.pk for group in groups]).exists()
|
||||
|
||||
def __str__(self):
|
||||
return f"Group {self.name}"
|
||||
|
||||
@ -116,10 +147,12 @@ class User(GuardianUserMixin, AbstractUser):
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
def group_attributes(self) -> dict[str, Any]:
|
||||
def group_attributes(self, request: Optional[HttpRequest] = None) -> dict[str, Any]:
|
||||
"""Get a dictionary containing the attributes from all groups the user belongs to,
|
||||
including the users attributes"""
|
||||
final_attributes = {}
|
||||
if request and hasattr(request, "tenant"):
|
||||
always_merger.merge(final_attributes, request.tenant.attributes)
|
||||
for group in self.ak_groups.all().order_by("name"):
|
||||
always_merger.merge(final_attributes, group.attributes)
|
||||
always_merger.merge(final_attributes, self.attributes)
|
||||
@ -135,15 +168,31 @@ class User(GuardianUserMixin, AbstractUser):
|
||||
"""superuser == staff user"""
|
||||
return self.is_superuser # type: ignore
|
||||
|
||||
def set_password(self, password, signal=True):
|
||||
def set_password(self, raw_password, signal=True):
|
||||
if self.pk and signal:
|
||||
password_changed.send(sender=self, user=self, password=password)
|
||||
password_changed.send(sender=self, user=self, password=raw_password)
|
||||
self.password_change_date = now()
|
||||
return super().set_password(password)
|
||||
return super().set_password(raw_password)
|
||||
|
||||
def check_password(self, raw_password: str) -> bool:
|
||||
"""
|
||||
Return a boolean of whether the raw_password was correct. Handles
|
||||
hashing formats behind the scenes.
|
||||
|
||||
Slightly changed version which doesn't send a signal for such internal hash upgrades
|
||||
"""
|
||||
|
||||
def setter(raw_password):
|
||||
self.set_password(raw_password, signal=False)
|
||||
# Password hash upgrades shouldn't be considered password changes.
|
||||
self._password = None
|
||||
self.save(update_fields=["password"])
|
||||
|
||||
return check_password(raw_password, self.password, setter)
|
||||
|
||||
@property
|
||||
def uid(self) -> str:
|
||||
"""Generate a globall unique UID, based on the user ID and the hashed secret key"""
|
||||
"""Generate a globally unique UID, based on the user ID and the hashed secret key"""
|
||||
return sha256(f"{self.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest()
|
||||
|
||||
@property
|
||||
@ -153,7 +202,7 @@ class User(GuardianUserMixin, AbstractUser):
|
||||
if mode == "none":
|
||||
return DEFAULT_AVATAR
|
||||
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
||||
mail_hash = md5(self.email.encode("utf-8")).hexdigest() # nosec
|
||||
mail_hash = md5(self.email.lower().encode("utf-8")).hexdigest() # nosec
|
||||
if mode == "gravatar":
|
||||
parameters = [
|
||||
("s", "158"),
|
||||
@ -183,7 +232,7 @@ class Provider(SerializerModel):
|
||||
name = models.TextField()
|
||||
|
||||
authorization_flow = models.ForeignKey(
|
||||
Flow,
|
||||
"authentik_flows.Flow",
|
||||
on_delete=models.CASCADE,
|
||||
help_text=_("Flow used when authorizing this provider."),
|
||||
related_name="provider_authorization",
|
||||
@ -205,7 +254,7 @@ class Provider(SerializerModel):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def serializer(self) -> Type[Serializer]:
|
||||
def serializer(self) -> type[Serializer]:
|
||||
"""Get serializer for this model"""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -220,13 +269,20 @@ class Application(PolicyBindingModel):
|
||||
|
||||
name = models.TextField(help_text=_("Application's display Name."))
|
||||
slug = models.SlugField(help_text=_("Internal application name, used in URLs."), unique=True)
|
||||
group = models.TextField(blank=True, default="")
|
||||
|
||||
provider = models.OneToOneField(
|
||||
"Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT
|
||||
)
|
||||
|
||||
meta_launch_url = models.TextField(
|
||||
default="", blank=True, validators=[validators.URLValidator()]
|
||||
default="", blank=True, validators=[DomainlessURLValidator()]
|
||||
)
|
||||
|
||||
open_in_new_tab = models.BooleanField(
|
||||
default=False, help_text=_("Open launch URL in a new browser tab or window.")
|
||||
)
|
||||
|
||||
# For template applications, this can be set to /static/authentik/applications/*
|
||||
meta_icon = models.FileField(
|
||||
upload_to="application-icons/",
|
||||
@ -243,23 +299,40 @@ class Application(PolicyBindingModel):
|
||||
it is returned as-is"""
|
||||
if not self.meta_icon:
|
||||
return None
|
||||
if self.meta_icon.name.startswith("http") or self.meta_icon.name.startswith("/static"):
|
||||
if "://" in self.meta_icon.name or self.meta_icon.name.startswith("/static"):
|
||||
return self.meta_icon.name
|
||||
return self.meta_icon.url
|
||||
|
||||
def get_launch_url(self) -> Optional[str]:
|
||||
def get_launch_url(self, user: Optional["User"] = None) -> Optional[str]:
|
||||
"""Get launch URL if set, otherwise attempt to get launch URL based on provider."""
|
||||
url = None
|
||||
if provider := self.get_provider():
|
||||
url = provider.launch_url
|
||||
if self.meta_launch_url:
|
||||
return self.meta_launch_url
|
||||
if self.provider:
|
||||
return self.get_provider().launch_url
|
||||
return None
|
||||
url = self.meta_launch_url
|
||||
if user and url:
|
||||
if isinstance(user, SimpleLazyObject):
|
||||
user._setup()
|
||||
user = user._wrapped
|
||||
try:
|
||||
return url % user.__dict__
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
LOGGER.warning("Failed to format launch url", exc=exc)
|
||||
return url
|
||||
return url
|
||||
|
||||
def get_provider(self) -> Optional[Provider]:
|
||||
"""Get casted provider instance"""
|
||||
if not self.provider:
|
||||
return None
|
||||
return Provider.objects.get_subclass(pk=self.provider.pk)
|
||||
# if the Application class has been cache, self.provider is set
|
||||
# but doing a direct query lookup will fail.
|
||||
# In that case, just return None
|
||||
try:
|
||||
return Provider.objects.get_subclass(pk=self.provider.pk)
|
||||
except Provider.DoesNotExist:
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -304,7 +377,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
|
||||
|
||||
authentication_flow = models.ForeignKey(
|
||||
Flow,
|
||||
"authentik_flows.Flow",
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
@ -313,7 +386,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
related_name="source_authentication",
|
||||
)
|
||||
enrollment_flow = models.ForeignKey(
|
||||
Flow,
|
||||
"authentik_flows.Flow",
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
@ -340,13 +413,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
"""Return component used to edit this object"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def ui_login_button(self) -> Optional[UILoginButton]:
|
||||
def ui_login_button(self, request: HttpRequest) -> Optional[UILoginButton]:
|
||||
"""If source uses a http-based flow, return UI Information about the login
|
||||
button. If source doesn't use http-based flow, return None."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
|
||||
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||
user settings are available, or UserSettingSerializer."""
|
||||
@ -433,6 +504,14 @@ class Token(ManagedModel, ExpiringModel):
|
||||
"""Handler which is called when this object is expired."""
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
if self.intent in [
|
||||
TokenIntents.INTENT_RECOVERY,
|
||||
TokenIntents.INTENT_VERIFICATION,
|
||||
TokenIntents.INTENT_APP_PASSWORD,
|
||||
]:
|
||||
super().expire_action(*args, **kwargs)
|
||||
return
|
||||
|
||||
self.key = default_token_key()
|
||||
self.expires = default_token_duration()
|
||||
self.save(*args, **kwargs)
|
||||
@ -474,7 +553,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def serializer(self) -> Type[Serializer]:
|
||||
def serializer(self) -> type[Serializer]:
|
||||
"""Get serializer for this model"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""authentik core signals"""
|
||||
from typing import TYPE_CHECKING, Type
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
@ -9,12 +9,11 @@ from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.http.request import HttpRequest
|
||||
from prometheus_client import Gauge
|
||||
|
||||
# Arguments: user: User, password: str
|
||||
password_changed = Signal()
|
||||
|
||||
GAUGE_MODELS = Gauge("authentik_models", "Count of various objects", ["model_name", "app"])
|
||||
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
|
||||
login_failed = Signal()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.core.models import AuthenticatedSession, User
|
||||
@ -27,11 +26,6 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
|
||||
from authentik.core.api.applications import user_app_cache_key
|
||||
from authentik.core.models import Application
|
||||
|
||||
GAUGE_MODELS.labels(
|
||||
model_name=sender._meta.model_name,
|
||||
app=sender._meta.app_label,
|
||||
).set(sender.objects.count())
|
||||
|
||||
if sender != Application:
|
||||
return
|
||||
if not created: # pragma: no cover
|
||||
@ -62,7 +56,7 @@ def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
|
||||
|
||||
|
||||
@receiver(pre_delete)
|
||||
def authenticated_session_delete(sender: Type[Model], instance: "AuthenticatedSession", **_):
|
||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||
"""Delete session when authenticated session is deleted"""
|
||||
from authentik.core.models import AuthenticatedSession
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Source decision helper"""
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, Type
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.contrib import messages
|
||||
from django.db import IntegrityError
|
||||
@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
|
||||
from authentik.core.models import Source, SourceUserMatchingModes, User, UserSourceConnection
|
||||
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION, PostUserEnrollmentStage
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import Flow, Stage, in_memory_stage
|
||||
from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_PENDING_USER,
|
||||
@ -24,6 +25,8 @@ from authentik.flows.planner import (
|
||||
)
|
||||
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.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.types import PolicyResult
|
||||
from authentik.policies.utils import delete_none_keys
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
@ -50,7 +53,10 @@ class SourceFlowManager:
|
||||
|
||||
identifier: str
|
||||
|
||||
connection_type: Type[UserSourceConnection] = UserSourceConnection
|
||||
connection_type: type[UserSourceConnection] = UserSourceConnection
|
||||
|
||||
enroll_info: dict[str, Any]
|
||||
policy_context: dict[str, Any]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -64,6 +70,7 @@ class SourceFlowManager:
|
||||
self.identifier = identifier
|
||||
self.enroll_info = enroll_info
|
||||
self._logger = get_logger().bind(source=source, identifier=identifier)
|
||||
self.policy_context = {}
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def get_action(self, **kwargs) -> tuple[Action, Optional[UserSourceConnection]]:
|
||||
@ -144,20 +151,23 @@ class SourceFlowManager:
|
||||
except IntegrityError as exc:
|
||||
self._logger.warning("failed to get action", exc=exc)
|
||||
return redirect("/")
|
||||
self._logger.debug("get_action() says", action=action, connection=connection)
|
||||
if connection:
|
||||
if action == Action.LINK:
|
||||
self._logger.debug("Linking existing user")
|
||||
return self.handle_existing_user_link(connection)
|
||||
if action == Action.AUTH:
|
||||
self._logger.debug("Handling auth user")
|
||||
return self.handle_auth_user(connection)
|
||||
if action == Action.ENROLL:
|
||||
self._logger.debug("Handling enrollment of new user")
|
||||
return self.handle_enroll(connection)
|
||||
self._logger.debug("get_action", action=action, connection=connection)
|
||||
try:
|
||||
if connection:
|
||||
if action == Action.LINK:
|
||||
self._logger.debug("Linking existing user")
|
||||
return self.handle_existing_user_link(connection)
|
||||
if action == Action.AUTH:
|
||||
self._logger.debug("Handling auth user")
|
||||
return self.handle_auth_user(connection)
|
||||
if action == Action.ENROLL:
|
||||
self._logger.debug("Handling enrollment of new user")
|
||||
return self.handle_enroll(connection)
|
||||
except FlowNonApplicableException as exc:
|
||||
self._logger.warning("Flow non applicable", exc=exc)
|
||||
return self.error_handler(exc, exc.policy_result)
|
||||
# Default case, assume deny
|
||||
messages.error(
|
||||
self.request,
|
||||
error = (
|
||||
_(
|
||||
(
|
||||
"Request to authenticate with %(source)s has been denied. Please authenticate "
|
||||
@ -166,7 +176,17 @@ class SourceFlowManager:
|
||||
% {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
return redirect(reverse("authentik_core:root-redirect"))
|
||||
return self.error_handler(error)
|
||||
|
||||
def error_handler(
|
||||
self, error: Exception, policy_result: Optional[PolicyResult] = None
|
||||
) -> HttpResponse:
|
||||
"""Handle any errors by returning an access denied stage"""
|
||||
response = AccessDeniedResponse(self.request)
|
||||
response.error_message = str(error)
|
||||
if policy_result:
|
||||
response.policy_result = policy_result
|
||||
return response
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
|
||||
@ -179,7 +199,9 @@ class SourceFlowManager:
|
||||
]
|
||||
return []
|
||||
|
||||
def _handle_login_flow(self, flow: Flow, **kwargs) -> HttpResponse:
|
||||
def _handle_login_flow(
|
||||
self, flow: Flow, connection: UserSourceConnection, **kwargs
|
||||
) -> HttpResponse:
|
||||
"""Prepare Authentication Plan, redirect user FlowExecutor"""
|
||||
# Ensure redirect is carried through when user was trying to
|
||||
# authorize application
|
||||
@ -193,8 +215,10 @@ class SourceFlowManager:
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_SOURCE: self.source,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
PLAN_CONTEXT_SOURCES_CONNECTION: connection,
|
||||
}
|
||||
)
|
||||
kwargs.update(self.policy_context)
|
||||
if not flow:
|
||||
return HttpResponseBadRequest()
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
@ -220,7 +244,7 @@ class SourceFlowManager:
|
||||
_("Successfully authenticated with %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user}
|
||||
return self._handle_login_flow(self.source.authentication_flow, **flow_kwargs)
|
||||
return self._handle_login_flow(self.source.authentication_flow, connection, **flow_kwargs)
|
||||
|
||||
def handle_existing_user_link(
|
||||
self,
|
||||
@ -264,8 +288,8 @@ class SourceFlowManager:
|
||||
return HttpResponseBadRequest()
|
||||
return self._handle_login_flow(
|
||||
self.source.enrollment_flow,
|
||||
connection,
|
||||
**{
|
||||
PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info),
|
||||
PLAN_CONTEXT_SOURCES_CONNECTION: connection,
|
||||
},
|
||||
)
|
||||
|
@ -1,35 +1,31 @@
|
||||
"""authentik core tasks"""
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
from os import environ
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from boto3.exceptions import Boto3Error
|
||||
from botocore.exceptions import BotoCoreError, ClientError
|
||||
from dbbackup.db.exceptions import CommandConnectorError
|
||||
from django.conf import settings
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core import management
|
||||
from django.core.cache import cache
|
||||
from django.utils.timezone import now
|
||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, ExpiringModel
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_EXPIRES,
|
||||
USER_ATTRIBUTE_GENERATED,
|
||||
AuthenticatedSession,
|
||||
ExpiringModel,
|
||||
User,
|
||||
)
|
||||
from authentik.events.monitored_tasks import (
|
||||
MonitoredTask,
|
||||
TaskResult,
|
||||
TaskResultStatus,
|
||||
prefill_task,
|
||||
)
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
@prefill_task
|
||||
def clean_expired_models(self: MonitoredTask):
|
||||
"""Remove expired objects"""
|
||||
messages = []
|
||||
@ -38,9 +34,9 @@ def clean_expired_models(self: MonitoredTask):
|
||||
objects = (
|
||||
cls.objects.all().exclude(expiring=False).exclude(expiring=True, expires__gt=now())
|
||||
)
|
||||
amount = objects.count()
|
||||
for obj in objects:
|
||||
obj.expire_action()
|
||||
amount = objects.count()
|
||||
LOGGER.debug("Expired models", model=cls, amount=amount)
|
||||
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
|
||||
# Special case
|
||||
@ -56,46 +52,22 @@ def clean_expired_models(self: MonitoredTask):
|
||||
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)
|
||||
@prefill_task()
|
||||
def backup_database(self: MonitoredTask): # pragma: no cover
|
||||
"""Database backup"""
|
||||
self.result_timeout_hours = 25
|
||||
if not should_backup():
|
||||
self.set_status(TaskResult(TaskResultStatus.UNKNOWN, ["Backups are not configured."]))
|
||||
return
|
||||
try:
|
||||
start = datetime.now()
|
||||
out = StringIO()
|
||||
management.call_command("dbbackup", quiet=True, stdout=out)
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL,
|
||||
[
|
||||
f"Successfully finished database backup {naturaltime(start)} {out.getvalue()}",
|
||||
],
|
||||
)
|
||||
@prefill_task
|
||||
def clean_temporary_users(self: MonitoredTask):
|
||||
"""Remove temporary users created by SAML Sources"""
|
||||
_now = datetime.now()
|
||||
messages = []
|
||||
deleted_users = 0
|
||||
for user in User.objects.filter(**{f"attributes__{USER_ATTRIBUTE_GENERATED}": True}):
|
||||
if not user.attributes.get(USER_ATTRIBUTE_EXPIRES):
|
||||
continue
|
||||
delta: timedelta = _now - datetime.fromtimestamp(
|
||||
user.attributes.get(USER_ATTRIBUTE_EXPIRES)
|
||||
)
|
||||
LOGGER.info("Successfully backed up database.")
|
||||
except (
|
||||
IOError,
|
||||
BotoCoreError,
|
||||
ClientError,
|
||||
Boto3Error,
|
||||
PermissionError,
|
||||
CommandConnectorError,
|
||||
ValueError,
|
||||
) as exc:
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
if delta.total_seconds() > 0:
|
||||
LOGGER.debug("User is expired and will be deleted.", user=user, delta=delta)
|
||||
user.delete()
|
||||
deleted_users += 1
|
||||
messages.append(f"Successfully deleted {deleted_users} users.")
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
|
||||
|
@ -16,9 +16,11 @@
|
||||
{% block head_before %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}">
|
||||
<script src="{% static 'dist/poly.js' %}" type="module"></script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
|
@ -4,12 +4,14 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/AdminInterface.js' %}" type="module"></script>
|
||||
<script src="{% static 'dist/admin/AdminInterface.js' %}" type="module"></script>
|
||||
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-interface-admin>
|
||||
<ak-message-container data-refresh-on-locale="true"></ak-message-container>
|
||||
<ak-interface-admin data-refresh-on-locale="true">
|
||||
<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">
|
||||
|
@ -5,23 +5,30 @@
|
||||
|
||||
{% block head_before %}
|
||||
{{ block.super }}
|
||||
<link rel="prefetch" href="{{ flow.background_url }}" />
|
||||
{% if flow.compatibility_mode and not inspector %}
|
||||
<script>ShadyDOM = { force: !navigator.webdriver };</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
window.authentik = {};
|
||||
window.authentik.flow = {
|
||||
"layout": "{{ flow.layout }}",
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/FlowInterface.js' %}" type="module"></script>
|
||||
<script src="{% static 'dist/flow/FlowInterface.js' %}" type="module"></script>
|
||||
<style>
|
||||
.pf-c-background-image::before {
|
||||
:root {
|
||||
--ak-flow-background: url("{{ flow.background_url }}");
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-flow-executor>
|
||||
<ak-message-container data-refresh-on-locale="true"></ak-message-container>
|
||||
<ak-flow-executor data-refresh-on-locale="true">
|
||||
<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">
|
||||
|
@ -4,12 +4,14 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/UserInterface.js' %}" type="module"></script>
|
||||
<script src="{% static 'dist/user/UserInterface.js' %}" type="module"></script>
|
||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-interface-user>
|
||||
<ak-message-container data-refresh-on-locale="true"></ak-message-container>
|
||||
<ak-interface-user data-refresh-on-locale="true">
|
||||
<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">
|
||||
|
@ -4,13 +4,38 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_before %}
|
||||
<link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.pf-c-background-image::before {
|
||||
:root {
|
||||
--ak-flow-background: url("/static/dist/assets/images/flow_background.jpg");
|
||||
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage--sm-2x: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage--lg: var(--ak-flow-background);
|
||||
}
|
||||
/* Form with user */
|
||||
.form-control-static {
|
||||
margin-top: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.form-control-static .avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-control-static img {
|
||||
margin-right: var(--pf-global--spacer--xs);
|
||||
}
|
||||
.form-control-static a {
|
||||
padding-top: var(--pf-global--spacer--xs);
|
||||
padding-bottom: var(--pf-global--spacer--xs);
|
||||
line-height: var(--pf-global--spacer--xl);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -59,13 +84,11 @@
|
||||
<a href="{{ link.href }}">{{ link.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if tenant.branding_title != "authentik" %}
|
||||
<li>
|
||||
<a href="https://goauthentik.io?utm_source=authentik">
|
||||
{% trans 'Powered by authentik' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
@ -1,19 +1,37 @@
|
||||
"""Test Applications API"""
|
||||
from json import loads
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
|
||||
|
||||
class TestApplicationsAPI(APITestCase):
|
||||
"""Test applications API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.allowed = Application.objects.create(name="allowed", slug="allowed")
|
||||
self.user = create_test_admin_user()
|
||||
self.provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
redirect_uris="http://some-other-domain",
|
||||
authorization_flow=Flow.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
||||
),
|
||||
)
|
||||
self.allowed = Application.objects.create(
|
||||
name="allowed",
|
||||
slug="allowed",
|
||||
meta_launch_url="https://goauthentik.io/%(username)s",
|
||||
open_in_new_tab=True,
|
||||
provider=self.provider,
|
||||
)
|
||||
self.denied = Application.objects.create(name="denied", slug="denied")
|
||||
PolicyBinding.objects.create(
|
||||
target=self.denied,
|
||||
@ -31,7 +49,10 @@ class TestApplicationsAPI(APITestCase):
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(force_str(response.content), {"messages": [], "passing": True})
|
||||
body = loads(response.content.decode())
|
||||
self.assertEqual(body["passing"], True)
|
||||
self.assertEqual(body["messages"], [])
|
||||
self.assertEqual(len(body["log_messages"]), 0)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:application-check-access",
|
||||
@ -39,14 +60,16 @@ class TestApplicationsAPI(APITestCase):
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(force_str(response.content), {"messages": ["dummy"], "passing": False})
|
||||
body = loads(response.content.decode())
|
||||
self.assertEqual(body["passing"], False)
|
||||
self.assertEqual(body["messages"], ["dummy"])
|
||||
|
||||
def test_list(self):
|
||||
"""Test list operation without superuser_full_list"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("authentik_api:application-list"))
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
response.content.decode(),
|
||||
{
|
||||
"pagination": {
|
||||
"next": 0,
|
||||
@ -62,10 +85,23 @@ class TestApplicationsAPI(APITestCase):
|
||||
"pk": str(self.allowed.pk),
|
||||
"name": "allowed",
|
||||
"slug": "allowed",
|
||||
"provider": None,
|
||||
"provider_obj": None,
|
||||
"launch_url": None,
|
||||
"meta_launch_url": "",
|
||||
"group": "",
|
||||
"provider": self.provider.pk,
|
||||
"provider_obj": {
|
||||
"assigned_application_name": "allowed",
|
||||
"assigned_application_slug": "allowed",
|
||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||
"component": "ak-provider-oauth2-form",
|
||||
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
||||
"name": self.provider.name,
|
||||
"pk": self.provider.pk,
|
||||
"property_mappings": [],
|
||||
"verbose_name": "OAuth2/OpenID Provider",
|
||||
"verbose_name_plural": "OAuth2/OpenID Providers",
|
||||
},
|
||||
"launch_url": f"https://goauthentik.io/{self.user.username}",
|
||||
"meta_launch_url": "https://goauthentik.io/%(username)s",
|
||||
"open_in_new_tab": True,
|
||||
"meta_icon": None,
|
||||
"meta_description": "",
|
||||
"meta_publisher": "",
|
||||
@ -82,7 +118,7 @@ class TestApplicationsAPI(APITestCase):
|
||||
reverse("authentik_api:application-list") + "?superuser_full_list=true"
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
response.content.decode(),
|
||||
{
|
||||
"pagination": {
|
||||
"next": 0,
|
||||
@ -98,10 +134,23 @@ class TestApplicationsAPI(APITestCase):
|
||||
"pk": str(self.allowed.pk),
|
||||
"name": "allowed",
|
||||
"slug": "allowed",
|
||||
"provider": None,
|
||||
"provider_obj": None,
|
||||
"launch_url": None,
|
||||
"meta_launch_url": "",
|
||||
"group": "",
|
||||
"provider": self.provider.pk,
|
||||
"provider_obj": {
|
||||
"assigned_application_name": "allowed",
|
||||
"assigned_application_slug": "allowed",
|
||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||
"component": "ak-provider-oauth2-form",
|
||||
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
||||
"name": self.provider.name,
|
||||
"pk": self.provider.pk,
|
||||
"property_mappings": [],
|
||||
"verbose_name": "OAuth2/OpenID Provider",
|
||||
"verbose_name_plural": "OAuth2/OpenID Providers",
|
||||
},
|
||||
"launch_url": f"https://goauthentik.io/{self.user.username}",
|
||||
"meta_launch_url": "https://goauthentik.io/%(username)s",
|
||||
"open_in_new_tab": True,
|
||||
"meta_icon": None,
|
||||
"meta_description": "",
|
||||
"meta_publisher": "",
|
||||
@ -112,7 +161,9 @@ class TestApplicationsAPI(APITestCase):
|
||||
"meta_description": "",
|
||||
"meta_icon": None,
|
||||
"meta_launch_url": "",
|
||||
"open_in_new_tab": False,
|
||||
"meta_publisher": "",
|
||||
"group": "",
|
||||
"name": "denied",
|
||||
"pk": str(self.denied.pk),
|
||||
"policy_engine_mode": "any",
|
||||
|
67
authentik/core/tests/test_applications_views.py
Normal file
67
authentik/core/tests/test_applications_views.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Test Applications API"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_tenant
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
class TestApplicationsViews(FlowTestCase):
|
||||
"""Test applications Views"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_admin_user()
|
||||
self.allowed = Application.objects.create(
|
||||
name="allowed", slug="allowed", meta_launch_url="https://goauthentik.io/%(username)s"
|
||||
)
|
||||
|
||||
def test_check_redirect(self):
|
||||
"""Test redirect"""
|
||||
empty_flow = Flow.objects.create(
|
||||
name="foo",
|
||||
slug="foo",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_authentication = empty_flow
|
||||
tenant.save()
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_core:application-launch",
|
||||
kwargs={"application_slug": self.allowed.slug},
|
||||
),
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
with patch(
|
||||
"authentik.flows.stage.StageView.get_pending_user", MagicMock(return_value=self.user)
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": empty_flow.slug})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(response, f"https://goauthentik.io/{self.user.username}")
|
||||
|
||||
def test_check_redirect_auth(self):
|
||||
"""Test redirect"""
|
||||
self.client.force_login(self.user)
|
||||
empty_flow = Flow.objects.create(
|
||||
name="foo",
|
||||
slug="foo",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_authentication = empty_flow
|
||||
tenant.save()
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_core:application-launch",
|
||||
kwargs={"application_slug": self.allowed.slug},
|
||||
),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, f"https://goauthentik.io/{self.user.username}")
|
@ -2,10 +2,10 @@
|
||||
from json import loads
|
||||
|
||||
from django.urls.base import reverse
|
||||
from django.utils.encoding import force_str
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestAuthenticatedSessionsAPI(APITestCase):
|
||||
@ -13,7 +13,7 @@ class TestAuthenticatedSessionsAPI(APITestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.other_user = User.objects.create(username="normal-user")
|
||||
|
||||
def test_list(self):
|
||||
@ -27,5 +27,5 @@ class TestAuthenticatedSessionsAPI(APITestCase):
|
||||
self.client.force_login(self.other_user)
|
||||
response = self.client.get(reverse("authentik_api:authenticatedsession-list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(force_str(response.content))
|
||||
body = loads(response.content.decode())
|
||||
self.assertEqual(body["pagination"]["count"], 1)
|
||||
|
40
authentik/core/tests/test_groups.py
Normal file
40
authentik/core/tests/test_groups.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""group tests"""
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
|
||||
|
||||
class TestGroups(TestCase):
|
||||
"""Test group membership"""
|
||||
|
||||
def test_group_membership_simple(self):
|
||||
"""Test simple membership"""
|
||||
user = User.objects.create(username="user")
|
||||
user2 = User.objects.create(username="user2")
|
||||
group = Group.objects.create(name="group")
|
||||
group.users.add(user)
|
||||
self.assertTrue(group.is_member(user))
|
||||
self.assertFalse(group.is_member(user2))
|
||||
|
||||
def test_group_membership_parent(self):
|
||||
"""Test parent membership"""
|
||||
user = User.objects.create(username="user")
|
||||
user2 = User.objects.create(username="user2")
|
||||
first = Group.objects.create(name="first")
|
||||
second = Group.objects.create(name="second", parent=first)
|
||||
second.users.add(user)
|
||||
self.assertTrue(first.is_member(user))
|
||||
self.assertFalse(first.is_member(user2))
|
||||
|
||||
def test_group_membership_parent_extra(self):
|
||||
"""Test parent membership"""
|
||||
user = User.objects.create(username="user")
|
||||
user2 = User.objects.create(username="user2")
|
||||
first = Group.objects.create(name="first")
|
||||
second = Group.objects.create(name="second", parent=first)
|
||||
third = Group.objects.create(name="third", parent=second)
|
||||
second.users.add(user)
|
||||
self.assertTrue(first.is_member(user))
|
||||
self.assertFalse(first.is_member(user2))
|
||||
self.assertFalse(third.is_member(user))
|
||||
self.assertFalse(third.is_member(user2))
|
@ -5,6 +5,7 @@ from django.test.testcases import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestImpersonation(TestCase):
|
||||
@ -13,14 +14,14 @@ class TestImpersonation(TestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.other_user = User.objects.create(username="to-impersonate")
|
||||
self.akadmin = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
|
||||
def test_impersonate_simple(self):
|
||||
"""test simple impersonation and un-impersonation"""
|
||||
# test with an inactive user to ensure that still works
|
||||
self.other_user.is_active = False
|
||||
self.other_user.save()
|
||||
self.client.force_login(self.akadmin)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.client.get(
|
||||
reverse(
|
||||
@ -32,13 +33,13 @@ class TestImpersonation(TestCase):
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
response_body = loads(response.content.decode())
|
||||
self.assertEqual(response_body["user"]["username"], self.other_user.username)
|
||||
self.assertEqual(response_body["original"]["username"], self.akadmin.username)
|
||||
self.assertEqual(response_body["original"]["username"], self.user.username)
|
||||
|
||||
self.client.get(reverse("authentik_core:impersonate-end"))
|
||||
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
response_body = loads(response.content.decode())
|
||||
self.assertEqual(response_body["user"]["username"], self.akadmin.username)
|
||||
self.assertEqual(response_body["user"]["username"], self.user.username)
|
||||
self.assertNotIn("original", response_body)
|
||||
|
||||
def test_impersonate_denied(self):
|
||||
@ -46,7 +47,7 @@ class TestImpersonation(TestCase):
|
||||
self.client.force_login(self.other_user)
|
||||
|
||||
self.client.get(
|
||||
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.akadmin.pk})
|
||||
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""authentik core models tests"""
|
||||
from time import sleep
|
||||
from typing import Callable, Type
|
||||
from typing import Callable
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
@ -27,9 +27,12 @@ class TestModels(TestCase):
|
||||
self.assertFalse(token.is_expired)
|
||||
|
||||
|
||||
def source_tester_factory(test_model: Type[Stage]) -> Callable:
|
||||
def source_tester_factory(test_model: type[Stage]) -> Callable:
|
||||
"""Test source"""
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.get("/")
|
||||
|
||||
def tester(self: TestModels):
|
||||
model_class = None
|
||||
if test_model._meta.abstract:
|
||||
@ -38,18 +41,18 @@ def source_tester_factory(test_model: Type[Stage]) -> Callable:
|
||||
model_class = test_model()
|
||||
model_class.slug = "test"
|
||||
self.assertIsNotNone(model_class.component)
|
||||
_ = model_class.ui_login_button
|
||||
_ = model_class.ui_user_settings
|
||||
_ = model_class.ui_login_button(request)
|
||||
_ = model_class.ui_user_settings()
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
def provider_tester_factory(test_model: Type[Stage]) -> Callable:
|
||||
def provider_tester_factory(test_model: type[Stage]) -> Callable:
|
||||
"""Test provider"""
|
||||
|
||||
def tester(self: TestModels):
|
||||
model_class = None
|
||||
if test_model._meta.abstract:
|
||||
if test_model._meta.abstract: # pragma: no cover
|
||||
model_class = test_model.__bases__[0]()
|
||||
else:
|
||||
model_class = test_model()
|
||||
@ -59,6 +62,6 @@ def provider_tester_factory(test_model: Type[Stage]) -> Callable:
|
||||
|
||||
|
||||
for model in all_subclasses(Source):
|
||||
setattr(TestModels, f"test_model_{model.__name__}", source_tester_factory(model))
|
||||
setattr(TestModels, f"test_source_{model.__name__}", source_tester_factory(model))
|
||||
for model in all_subclasses(Provider):
|
||||
setattr(TestModels, f"test_model_{model.__name__}", provider_tester_factory(model))
|
||||
setattr(TestModels, f"test_provider_{model.__name__}", provider_tester_factory(model))
|
||||
|
@ -6,7 +6,8 @@ from rest_framework.serializers import ValidationError
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||
from authentik.core.models import PropertyMapping, User
|
||||
from authentik.core.models import PropertyMapping
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestPropertyMappingAPI(APITestCase):
|
||||
@ -17,7 +18,7 @@ class TestPropertyMappingAPI(APITestCase):
|
||||
self.mapping = PropertyMapping.objects.create(
|
||||
name="dummy", expression="""return {'foo': 'bar'}"""
|
||||
)
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_test_call(self):
|
||||
@ -40,7 +41,7 @@ class TestPropertyMappingAPI(APITestCase):
|
||||
expr = "return True"
|
||||
self.assertEqual(PropertyMappingSerializer().validate_expression(expr), expr)
|
||||
with self.assertRaises(ValidationError):
|
||||
print(PropertyMappingSerializer().validate_expression("/"))
|
||||
PropertyMappingSerializer().validate_expression("/")
|
||||
|
||||
def test_types(self):
|
||||
"""Test PropertyMappigns's types endpoint"""
|
||||
|
@ -2,7 +2,8 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import PropertyMapping, User
|
||||
from authentik.core.models import PropertyMapping
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestProvidersAPI(APITestCase):
|
||||
@ -13,7 +14,7 @@ class TestProvidersAPI(APITestCase):
|
||||
self.mapping = PropertyMapping.objects.create(
|
||||
name="dummy", expression="""return {'foo': 'bar'}"""
|
||||
)
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_types(self):
|
||||
|
@ -6,8 +6,12 @@ from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import SourceUserMatchingModes, User
|
||||
from authentik.core.sources.flow_manager import Action
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.expression.models import ExpressionPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.views.callback import OAuthSourceFlowManager
|
||||
|
||||
@ -17,7 +21,7 @@ class TestSourceFlowManager(TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.source = OAuthSource.objects.create(name="test")
|
||||
self.source: OAuthSource = OAuthSource.objects.create(name="test")
|
||||
self.factory = RequestFactory()
|
||||
self.identifier = generate_id()
|
||||
|
||||
@ -143,3 +147,34 @@ class TestSourceFlowManager(TestCase):
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.ENROLL)
|
||||
flow_manager.get_flow()
|
||||
|
||||
def test_error_non_applicable_flow(self):
|
||||
"""Test error handling when a source selected flow is non-applicable due to a policy"""
|
||||
self.source.user_matching_mode = SourceUserMatchingModes.USERNAME_LINK
|
||||
|
||||
flow = Flow.objects.create(
|
||||
name="test", slug="test", title="test", designation=FlowDesignation.ENROLLMENT
|
||||
)
|
||||
policy = ExpressionPolicy.objects.create(
|
||||
name="false", expression="""ak_message("foo");return False"""
|
||||
)
|
||||
PolicyBinding.objects.create(
|
||||
policy=policy,
|
||||
target=flow,
|
||||
order=0,
|
||||
)
|
||||
self.source.enrollment_flow = flow
|
||||
self.source.save()
|
||||
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.ENROLL)
|
||||
response = flow_manager.get_flow()
|
||||
self.assertIsInstance(response, AccessDeniedResponse)
|
||||
# pylint: disable=no-member
|
||||
self.assertEqual(response.error_message, "foo")
|
||||
|
50
authentik/core/tests/test_tasks.py
Normal file
50
authentik/core/tests/test_tasks.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""Test tasks"""
|
||||
from time import mktime
|
||||
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_EXPIRES,
|
||||
USER_ATTRIBUTE_GENERATED,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
)
|
||||
from authentik.core.tasks import clean_expired_models, clean_temporary_users
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
class TestTasks(APITestCase):
|
||||
"""Test token API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user = User.objects.create(username="testuser")
|
||||
self.admin = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_token_expire(self):
|
||||
"""Test Token expire task"""
|
||||
token: Token = Token.objects.create(
|
||||
expires=now(), user=get_anonymous_user(), intent=TokenIntents.INTENT_API
|
||||
)
|
||||
key = token.key
|
||||
clean_expired_models.delay().get()
|
||||
token.refresh_from_db()
|
||||
self.assertNotEqual(key, token.key)
|
||||
|
||||
def test_clean_temporary_users(self):
|
||||
"""Test clean_temporary_users task"""
|
||||
username = generate_id
|
||||
User.objects.create(
|
||||
username=username,
|
||||
attributes={
|
||||
USER_ATTRIBUTE_GENERATED: True,
|
||||
USER_ATTRIBUTE_EXPIRES: mktime(now().timetuple()),
|
||||
},
|
||||
)
|
||||
clean_temporary_users.delay().get()
|
||||
self.assertFalse(User.objects.filter(username=username))
|
@ -2,12 +2,11 @@
|
||||
from json import loads
|
||||
|
||||
from django.urls.base import reverse
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
|
||||
from authentik.core.tasks import clean_expired_models
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestTokenAPI(APITestCase):
|
||||
@ -16,7 +15,7 @@ class TestTokenAPI(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user = User.objects.create(username="testuser")
|
||||
self.admin = User.objects.get(username="akadmin")
|
||||
self.admin = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_token_create(self):
|
||||
@ -29,6 +28,7 @@ class TestTokenAPI(APITestCase):
|
||||
self.assertEqual(token.user, self.user)
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, True)
|
||||
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
|
||||
|
||||
def test_token_create_invalid(self):
|
||||
"""Test token creation endpoint (invalid data)"""
|
||||
@ -51,14 +51,6 @@ class TestTokenAPI(APITestCase):
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, False)
|
||||
|
||||
def test_token_expire(self):
|
||||
"""Test Token expire task"""
|
||||
token: Token = Token.objects.create(expires=now(), user=get_anonymous_user())
|
||||
key = token.key
|
||||
clean_expired_models.delay().get()
|
||||
token.refresh_from_db()
|
||||
self.assertNotEqual(key, token.key)
|
||||
|
||||
def test_list(self):
|
||||
"""Test Token List (Test normal authentication)"""
|
||||
token_should: Token = Token.objects.create(
|
||||
|
@ -2,8 +2,10 @@
|
||||
from django.urls.base import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import USER_ATTRIBUTE_CHANGE_EMAIL, USER_ATTRIBUTE_CHANGE_USERNAME, User
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
@ -12,37 +14,9 @@ class TestUsersAPI(APITestCase):
|
||||
"""Test Users API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.admin = User.objects.get(username="akadmin")
|
||||
self.admin = create_test_admin_user()
|
||||
self.user = User.objects.create(username="test-user")
|
||||
|
||||
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):
|
||||
"""Test user's metrics"""
|
||||
self.client.force_login(self.admin)
|
||||
@ -67,12 +41,22 @@ class TestUsersAPI(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_set_password(self):
|
||||
"""Test Direct password set"""
|
||||
self.client.force_login(self.admin)
|
||||
new_pw = generate_key()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-set-password", kwargs={"pk": self.admin.pk}),
|
||||
data={"password": new_pw},
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.admin.refresh_from_db()
|
||||
self.assertTrue(self.admin.check_password(new_pw))
|
||||
|
||||
def test_recovery(self):
|
||||
"""Test user recovery link (no recovery flow set)"""
|
||||
flow = Flow.objects.create(
|
||||
name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY
|
||||
)
|
||||
tenant: Tenant = Tenant.objects.first()
|
||||
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_recovery = flow
|
||||
tenant.save()
|
||||
self.client.force_login(self.admin)
|
||||
@ -99,10 +83,8 @@ class TestUsersAPI(APITestCase):
|
||||
"""Test user recovery link (no email stage)"""
|
||||
self.user.email = "foo@bar.baz"
|
||||
self.user.save()
|
||||
flow = Flow.objects.create(
|
||||
name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY
|
||||
)
|
||||
tenant: Tenant = Tenant.objects.first()
|
||||
flow = create_test_flow(designation=FlowDesignation.RECOVERY)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_recovery = flow
|
||||
tenant.save()
|
||||
self.client.force_login(self.admin)
|
||||
@ -115,10 +97,8 @@ class TestUsersAPI(APITestCase):
|
||||
"""Test user recovery link"""
|
||||
self.user.email = "foo@bar.baz"
|
||||
self.user.save()
|
||||
flow = Flow.objects.create(
|
||||
name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY
|
||||
)
|
||||
tenant: Tenant = Tenant.objects.first()
|
||||
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_recovery = flow
|
||||
tenant.save()
|
||||
|
||||
|
57
authentik/core/tests/utils.py
Normal file
57
authentik/core/tests/utils.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""Test Utils"""
|
||||
from typing import Optional
|
||||
|
||||
from django.utils.text import slugify
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
def create_test_flow(designation: FlowDesignation = FlowDesignation.STAGE_CONFIGURATION) -> Flow:
|
||||
"""Generate a flow that can be used for testing"""
|
||||
uid = generate_id(10)
|
||||
return Flow.objects.create(
|
||||
name=uid,
|
||||
title=uid,
|
||||
slug=slugify(uid),
|
||||
designation=designation,
|
||||
)
|
||||
|
||||
|
||||
def create_test_admin_user(name: Optional[str] = None) -> User:
|
||||
"""Generate a test-admin user"""
|
||||
uid = generate_id(20) if not name else name
|
||||
group = Group.objects.create(name=uid, is_superuser=True)
|
||||
user: User = User.objects.create(
|
||||
username=uid,
|
||||
name=uid,
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
user.set_password(uid)
|
||||
user.save()
|
||||
group.users.add(user)
|
||||
return user
|
||||
|
||||
|
||||
def create_test_tenant() -> Tenant:
|
||||
"""Generate a test tenant, removing all other tenants to make sure this one
|
||||
matches."""
|
||||
uid = generate_id(20)
|
||||
Tenant.objects.all().delete()
|
||||
return Tenant.objects.create(domain=uid, default=True)
|
||||
|
||||
|
||||
def create_test_cert() -> CertificateKeyPair:
|
||||
"""Generate a certificate for testing"""
|
||||
builder = CertificateBuilder()
|
||||
builder.common_name = "goauthentik.io"
|
||||
builder.build(
|
||||
subject_alt_names=["goauthentik.io"],
|
||||
validity_days=360,
|
||||
)
|
||||
builder.name = generate_id()
|
||||
return builder.save()
|
@ -29,3 +29,4 @@ class UserSettingSerializer(PassiveSerializer):
|
||||
component = CharField()
|
||||
title = CharField()
|
||||
configure_url = CharField(required=False)
|
||||
icon_url = CharField(required=False)
|
||||
|
@ -1,11 +1,13 @@
|
||||
"""authentik URL Configuration"""
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import path
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.generic import RedirectView
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from authentik.core.views import impersonate
|
||||
from authentik.core.views import apps, impersonate
|
||||
from authentik.core.views.debug import AccessDeniedView
|
||||
from authentik.core.views.interface import FlowInterfaceView
|
||||
from authentik.core.views.session import EndSessionView
|
||||
|
||||
@ -15,6 +17,12 @@ urlpatterns = [
|
||||
login_required(RedirectView.as_view(pattern_name="authentik_core:if-user")),
|
||||
name="root-redirect",
|
||||
),
|
||||
path(
|
||||
# We have to use this format since everything else uses applications/o or applications/saml
|
||||
"application/launch/<slug:application_slug>/",
|
||||
apps.RedirectToAppLaunch.as_view(),
|
||||
name="application-launch",
|
||||
),
|
||||
# Impersonation
|
||||
path(
|
||||
"-/impersonation/<int:user_id>/",
|
||||
@ -54,3 +62,8 @@ urlpatterns = [
|
||||
TemplateView.as_view(template_name="if/admin.html"),
|
||||
),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path("debug/policy/deny/", AccessDeniedView.as_view(), name="debug-policy-deny"),
|
||||
]
|
||||
|
75
authentik/core/views/apps.py
Normal file
75
authentik/core/views/apps.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""app views"""
|
||||
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.flows.challenge import (
|
||||
ChallengeResponse,
|
||||
ChallengeTypes,
|
||||
HttpChallengeResponse,
|
||||
RedirectChallenge,
|
||||
)
|
||||
from authentik.flows.models import in_memory_stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.stages.consent.stage import (
|
||||
PLAN_CONTEXT_CONSENT_HEADER,
|
||||
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||
)
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
class RedirectToAppLaunch(View):
|
||||
"""Application launch view, redirect to the launch URL"""
|
||||
|
||||
def dispatch(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||
app = get_object_or_404(Application, slug=application_slug)
|
||||
# Check here if the application has any launch URL set, if not 404
|
||||
launch = app.get_launch_url()
|
||||
if not launch:
|
||||
raise Http404
|
||||
# Check if we're authenticated already, saves us the flow run
|
||||
if request.user.is_authenticated:
|
||||
return HttpResponseRedirect(app.get_launch_url(request.user))
|
||||
# otherwise, do a custom flow plan that includes the application that's
|
||||
# being accessed, to improve usability
|
||||
tenant: Tenant = request.tenant
|
||||
flow = tenant.flow_authentication
|
||||
planner = FlowPlanner(flow)
|
||||
planner.allow_empty_flows = True
|
||||
plan = planner.plan(
|
||||
request,
|
||||
{
|
||||
PLAN_CONTEXT_APPLICATION: app,
|
||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||
% {"application": app.name},
|
||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
||||
},
|
||||
)
|
||||
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
||||
request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
||||
|
||||
|
||||
class RedirectToAppStage(ChallengeStageView):
|
||||
"""Final stage to be inserted after the user logs in"""
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> RedirectChallenge:
|
||||
app = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
|
||||
launch = app.get_launch_url(self.get_pending_user())
|
||||
# sanity check to ensure launch is still set
|
||||
if not launch:
|
||||
raise Http404
|
||||
return RedirectChallenge(
|
||||
instance={
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": launch,
|
||||
}
|
||||
)
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
return HttpChallengeResponse(self.get_challenge())
|
12
authentik/core/views/debug.py
Normal file
12
authentik/core/views/debug.py
Normal file
@ -0,0 +1,12 @@
|
||||
"""debug view"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.views.generic import View
|
||||
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
|
||||
|
||||
class AccessDeniedView(View):
|
||||
"""Easily access AccessDeniedResponse"""
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
return AccessDeniedResponse(request)
|
@ -5,9 +5,13 @@ from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
|
||||
from authentik.core.middleware import (
|
||||
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
|
||||
SESSION_KEY_IMPERSONATE_USER,
|
||||
)
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@ -17,14 +21,17 @@ class ImpersonateInitView(View):
|
||||
|
||||
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
|
||||
"""Impersonation handler, checks permissions"""
|
||||
if not CONFIG.y_bool("impersonation"):
|
||||
LOGGER.debug("User attempted to impersonate", user=request.user)
|
||||
return HttpResponse("Unauthorized", status=401)
|
||||
if not request.user.has_perm("impersonate"):
|
||||
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
|
||||
return HttpResponse("Unauthorized", status=401)
|
||||
|
||||
user_to_be = get_object_or_404(User, pk=user_id)
|
||||
|
||||
request.session[SESSION_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||
request.session[SESSION_IMPERSONATE_USER] = user_to_be
|
||||
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
||||
|
||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||
|
||||
@ -37,16 +44,16 @@ class ImpersonateEndView(View):
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
"""End Impersonation handler"""
|
||||
if (
|
||||
SESSION_IMPERSONATE_USER not in request.session
|
||||
or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||
SESSION_KEY_IMPERSONATE_USER not in request.session
|
||||
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||
):
|
||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||
return redirect("authentik_core:if-user")
|
||||
|
||||
original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
del request.session[SESSION_IMPERSONATE_USER]
|
||||
del request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||
del request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Crypto API Views"""
|
||||
from typing import Optional
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
@ -15,14 +17,18 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.managed import MANAGED_KEY
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"""CertificateKeyPair Serializer"""
|
||||
@ -30,6 +36,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
cert_expiry = DateTimeField(source="certificate.not_valid_after", read_only=True)
|
||||
cert_subject = SerializerMethodField()
|
||||
private_key_available = SerializerMethodField()
|
||||
private_key_type = SerializerMethodField()
|
||||
|
||||
certificate_download_url = SerializerMethodField()
|
||||
private_key_download_url = SerializerMethodField()
|
||||
@ -42,6 +49,13 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"""Show if this keypair has a private key configured or not"""
|
||||
return instance.key_data != "" and instance.key_data is not None
|
||||
|
||||
def get_private_key_type(self, instance: CertificateKeyPair) -> Optional[str]:
|
||||
"""Get the private key's type, if set"""
|
||||
key = instance.private_key
|
||||
if key:
|
||||
return key.__class__.__name__.replace("_", "").lower().replace("privatekey", "")
|
||||
return None
|
||||
|
||||
def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:
|
||||
"""Get URL to download certificate"""
|
||||
return (
|
||||
@ -65,22 +79,30 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
def validate_certificate_data(self, value: str) -> str:
|
||||
"""Verify that input is a valid PEM x509 Certificate"""
|
||||
try:
|
||||
load_pem_x509_certificate(value.encode("utf-8"), default_backend())
|
||||
except ValueError:
|
||||
# Cast to string to fully load and parse certificate
|
||||
# Prevents issues like https://github.com/goauthentik/authentik/issues/2082
|
||||
str(load_pem_x509_certificate(value.encode("utf-8"), default_backend()))
|
||||
except ValueError as exc:
|
||||
LOGGER.warning("Failed to load certificate", exc=exc)
|
||||
raise ValidationError("Unable to load certificate.")
|
||||
return value
|
||||
|
||||
def validate_key_data(self, value: str) -> str:
|
||||
"""Verify that input is a valid PEM RSA Key"""
|
||||
"""Verify that input is a valid PEM Key"""
|
||||
# Since this field is optional, data can be empty.
|
||||
if value != "":
|
||||
try:
|
||||
load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in value.split("\n")])),
|
||||
password=None,
|
||||
backend=default_backend(),
|
||||
# Cast to string to fully load and parse certificate
|
||||
# Prevents issues like https://github.com/goauthentik/authentik/issues/2082
|
||||
str(
|
||||
load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in value.split("\n")])),
|
||||
password=None,
|
||||
backend=default_backend(),
|
||||
)
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
except (ValueError, TypeError) as exc:
|
||||
LOGGER.warning("Failed to load private key", exc=exc)
|
||||
raise ValidationError("Unable to load private key (possibly encrypted?).")
|
||||
return value
|
||||
|
||||
@ -97,6 +119,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"cert_expiry",
|
||||
"cert_subject",
|
||||
"private_key_available",
|
||||
"private_key_type",
|
||||
"certificate_download_url",
|
||||
"private_key_download_url",
|
||||
"managed",
|
||||
@ -141,9 +164,11 @@ class CertificateKeyPairFilter(FilterSet):
|
||||
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
"""CertificateKeyPair Viewset"""
|
||||
|
||||
queryset = CertificateKeyPair.objects.exclude(managed__isnull=False)
|
||||
queryset = CertificateKeyPair.objects.exclude(managed=MANAGED_KEY)
|
||||
serializer_class = CertificateKeyPairSerializer
|
||||
filterset_class = CertificateKeyPairFilter
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
@permission_required(None, ["authentik_crypto.add_certificatekeypair"])
|
||||
@extend_schema(
|
||||
@ -189,7 +214,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
secret=certificate,
|
||||
type="certificate",
|
||||
).from_http(request)
|
||||
if "download" in request._request.GET:
|
||||
if "download" in request.query_params:
|
||||
# Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html
|
||||
response = HttpResponse(
|
||||
certificate.certificate_data, content_type="application/x-pem-file"
|
||||
@ -220,7 +245,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
secret=certificate,
|
||||
type="private_key",
|
||||
).from_http(request)
|
||||
if "download" in request._request.GET:
|
||||
if "download" in request.query_params:
|
||||
# Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html
|
||||
response = HttpResponse(certificate.key_data, content_type="application/x-pem-file")
|
||||
response[
|
||||
|
@ -13,3 +13,4 @@ class AuthentikCryptoConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
import_module("authentik.crypto.managed")
|
||||
import_module("authentik.crypto.tasks")
|
||||
|
@ -44,7 +44,7 @@ class CertificateBuilder:
|
||||
"""Build self-signed certificate"""
|
||||
one_day = datetime.timedelta(1, 0, 0)
|
||||
self.__private_key = rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=2048, backend=default_backend()
|
||||
public_exponent=65537, key_size=4096, backend=default_backend()
|
||||
)
|
||||
self.__public_key = self.__private_key.public_key()
|
||||
alt_names: list[x509.GeneralName] = [x509.DNSName(x) for x in subject_alt_names or []]
|
||||
@ -53,10 +53,7 @@ class CertificateBuilder:
|
||||
.subject_name(
|
||||
x509.Name(
|
||||
[
|
||||
x509.NameAttribute(
|
||||
NameOID.COMMON_NAME,
|
||||
self.common_name,
|
||||
),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, self.common_name),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "authentik"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Self-signed"),
|
||||
]
|
||||
@ -65,10 +62,7 @@ class CertificateBuilder:
|
||||
.issuer_name(
|
||||
x509.Name(
|
||||
[
|
||||
x509.NameAttribute(
|
||||
NameOID.COMMON_NAME,
|
||||
f"authentik {__version__}",
|
||||
),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, f"authentik {__version__}"),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user