Compare commits
1502 Commits
blueprint-
...
lib/sync/d
Author | SHA1 | Date | |
---|---|---|---|
4b4e3a09a6 | |||
7a8c2e7ad9 | |||
a57381ca4a | |||
154dde9a9a | |||
a15365a9f1 | |||
10f11cbc31 | |||
caec23d52a | |||
7e1781ed76 | |||
0cfdbbbec6 | |||
8a1b7cb166 | |||
f367a84676 | |||
32d6b03a3c | |||
08027bf0ad | |||
8c02b25677 | |||
160f137707 | |||
52c35fab06 | |||
69a07c1c88 | |||
691a0d66ee | |||
3f4328bf2a | |||
b945552b7c | |||
5347b85c9f | |||
fb2401cf9e | |||
b161315811 | |||
0fa2267b86 | |||
4bbdddb876 | |||
bca9c0965e | |||
dd58b5044e | |||
c4f081cb68 | |||
59aad31459 | |||
de9db3cb83 | |||
24eb5fcda9 | |||
556ae6a5cb | |||
a479d9c1d8 | |||
b8bb969ee7 | |||
7d361e4734 | |||
dc7c7686a3 | |||
94b4977397 | |||
7f822e1cb7 | |||
fb3ec1f38b | |||
87505517ee | |||
4c5fe84f92 | |||
5faa224c81 | |||
736da3abef | |||
52d90f8d3b | |||
7b812de977 | |||
a4bd2cc263 | |||
14038ba8d2 | |||
eaff59b6b0 | |||
cb702ca07a | |||
cb0bfb0dad | |||
bf46d5c916 | |||
59e686c8b9 | |||
9e736f2838 | |||
c2dd3d9c1b | |||
42302d3187 | |||
20ccabf3ec | |||
8f939fa577 | |||
2519bcef89 | |||
3e3615a859 | |||
79e82c8dc9 | |||
ccd4432e1f | |||
b3137f5307 | |||
2591ed9840 | |||
b3e89ef570 | |||
45b48c5cd6 | |||
1eefd834fc | |||
4cc6ed97c5 | |||
bb55d9b3de | |||
3972afb865 | |||
04a013cc1b | |||
fb396f7737 | |||
cf120ff3ff | |||
3e4923d52e | |||
01793088f0 | |||
e2bf2ec2cc | |||
4dfbe28709 | |||
b2021a7191 | |||
81e5fb0c18 | |||
a2a2d940a8 | |||
c034930219 | |||
da3dc51d87 | |||
d217a39513 | |||
7729a9317c | |||
be5f5dd3f0 | |||
bed8d5da4b | |||
4f70f84e80 | |||
97b8551866 | |||
9a0b67e700 | |||
97e4c89cec | |||
65aedde8f7 | |||
17450f23bf | |||
ab3ad6b7fd | |||
45bc3cbd41 | |||
9c1bcac6af | |||
0a133265c5 | |||
57f25a97c9 | |||
8f32242787 | |||
c4bb19051d | |||
10f4fae711 | |||
2d9eab3f60 | |||
fa66195619 | |||
134eb126b6 | |||
f5a6136a58 | |||
1a82dfcd61 | |||
61fc1dc1fb | |||
1f921cc18e | |||
2f94ee3f1f | |||
154fba12e0 | |||
0d18c1d797 | |||
e905dd52d8 | |||
245126a1c3 | |||
15d84d30ba | |||
c6333f9e19 | |||
56565b0895 | |||
cbbc7c1825 | |||
908aaa5afa | |||
937342eab1 | |||
82823a7449 | |||
ad50f14a3e | |||
e0cf6128df | |||
bfbe8b8038 | |||
36ba8bc4e7 | |||
dd5edf7fd9 | |||
da1b252f3b | |||
a8e543972a | |||
6e03045d1f | |||
f4b39e7465 | |||
e7cd5880b5 | |||
d8c6a2417d | |||
a1fe471a59 | |||
054dfda73f | |||
2e5e8f5c58 | |||
c28b65a3f2 | |||
afc9847e36 | |||
620c95dfa1 | |||
15c7a0a9be | |||
5fed8ca575 | |||
f471ddfb29 | |||
1b1f06c9f7 | |||
67c31a8ac3 | |||
638180d246 | |||
a3be1bbb57 | |||
fbd0ba2865 | |||
182ad912cb | |||
e850b2ba1a | |||
b4ce7f9ab0 | |||
5f0bd6f5ea | |||
f5944ccb95 | |||
9bd3bad605 | |||
dff60ee9fb | |||
4e932e47c9 | |||
e57a98aeb5 | |||
807ea2a52a | |||
0775bc0f1e | |||
35a4d9cc71 | |||
ed9008a7d4 | |||
a377ce6b45 | |||
dac24ba62d | |||
826acbde2a | |||
b7d97da2bc | |||
cc6fcd831d | |||
e5e3a5df80 | |||
74268500b0 | |||
614740a4ff | |||
f48496b2cf | |||
35da3d65d2 | |||
fb53fe2b3e | |||
dda2338258 | |||
f582e66c67 | |||
f595375f2d | |||
fd8317de7f | |||
2f1eab5aed | |||
70460bfb30 | |||
0be9c60a71 | |||
abaf8d9544 | |||
73a3f29001 | |||
159bf4012e | |||
9b3c1b5cff | |||
19aa268e4e | |||
1c5e906a3e | |||
c133ba9bd3 | |||
65517f3b7f | |||
b361dd3b59 | |||
40f598f3f1 | |||
b72d0e84c9 | |||
d97297e0ce | |||
1a80353bc0 | |||
beece507fd | |||
e2bec88403 | |||
26b6c2e130 | |||
1a38679ecf | |||
b2334c3680 | |||
13251bb8c4 | |||
9fe6bac99d | |||
7c9fe53b47 | |||
b20c4eab29 | |||
8ca09a9ece | |||
856598fc54 | |||
fdb7b29d9a | |||
3748781368 | |||
99b559893b | |||
8014088c3a | |||
3ee353126f | |||
db76c5d9e2 | |||
61bff69b7d | |||
69651323e3 | |||
75a0ac9588 | |||
941a697397 | |||
4a74db17a1 | |||
0cf6bff93c | |||
814e438422 | |||
2db77a37dd | |||
e40c5ac617 | |||
7440900dac | |||
ca96b27825 | |||
ad4a765a80 | |||
4dcd481010 | |||
d0dc14d84d | |||
7bf960352b | |||
c07d01661b | |||
427597ec14 | |||
7cc77bd387 | |||
381a1a2c49 | |||
08f8222224 | |||
1211c34a18 | |||
22efb57369 | |||
3eeda53be6 | |||
82ace18703 | |||
8589079252 | |||
ae2af6e58e | |||
86a7f98ff6 | |||
3af45371d3 | |||
b01ffd934f | |||
f11ba94603 | |||
7d2aa43364 | |||
f1351a7577 | |||
0611eea0e7 | |||
d0b46fcf9c | |||
dcbdc37d31 | |||
d07f396379 | |||
0972103b83 | |||
b448e76db4 | |||
f2937bd6dd | |||
53c2e3e77c | |||
7dd62c1f55 | |||
33e3510fba | |||
0e5fac2642 | |||
c53b1fe78a | |||
838a7457b2 | |||
a3c07bc9ff | |||
121f2c609d | |||
365affc28e | |||
f367822779 | |||
848198125d | |||
497ac5e3d0 | |||
1773d4d681 | |||
4edbb51939 | |||
c7e97ab48e | |||
31f7faae1c | |||
f5dae2ae92 | |||
2c043dba0b | |||
bda10e5db1 | |||
be9ae7d4f7 | |||
b4a6189bfa | |||
bfdb827ff9 | |||
488a58e1c5 | |||
3f83e69453 | |||
e92fa5df0b | |||
f8c22170df | |||
e3d08a8434 | |||
97d3e9afdc | |||
1eb08def73 | |||
6e3b379e4a | |||
264f59775c | |||
d048f1ecbd | |||
eb31f31584 | |||
fe5c842e92 | |||
b82d3100c9 | |||
49bb668036 | |||
52c70c7700 | |||
b99fd36f86 | |||
8a5381eca3 | |||
2c77830179 | |||
ffcd7def60 | |||
ed121bc2a3 | |||
d5ab9d9167 | |||
a983321ad6 | |||
9c3420ede4 | |||
91b40350aa | |||
1912991682 | |||
71b9117f53 | |||
b5f947f460 | |||
3a2f7e9549 | |||
1582ce0920 | |||
6d3eea5266 | |||
e987208bd1 | |||
0efab8eef7 | |||
9402dac8ae | |||
f57a290eee | |||
5dab0d2b7a | |||
2da6036248 | |||
cdba94cea4 | |||
c59eca664a | |||
d5b205f9c0 | |||
8ad9ad833e | |||
599ce15f68 | |||
91310eff52 | |||
b522d6732a | |||
17d96f204e | |||
65e4667bc3 | |||
f67f9e5ed0 | |||
62dd6a4393 | |||
a46eae8276 | |||
c4acc9fc24 | |||
e748a03082 | |||
e473f28e21 | |||
f70635c295 | |||
70d60c7ab2 | |||
61a26c02b7 | |||
a06645d558 | |||
7730ecbd37 | |||
80e1be8db7 | |||
c528c74e48 | |||
6d7bf36afe | |||
44fb59eb18 | |||
8f8d924935 | |||
602adaa5c5 | |||
5c9e97e11c | |||
2e7c620c9c | |||
30a2770781 | |||
ef49fa0e79 | |||
ac524ef425 | |||
6f3c1c4537 | |||
87886ca1b6 | |||
7ff96e30f9 | |||
b26271557a | |||
15c99ff129 | |||
2a38e08e31 | |||
3696706466 | |||
d0c9635033 | |||
7731014e1c | |||
d478582a5c | |||
6255f380aa | |||
1f02e67c5c | |||
d0bfb894b4 | |||
c5dfdc6deb | |||
d04a66ad9a | |||
a5edaabec0 | |||
daa367bc62 | |||
78345853c2 | |||
f0fa8a3226 | |||
3335fdc6ad | |||
29c2c0f7dc | |||
ada4254f52 | |||
39035de552 | |||
e76d388ce4 | |||
a52f887692 | |||
d8b12a9a07 | |||
ec01f16e99 | |||
9e3aaefc20 | |||
4454592442 | |||
593c953ecc | |||
bcefe7123c | |||
812cf6c4f2 | |||
73b6ef6a73 | |||
b58ebcddbf | |||
8b6ac3c806 | |||
c6aa792076 | |||
ee4792734e | |||
445f11ca6b | |||
8e4810fb20 | |||
96a122c5d1 | |||
3c6b8b10e5 | |||
15999caa5d | |||
57d8375de1 | |||
07ec787076 | |||
bc96bef097 | |||
28869858b5 | |||
cbc5a1c39d | |||
5f6b69c998 | |||
cf065db3d5 | |||
86c65325ce | |||
2b8e10e979 | |||
9298807275 | |||
ed56d6ac50 | |||
8c07b385ad | |||
880db7a86c | |||
99c1250ba5 | |||
5ce126ac83 | |||
dfa21d0725 | |||
e7e4af3894 | |||
931d6ec579 | |||
ff45acb25c | |||
c96557ff2d | |||
734feac4ae | |||
b17a9ed145 | |||
2bef7695db | |||
df472dd842 | |||
98d201d34c | |||
47e89602ab | |||
ceb0851452 | |||
cac2593658 | |||
1c9705bfaa | |||
9e2566cec4 | |||
5bdef1c4f6 | |||
ae41ccd862 | |||
337956672f | |||
cf160f800d | |||
e9822cd937 | |||
5244f64be4 | |||
0df4824fd4 | |||
ea22abc75d | |||
b09bab7543 | |||
5aedc8a5f2 | |||
2f3ae0f607 | |||
e3674426b7 | |||
df915d3a5e | |||
4949c31860 | |||
4580dec06b | |||
56de969640 | |||
413902508d | |||
64af0ccba6 | |||
673db53777 | |||
8df7716d90 | |||
19bb2de13f | |||
a218fd7628 | |||
78cfb50a90 | |||
2033d52dc2 | |||
be00f47ddc | |||
2cc5f4b273 | |||
4e8f3407a4 | |||
7f861cc2a1 | |||
7bf58d0ba2 | |||
fffcb00f39 | |||
77ee868573 | |||
6aaec08496 | |||
cc15584650 | |||
e55e446b89 | |||
76088e48b5 | |||
4165a0a6b2 | |||
647fefe5ce | |||
723dccdae3 | |||
c82f747e5e | |||
43406e2464 | |||
a0ff0bef85 | |||
bedf548a5f | |||
976e81c1dd | |||
ad733033d7 | |||
ba686f6a93 | |||
dc50be1e13 | |||
205686d252 | |||
6d589013e6 | |||
2d6433ca9a | |||
b5f07acb26 | |||
ea8702077c | |||
6593357115 | |||
6daed865c1 | |||
c48a21707a | |||
e857770c0a | |||
add74c8799 | |||
12d854035d | |||
57dd4ae91d | |||
37fbc98177 | |||
14f216eb40 | |||
1209dd022e | |||
c96f13ac66 | |||
5e6874cc1f | |||
fb5053ec83 | |||
6f7dc2c543 | |||
542b69b224 | |||
c15c0cbe86 | |||
c6fe0c1d85 | |||
07f0666a6f | |||
51609d696d | |||
c0d08df161 | |||
643a97f0a5 | |||
155a31fd70 | |||
c6f9d5df7b | |||
ea85331a7e | |||
4f4c5253dd | |||
83b2fc36df | |||
d99d2b8bdc | |||
9b96d04b3a | |||
ca5b99eb16 | |||
4c1676e97c | |||
81855cf2fe | |||
bd904027be | |||
0ffc97db15 | |||
2c515b1e17 | |||
f8900fbaf3 | |||
0f4a98d9c6 | |||
8853f25b45 | |||
1c40f7b95a | |||
9b5d6ec1af | |||
36d29a9ae1 | |||
0606b1aba4 | |||
03d5dad867 | |||
38a9e46af3 | |||
5eb848e376 | |||
61a293daad | |||
edf3300944 | |||
5d9c40eac8 | |||
6ebfbcb66e | |||
bf0235c113 | |||
895cd23b57 | |||
c908d9e95e | |||
a07fd8d54b | |||
39a46a6dc4 | |||
ad71960d77 | |||
2a384511f5 | |||
4dcc104947 | |||
71fe526e47 | |||
03e3f516ac | |||
3b59333246 | |||
4e800c14cb | |||
789b29a3e7 | |||
857b6e63a0 | |||
edc937dd78 | |||
d98b6f29d4 | |||
53ba2a0ca8 | |||
ae364292e6 | |||
f15bc2df97 | |||
b27d49e55f | |||
e0d2beb225 | |||
2313b4755b | |||
1cffadecb0 | |||
5e163d6da1 | |||
0626e18674 | |||
e986a62a12 | |||
e25afcb84a | |||
bb95613104 | |||
89dfac2f57 | |||
31462b55e6 | |||
60337c1cf0 | |||
343d3bb1fb | |||
11fe86c4f6 | |||
963ce085e4 | |||
3642b89ab0 | |||
8cfb371ed3 | |||
6e74edb9f2 | |||
397905f8f0 | |||
7fd35b1dfc | |||
9ba03f5439 | |||
1139d6d27c | |||
077fd966c2 | |||
bd41822a57 | |||
dfd3d76434 | |||
397e98906d | |||
65d8da8c64 | |||
5b435297c5 | |||
f792fd42f6 | |||
70c0fdd5fa | |||
9b636eba01 | |||
a982224502 | |||
6a16cccb40 | |||
6dac91e2b4 | |||
3e2d0532d1 | |||
4e1300650b | |||
06b3ed0c9c | |||
395ad722b7 | |||
9917d81246 | |||
2a87687d34 | |||
a726c2260a | |||
44e0bfd4ef | |||
8d0b362c9c | |||
e5e53f034e | |||
71b87127d1 | |||
d5d67fe22d | |||
5d2685341d | |||
f1ac4ff9c9 | |||
79f4c66286 | |||
1f82094c0b | |||
35440acba3 | |||
eca9901704 | |||
6ddd5a3d5f | |||
5664e62eca | |||
1403f17d62 | |||
1ac8989e81 | |||
b0a1db77e3 | |||
46da4cb59e | |||
154df5cdf7 | |||
5b889456f6 | |||
3eaed82c48 | |||
feaf9d8bc9 | |||
2899668ae2 | |||
4c25e1bb24 | |||
464ff3f5b1 | |||
22eb5f56f1 | |||
7e48e87f49 | |||
8ce12f7850 | |||
2514baabeb | |||
945930a507 | |||
537a80ad97 | |||
5c993e23fe | |||
eb2db18494 | |||
12a46a8426 | |||
4a1213310a | |||
84c2097148 | |||
c05dedc573 | |||
18c197e75b | |||
0c26a0bce2 | |||
5fd6a4cead | |||
51fb1bd8e7 | |||
4a30f87a42 | |||
8e6b6ede30 | |||
af30c2a68e | |||
9b65627a3e | |||
4bad91c901 | |||
f3c479d077 | |||
b024df9903 | |||
f6a6458088 | |||
f0dc0e8900 | |||
79e89b0376 | |||
4cc7d91379 | |||
245909e31a | |||
997a1ddb3d | |||
42335a60bf | |||
fc539332e1 | |||
d9efb02078 | |||
6212250e19 | |||
c18beefc8f | |||
f23da6e402 | |||
e934b246c8 | |||
ead684a410 | |||
d782aadab7 | |||
4ac6f83aea | |||
6281d36a69 | |||
8129ad4ec0 | |||
24eea415b2 | |||
a615ce8e95 | |||
5b275cf7fb | |||
d6e91c119f | |||
7841e47e74 | |||
ad2a4bea3e | |||
a554c085c1 | |||
ff0d978754 | |||
de48e62819 | |||
e50e995d2f | |||
3bf4156cb3 | |||
89990facf5 | |||
48545950ed | |||
0544aa5fae | |||
5d69455b87 | |||
3d291cf4da | |||
44d7c42dc7 | |||
4ea4e925e3 | |||
169172c85f | |||
adea637fa4 | |||
0231277d9c | |||
45643ed1f6 | |||
3823d56dbd | |||
43cfd59ac0 | |||
c8555bbf59 | |||
a4251a3410 | |||
50985f9b0b | |||
9ec24528d4 | |||
5eac38c0cc | |||
010df0c31c | |||
7ba858eff3 | |||
817d2d5ff8 | |||
70e34e03b4 | |||
d61f9f6d57 | |||
bdf81706b8 | |||
7b56602fc9 | |||
7c6e25a996 | |||
0eeaeaf1ff | |||
9ce4337b11 | |||
c6a3c7371c | |||
42a7cf10f2 | |||
bb4f7b1193 | |||
3eecfb835b | |||
92ab856bd3 | |||
178549a756 | |||
67d178aa11 | |||
ef53abace9 | |||
5effb3a0f6 | |||
3a37916a8f | |||
428d5ac9cf | |||
7b4037fdda | |||
2c7bbcc27b | |||
19fb24de99 | |||
2709702896 | |||
7d0d5a7dc2 | |||
6a04a2ca69 | |||
ea561c9da6 | |||
9b9c55f17c | |||
bd5e78bd44 | |||
ab98028022 | |||
813ff64ba1 | |||
c99e742214 | |||
dac6ad3cd6 | |||
e4d2a53ccc | |||
3b6775fd9c | |||
5882e0b2cb | |||
65f0b471d8 | |||
7d054db1a5 | |||
cb75ba2e5e | |||
36cecc1391 | |||
81b91d8777 | |||
41dc23b3c2 | |||
370eff1494 | |||
0ff8def03b | |||
b01cafd9fe | |||
90aa8abb80 | |||
fd21aae4f9 | |||
360223a2ff | |||
0e83de2697 | |||
a23bac9d9b | |||
220378b3f2 | |||
363d655378 | |||
e93b2a1a75 | |||
76665cf65e | |||
3ad7f4dc24 | |||
c5045e8792 | |||
a8c9b3a8ba | |||
148506639a | |||
53814d9919 | |||
08b04c32f5 | |||
1c1d97339d | |||
cafa9c1737 | |||
5f64347ba1 | |||
45ef54480a | |||
a3dc8af4c6 | |||
36933a0aca | |||
8f689890df | |||
ec49b2e0e0 | |||
22ebe05706 | |||
f0e58a6f49 | |||
a3d642c08e | |||
5d42cb9185 | |||
1fd0cc5bb5 | |||
deef365ff5 | |||
d1ae6287f2 | |||
2e152cd264 | |||
f5941e403b | |||
ff3cf8c10e | |||
bfa6328172 | |||
4c9691c932 | |||
a0f1566b4c | |||
46261a4f42 | |||
8b42ff1e97 | |||
ca4cb0d251 | |||
a5a0fa79dd | |||
c06a871f61 | |||
4a3df67134 | |||
422ccf61fa | |||
d989f23907 | |||
059180edef | |||
22f30634a8 | |||
35ff418c42 | |||
7826e7a605 | |||
64f1b8207d | |||
b2c13f0614 | |||
6965628020 | |||
608f63e9a2 | |||
22fa3a7fba | |||
bcfd6fefa7 | |||
eae18d0016 | |||
4a12a57c5f | |||
71294b7deb | |||
5af907db0c | |||
63a118a2ba | |||
d9a3c34a44 | |||
23bdad7574 | |||
8ee90826fc | |||
8c7d4d2f5e | |||
d72def0368 | |||
5bcf501842 | |||
13fc216c68 | |||
27aed4b315 | |||
84b5992e55 | |||
7eb985f636 | |||
d3172ae904 | |||
88662b54c1 | |||
b38bc8c1c4 | |||
a9b648842a | |||
5fda531e2b | |||
921a3e6eb8 | |||
fd898bea66 | |||
cbf9ee55ae | |||
590ee7d9d4 | |||
b8cd1d1ae2 | |||
9f9524fbcb | |||
1df87cdf77 | |||
6383550914 | |||
10771b4779 | |||
fcaf1193ed | |||
b9f6093e6f | |||
47f6d59758 | |||
59d20e3bc0 | |||
ae347cd1c5 | |||
7653a35caa | |||
dc9b12fd37 | |||
b7dac0674a | |||
5a17dea765 | |||
044547c316 | |||
6a84e7e6b0 | |||
6d4bb77960 | |||
1b588b98bc | |||
3eccef88aa | |||
8f50dfa0c5 | |||
8417d8508f | |||
b2c2fc001b | |||
f60312cbbc | |||
7614b17a05 | |||
8947376edb | |||
ce23209ae8 | |||
0b806b7130 | |||
9538cf4690 | |||
63da458fb3 | |||
873dab29a9 | |||
1e96c80593 | |||
ee4a922234 | |||
37a2eff716 | |||
50e2f1c474 | |||
ab7338b50e | |||
bcdc6fcd36 | |||
98c3e0d68b | |||
a2b82b6448 | |||
0456ace646 | |||
d3a11ce810 | |||
bfd1445c69 | |||
c2b3e9b05c | |||
2c7d841e4a | |||
c5d13c4a15 | |||
079ef6e114 | |||
98bfca0b4d | |||
a247bd5b9f | |||
27856ec301 | |||
e4a8c05d25 | |||
cb2e0c6d54 | |||
f37e1ca642 | |||
70b1f05a84 | |||
192ed8f494 | |||
b69d77d270 | |||
35b6801ba0 | |||
f9e6f57aad | |||
868261c883 | |||
b6442c233d | |||
74292e6c23 | |||
3e2cf4fd30 | |||
05cbb4ce0c | |||
c93d85731c | |||
d163afe87c | |||
eac2c9a12b | |||
c10e4a9063 | |||
4e4adcc672 | |||
bb20576d84 | |||
5f315bddbd | |||
9e0404646b | |||
45883ff86b | |||
915f5689c6 | |||
ce1ea926f8 | |||
2e3624ea82 | |||
4e52fb7e52 | |||
7e36fb2153 | |||
2b00754324 | |||
12a73ef306 | |||
4469db9b23 | |||
b7beac6795 | |||
ad27f268dc | |||
a3f86115e1 | |||
75eb025ef4 | |||
efb3803371 | |||
904d6cd81b | |||
b445cff4c9 | |||
89437ac73b | |||
e354e110ca | |||
cf5eea74ee | |||
54433e614a | |||
78a02ff1f0 | |||
749e015414 | |||
2c9bf4befe | |||
f14b2fd4c5 | |||
cda764c5fd | |||
4cee9f3a31 | |||
9972b43399 | |||
d4805f326f | |||
38864e8e9a | |||
5618545248 | |||
876feccd51 | |||
2e28683381 | |||
5d803a9bf3 | |||
c7b3272cf6 | |||
2688fa4fe8 | |||
b713660e5d | |||
de237aab10 | |||
4068d67424 | |||
ab6595b597 | |||
0f89b6b746 | |||
45f74debd9 | |||
5a52225ee2 | |||
d36f0d187b | |||
b7bfbff2fe | |||
46d8be8d20 | |||
58158f61e4 | |||
9543800442 | |||
c0adac3625 | |||
cd7dce2cae | |||
09570a30f9 | |||
8617bb098d | |||
c47fb2612a | |||
23c0d90b3e | |||
593ae3b52e | |||
7a62965928 | |||
2d060576c7 | |||
a51252e1d3 | |||
20904776bb | |||
4a50c1f640 | |||
41555c88c4 | |||
408e6ec34e | |||
5bc65e253b | |||
f5d1f72d22 | |||
ec9e815e7a | |||
b0671e26c8 | |||
f185a41813 | |||
a2211135bc | |||
b082849fb5 | |||
e933fd5692 | |||
38649e5347 | |||
ff91ecf873 | |||
15ee17ea60 | |||
75a6d8c0c5 | |||
ef4d532b9c | |||
985d491073 | |||
2bdc415068 | |||
547e5be7a2 | |||
1bc99e48e0 | |||
349f66e53c | |||
9e0a9f4eee | |||
727404c9a4 | |||
0fa4637640 | |||
afdf830e8a | |||
7ab636e103 | |||
4efb4d6191 | |||
b855d98b78 | |||
354634cdf4 | |||
319f2ef8d1 | |||
cf58c5617a | |||
71344d0b6a | |||
696db2ae05 | |||
f08da8f295 | |||
89106c8131 | |||
f6b0eecde7 | |||
4ca151ee14 | |||
f66fea4b0a | |||
6d8dc4ac43 | |||
04982c8147 | |||
2ab68480a0 | |||
248d9e48bb | |||
e58e4bdbae | |||
a07ce35985 | |||
cfe275a374 | |||
7f474cde19 | |||
0597a3450b | |||
8191b90126 | |||
2613a5da4b | |||
2c4dd232a1 | |||
6b5c11ccfd | |||
a0b3d37b4a | |||
56eca6dc8f | |||
0377da2779 | |||
b16c67cc82 | |||
28f55635be | |||
8d4b2610b1 | |||
419cf80469 | |||
632dc4b1b2 | |||
93cfa64f5a | |||
fa8f9d4017 | |||
d4c0696a8c | |||
20635a8cc6 | |||
c621ac0a6f | |||
0487c8d0f5 | |||
37511f07a0 | |||
7840a3b52a | |||
787e9e05e4 | |||
3c14b8931f | |||
e3f1d259cf | |||
3d981f9391 | |||
ba1c919781 | |||
38696d4bd9 | |||
7213a1f27a | |||
34b5a51990 | |||
79e779b339 | |||
2a35b13ad6 | |||
3754f27275 | |||
b0547844b9 | |||
1b5abd3a3a | |||
8244c2340a | |||
28080595d0 | |||
3999aa96fb | |||
b5a8957720 | |||
9b01213990 | |||
ae64d9f0fd | |||
ea55083929 | |||
786c38b4cc | |||
60521d89cb | |||
7e7fc75e77 | |||
d0d46299d2 | |||
e025eabdef | |||
44238e6372 | |||
be986c8474 | |||
afb3623622 | |||
5eb6d62c9c | |||
2c802cad63 | |||
c24fd618f5 | |||
c36434bfc8 | |||
1751d0ce17 | |||
7c386da474 | |||
b8112de172 | |||
a2644ca865 | |||
a036513669 | |||
44809b8d26 | |||
73b21a01d1 | |||
1e66a23172 | |||
44c50157b7 | |||
ab631e6d9b | |||
043e57ab2b | |||
989d39b154 | |||
1ed6999994 | |||
3bc8dd40d5 | |||
802d6a548c | |||
f82c6eda58 | |||
05cc64c434 | |||
a22b558143 | |||
bb2b6d163b | |||
199a2ff11a | |||
cc0659168d | |||
805332061b | |||
aa340fbfe0 | |||
91572b8621 | |||
080d31f189 | |||
15b59594e2 | |||
b4e295a14a | |||
b590b6be44 | |||
15ee3d3566 | |||
aea6c7adbe | |||
42a2337200 | |||
ffdd49e176 | |||
b41231141c | |||
88d3b7f5a4 | |||
2b39748c84 | |||
93b93517be | |||
6da55dc8aa | |||
b93dc48030 | |||
7aba4b0c01 | |||
d5572a2570 | |||
55b1ddff6e | |||
77c913bfd3 | |||
69b80e5bb5 | |||
ba63399a7b | |||
86893d83b8 | |||
85ab201803 | |||
2c96b24b62 | |||
1f2cbca833 | |||
c2db998041 | |||
18a70e93a1 | |||
3123b3ac5e | |||
f2e1b6d466 | |||
6bcacd744b | |||
e5af964d9d | |||
122b95197b | |||
8d4e7f5d55 | |||
9d32ba261a | |||
b5a9b645f4 | |||
46303cc59f | |||
4af415f3fd | |||
ef82143811 | |||
c7567e031a | |||
3b2cd9e8d6 | |||
261e18b3d6 | |||
51a0f7d314 | |||
041ffef812 | |||
68b4d58ebd | |||
881571bd14 | |||
64a0f66e62 | |||
7d5cda4c25 | |||
8ba2679036 | |||
d98523f243 | |||
6da0548fa2 | |||
8734710e61 | |||
64b996aa1f | |||
dbe91cbc55 | |||
a56e037eae | |||
b8f1e2fac0 | |||
e1b56aac05 | |||
794731eed7 | |||
19fbc2a022 | |||
38e467bf8e | |||
9e32cf361b | |||
42a5a43640 | |||
8d5b835c4f | |||
ca3b948895 | |||
a714c781a6 | |||
df2e3878d5 | |||
1370c32aea | |||
0ae373bc1e | |||
6facb5872e | |||
c67de17dd8 | |||
2128e7f45f | |||
0e7a4849f6 | |||
85343fa5d4 | |||
12f16241fb | |||
2c3a040e35 | |||
ec0dd8c6a0 | |||
7b8c27ad2c | |||
79b80c2ed2 | |||
28485e8a15 | |||
e86b4514bc | |||
179f5c7acf | |||
e7538b85e1 | |||
ab8f5a2ac4 | |||
67c22c1313 | |||
74e090239a | |||
e5f0fc6469 | |||
945987f10f | |||
4ba360e7af | |||
a8fd0c376f | |||
0e5d647238 | |||
306f227813 | |||
e89e592061 | |||
454bf554a6 | |||
eab6ca96a7 | |||
7746d2ab7a | |||
4fe38172e3 | |||
e6082e0f08 | |||
9402c19966 | |||
e9c944c0d5 | |||
b865e97973 | |||
24a364bd6b | |||
65579c0a2b | |||
de20897321 | |||
39f7bc8e9b | |||
4ade549ce2 | |||
a4d87ef011 | |||
b851c3daaf | |||
198af84b3b | |||
69ced3ae02 | |||
4a2f58561b | |||
8becaf3418 | |||
bcfbc46839 | |||
af287ee7b0 | |||
ebf3d12874 | |||
7fbdd0452e | |||
18298a856f | |||
ef6836207a | |||
5ad176adf2 | |||
011afc8b2f | |||
4c32c1503b | |||
774a8e6eeb | |||
297d7f100a | |||
0d3692a619 | |||
ba20748b07 | |||
3fc296ad0b | |||
0aba428787 | |||
4a88e29de6 | |||
0d6fced7d8 | |||
29c6c1e33b | |||
e2e8b7c114 | |||
bf2e854f12 | |||
3fbc059f2d | |||
e051e8ebd8 | |||
880a99efe5 | |||
27d5063d16 | |||
e130bca344 | |||
325d590679 | |||
f40a4b5076 | |||
89a19f6e4c | |||
9bc51c683e | |||
3d2bd4d8dd | |||
46a968d1dd | |||
49cc70eb96 | |||
143b02b51a | |||
5904fae80b | |||
6f9479a085 | |||
ce10dbfa4e | |||
394881dcd3 | |||
a6e322507c | |||
755e2f1507 | |||
d41c9eb442 | |||
dea48e6ac7 | |||
1614f3174f | |||
d18950f7bb | |||
4fe533a92f | |||
82d4e8aa4e | |||
98129d3e9a | |||
98f3b9ae97 | |||
bd69dbc0e1 | |||
ac4d6ae9f6 | |||
cdc0d0a857 | |||
3656c38aa0 | |||
fe4e364492 | |||
ce86cbe2a0 | |||
8f0e9ff534 | |||
ff60607851 | |||
b6cf27b421 | |||
9457c80d62 | |||
409035b692 | |||
7798d16e01 | |||
8f16a182aa | |||
50c68df0a1 | |||
556248c7c9 | |||
ed2e2380cc | |||
1f79b5acb7 | |||
6185e7cdc7 | |||
aedce2a6a1 | |||
fefa189ff4 | |||
b5bdad6804 | |||
1d03f92dee | |||
01b20153ca | |||
83a2728500 | |||
c57f17bff8 | |||
5533f7dd7a | |||
daebeb1192 | |||
26a08fcaac | |||
330fc8cee3 | |||
205c01038f | |||
23eb93c981 | |||
5679352c15 | |||
fb7d637da1 | |||
cee48909e9 | |||
6549b303d5 | |||
e2d6d3860c | |||
91155f9ce3 | |||
bdcd1059dd | |||
e4b6df3f27 | |||
7a6d7919c8 | |||
fda9b137a7 | |||
7686d12f1b | |||
34ee29227a | |||
334e2c466f | |||
7c944b954c | |||
427a8c91c8 | |||
22d6dd3098 | |||
36c81a30ad | |||
f7dc7faea5 | |||
62720e6c51 | |||
64dfe7e3c2 | |||
c803b4da51 | |||
3568cd601f | |||
8cad66536c | |||
220e79e668 | |||
316f43e6eb | |||
b7053dfffd | |||
fccdaaf210 | |||
cf530c6f31 | |||
94d84ae1dc | |||
de1bb03619 | |||
e41d86bd2a | |||
a10e6b7fd7 | |||
92d6d74c2d | |||
773c57b8d7 | |||
692a6be07f | |||
645323cd02 | |||
06d57a7574 | |||
102c7e4c5c | |||
7e7ed83dfe | |||
141ced8317 | |||
5109af0ab4 | |||
1a1912e391 | |||
6702652824 | |||
b04ff5bbee | |||
3daa39080a | |||
d3d6040e23 | |||
e08ccf4ca0 | |||
0e346c6e7c | |||
62187e60d4 | |||
467b1fcd14 | |||
9e2fccb045 | |||
39d8b41357 | |||
0a0f8433c6 | |||
3b61e08d3d | |||
921e1923b0 | |||
a666c20c40 | |||
1ed96fd5a5 | |||
f245dada2c | |||
7d8094d9c4 | |||
d63cba0a9d | |||
fdc3de8646 | |||
7163d333dc | |||
02bdf093e0 | |||
1ce3dfd17f | |||
ce7e539f59 | |||
12e6282316 | |||
3253de73ec | |||
afe8ab7850 | |||
f2e3199050 | |||
04148e08a7 | |||
656b296d6e | |||
f76014710c | |||
04517d46b0 | |||
365e9c9ca3 | |||
5b01f44333 | |||
388b29ef87 | |||
7659afdd30 | |||
faab182404 | |||
90a85abf9d | |||
4d061e1af9 | |||
0720b3db3c | |||
236455fc45 | |||
ac08805d73 | |||
656beebd63 | |||
6430cdcd68 | |||
b8c97eb7c1 | |||
9eef9ee230 | |||
84cc2b4f11 | |||
e988799e12 | |||
7c71f9fcac | |||
1eeb85a4e7 | |||
4182ead0b9 | |||
dc45e8c08c | |||
d111740f6b | |||
4597ee45f8 | |||
735f48981d | |||
f35457492b | |||
af9ba83529 | |||
3c6cb9dbad | |||
1d63359077 | |||
33121d86f2 | |||
0c235909a2 | |||
91ef8c2c8d | |||
4ee45bb5cc | |||
b4ae3ba390 | |||
f3834016dc | |||
661a966e23 | |||
813273338e | |||
99639a9ed0 | |||
41aa36d06f | |||
62fc4c56e4 | |||
4514412010 | |||
463efac469 | |||
f4508659cf | |||
336f6f0dc2 | |||
c19a887356 | |||
09931bcbc2 | |||
7a4293bf17 | |||
6e569acd84 | |||
02c69d767f | |||
1863a9a12b | |||
b981bc5ba1 | |||
5da02971eb | |||
1f49ee77df | |||
baf8f18d54 | |||
5445b1235a | |||
2893a54ffb | |||
94eff50306 | |||
0befc26507 | |||
629d5df763 | |||
3098313981 | |||
c0a370bb2b | |||
a19d915d2b | |||
9a0dc50174 | |||
ac0a708f92 | |||
0ffaf0393e | |||
9bb3aa0374 | |||
f6a32dc6e5 | |||
af83fc7245 | |||
84de15568a | |||
29f8a82b49 | |||
cd05c0ec19 | |||
c19a1b373a | |||
31b9cbfb85 | |||
c0fe0dab61 | |||
1bd42345b9 | |||
90e7545d57 | |||
78d42c391d | |||
2ad831adb0 | |||
5eaa94917b | |||
6c0d462410 | |||
9dc2c26ba9 | |||
774a84f9e6 | |||
56015d883b | |||
9d15fa4a57 | |||
bb7338f5c1 | |||
f949141d03 | |||
646d133c30 | |||
3ee3adc509 | |||
1b4fee2bac | |||
10c358401d | |||
9dddbd2f0c | |||
078d643c20 | |||
733b7cf139 | |||
f83fab214b | |||
9ce460a0ac | |||
e69a380a39 | |||
2d89f42c68 | |||
3d4d167542 | |||
ee8d3c5146 | |||
0406b0d95a | |||
44d49bb14c | |||
afb1686be7 | |||
6b1802697d | |||
943fd6b78b | |||
ed33d314cd | |||
d343ccc539 | |||
31e8fb7c8c | |||
23faa0b839 | |||
3cbfd836ac | |||
10ab6e4327 | |||
561d2220bc | |||
e6c47db9f8 | |||
5f5171c472 | |||
bdf4236973 | |||
a61a41d7d0 | |||
c7532d35f2 | |||
27baedfea4 | |||
e3011eab9a | |||
9635dd98f3 | |||
bd0d7edbc4 | |||
9b05418306 | |||
d4e15f0f39 | |||
ec9c2266eb | |||
5ebd280087 | |||
1cc8d80600 | |||
3b70cd735e | |||
42766e13da | |||
8938fa5a7e | |||
4c8f610cdb | |||
8690200cd8 | |||
91145b7929 | |||
d255e53756 | |||
d51e6a5551 | |||
5433839ea0 | |||
863a7e6095 | |||
50db80428c | |||
ffd5234396 | |||
95890638a5 | |||
f7d2a68b1d | |||
83ecb64f33 | |||
40b0f7df8d | |||
ee6fcdfbd8 | |||
94623615a6 | |||
aa4f817856 | |||
c3aefd55a2 | |||
1298cdc338 | |||
3eaaa35a4c | |||
d17f781d11 | |||
c82b79f10f | |||
0aa7be6e2c | |||
9811ec57df | |||
393e5f236c | |||
59ae9c6148 | |||
fd8e20bdeb | |||
737aced000 | |||
dc3559c7e9 | |||
02bd699917 | |||
5fccbd7c04 | |||
6fc92bd50c | |||
687f6d683a | |||
4a8329649c | |||
0c296efede | |||
112520fd88 | |||
ee648269f7 | |||
15be3f2461 | |||
ef9557c578 | |||
48700c0e9c | |||
18a48030a8 | |||
640d0a4a95 | |||
6b8782556c | |||
7f6f3b6602 | |||
3367ac0e08 | |||
d5ea0ffdc6 | |||
93f1638b39 | |||
37525175fa | |||
0db1e52f90 | |||
3e8620b686 | |||
6687ffc6d2 | |||
e265ee253b | |||
7763a3673c | |||
d99005e130 | |||
c61f96e770 | |||
83622dd934 | |||
2eebd0eaa1 | |||
b61d918c5c | |||
076a4f4772 | |||
b3872b35f8 | |||
f06534cdf0 | |||
c528a6c336 | |||
821f06ffdf | |||
e83d040a48 | |||
9affd90850 | |||
80d84cb03f | |||
a9cc5fdafe | |||
b45109afce | |||
c8711d9f8f | |||
40a7135c0c | |||
675a4a6788 | |||
98b5b75f29 | |||
22b0a1bd23 | |||
1a1d499833 | |||
1573cfbaa1 | |||
b88ce32111 | |||
a1965ceada | |||
9c536a1b4b | |||
f3e0ff2833 | |||
06dc47b582 | |||
a4bf24a039 | |||
1715c3e268 | |||
feb3be7cee | |||
db05232f12 | |||
ebfa7dbcfc | |||
8c4dab7399 |
@ -1,22 +1,24 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2024.10.5
|
current_version = 2025.6.0
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||||
serialize =
|
serialize =
|
||||||
{major}.{minor}.{patch}-{rc_t}{rc_n}
|
{major}.{minor}.{patch}-{rc_t}{rc_n}
|
||||||
{major}.{minor}.{patch}
|
{major}.{minor}.{patch}
|
||||||
message = release: {new_version}
|
message = release: {new_version}
|
||||||
tag_name = version/{new_version}
|
tag_name = version/{new_version}
|
||||||
|
|
||||||
[bumpversion:part:rc_t]
|
[bumpversion:part:rc_t]
|
||||||
values =
|
values =
|
||||||
rc
|
rc
|
||||||
final
|
final
|
||||||
optional_value = final
|
optional_value = final
|
||||||
|
|
||||||
[bumpversion:file:pyproject.toml]
|
[bumpversion:file:pyproject.toml]
|
||||||
|
|
||||||
|
[bumpversion:file:uv.lock]
|
||||||
|
|
||||||
[bumpversion:file:package.json]
|
[bumpversion:file:package.json]
|
||||||
|
|
||||||
[bumpversion:file:docker-compose.yml]
|
[bumpversion:file:docker-compose.yml]
|
||||||
@ -31,4 +33,4 @@ optional_value = final
|
|||||||
|
|
||||||
[bumpversion:file:web/src/common/constants.ts]
|
[bumpversion:file:web/src/common/constants.ts]
|
||||||
|
|
||||||
[bumpversion:file:website/docs/install-config/install/aws/template.yaml]
|
[bumpversion:file:lifecycle/aws/template.yaml]
|
||||||
|
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -28,7 +28,11 @@ Output of docker-compose logs or kubectl logs respectively
|
|||||||
|
|
||||||
**Version and Deployment (please complete the following information):**
|
**Version and Deployment (please complete the following information):**
|
||||||
|
|
||||||
- authentik version: [e.g. 2021.8.5]
|
<!--
|
||||||
|
Notice: authentik supports installation via Docker, Kubernetes, and AWS CloudFormation only. Support is not available for other methods. For detailed installation and configuration instructions, please refer to the official documentation at https://docs.goauthentik.io/docs/install-config/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- authentik version: [e.g. 2025.2.0]
|
||||||
- Deployment: [e.g. docker-compose, helm]
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Documentation issue
|
||||||
|
about: Suggest an improvement or report a problem
|
||||||
|
title: ""
|
||||||
|
labels: documentation
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.**
|
||||||
|
A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...]
|
||||||
|
|
||||||
|
**Provide the URL or link to the exact page in the documentation to which you are referring.**
|
||||||
|
If there are multiple pages, list them all, and be sure to state the header or section where the content is.
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the documentation issue here.
|
||||||
|
|
||||||
|
**Consider opening a PR!**
|
||||||
|
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).
|
7
.github/ISSUE_TEMPLATE/question.md
vendored
7
.github/ISSUE_TEMPLATE/question.md
vendored
@ -20,7 +20,12 @@ Output of docker-compose logs or kubectl logs respectively
|
|||||||
|
|
||||||
**Version and Deployment (please complete the following information):**
|
**Version and Deployment (please complete the following information):**
|
||||||
|
|
||||||
- authentik version: [e.g. 2021.8.5]
|
<!--
|
||||||
|
Notice: authentik supports installation via Docker, Kubernetes, and AWS CloudFormation only. Support is not available for other methods. For detailed installation and configuration instructions, please refer to the official documentation at https://docs.goauthentik.io/docs/install-config/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
- authentik version: [e.g. 2025.2.0]
|
||||||
- Deployment: [e.g. docker-compose, helm]
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
@ -35,14 +35,6 @@ runs:
|
|||||||
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
|
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
|
||||||
```
|
```
|
||||||
|
|
||||||
For arm64, use these values:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
AUTHENTIK_IMAGE=ghcr.io/goauthentik/dev-server
|
|
||||||
AUTHENTIK_TAG=${{ inputs.tag }}-arm64
|
|
||||||
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
|
|
||||||
```
|
|
||||||
|
|
||||||
Afterwards, run the upgrade commands from the latest release notes.
|
Afterwards, run the upgrade commands from the latest release notes.
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
@ -60,18 +52,6 @@ runs:
|
|||||||
tag: ${{ inputs.tag }}
|
tag: ${{ inputs.tag }}
|
||||||
```
|
```
|
||||||
|
|
||||||
For arm64, use these values:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
authentik:
|
|
||||||
outposts:
|
|
||||||
container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
|
|
||||||
global:
|
|
||||||
image:
|
|
||||||
repository: ghcr.io/goauthentik/dev-server
|
|
||||||
tag: ${{ inputs.tag }}-arm64
|
|
||||||
```
|
|
||||||
|
|
||||||
Afterwards, run the upgrade commands from the latest release notes.
|
Afterwards, run the upgrade commands from the latest release notes.
|
||||||
</details>
|
</details>
|
||||||
edit-mode: replace
|
edit-mode: replace
|
||||||
|
14
.github/actions/docker-push-variables/action.yml
vendored
14
.github/actions/docker-push-variables/action.yml
vendored
@ -9,6 +9,9 @@ inputs:
|
|||||||
image-arch:
|
image-arch:
|
||||||
required: false
|
required: false
|
||||||
description: "Docker image arch"
|
description: "Docker image arch"
|
||||||
|
release:
|
||||||
|
required: true
|
||||||
|
description: "True if this is a release build, false if this is a dev/PR build"
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
shouldPush:
|
shouldPush:
|
||||||
@ -29,15 +32,24 @@ outputs:
|
|||||||
imageTags:
|
imageTags:
|
||||||
description: "Docker image tags"
|
description: "Docker image tags"
|
||||||
value: ${{ steps.ev.outputs.imageTags }}
|
value: ${{ steps.ev.outputs.imageTags }}
|
||||||
|
imageTagsJSON:
|
||||||
|
description: "Docker image tags, as a JSON array"
|
||||||
|
value: ${{ steps.ev.outputs.imageTagsJSON }}
|
||||||
attestImageNames:
|
attestImageNames:
|
||||||
description: "Docker image names used for attestation"
|
description: "Docker image names used for attestation"
|
||||||
value: ${{ steps.ev.outputs.attestImageNames }}
|
value: ${{ steps.ev.outputs.attestImageNames }}
|
||||||
|
cacheTo:
|
||||||
|
description: "cache-to value for the docker build step"
|
||||||
|
value: ${{ steps.ev.outputs.cacheTo }}
|
||||||
imageMainTag:
|
imageMainTag:
|
||||||
description: "Docker image main tag"
|
description: "Docker image main tag"
|
||||||
value: ${{ steps.ev.outputs.imageMainTag }}
|
value: ${{ steps.ev.outputs.imageMainTag }}
|
||||||
imageMainName:
|
imageMainName:
|
||||||
description: "Docker image main name"
|
description: "Docker image main name"
|
||||||
value: ${{ steps.ev.outputs.imageMainName }}
|
value: ${{ steps.ev.outputs.imageMainName }}
|
||||||
|
imageBuildArgs:
|
||||||
|
description: "Docker image build args"
|
||||||
|
value: ${{ steps.ev.outputs.imageBuildArgs }}
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
@ -48,6 +60,8 @@ runs:
|
|||||||
env:
|
env:
|
||||||
IMAGE_NAME: ${{ inputs.image-name }}
|
IMAGE_NAME: ${{ inputs.image-name }}
|
||||||
IMAGE_ARCH: ${{ inputs.image-arch }}
|
IMAGE_ARCH: ${{ inputs.image-arch }}
|
||||||
|
RELEASE: ${{ inputs.release }}
|
||||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
REF: ${{ github.ref }}
|
||||||
run: |
|
run: |
|
||||||
python3 ${{ github.action_path }}/push_vars.py
|
python3 ${{ github.action_path }}/push_vars.py
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
from json import dumps
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
parser = configparser.ConfigParser()
|
parser = configparser.ConfigParser()
|
||||||
@ -43,12 +44,11 @@ if is_release:
|
|||||||
]
|
]
|
||||||
if not prerelease:
|
if not prerelease:
|
||||||
image_tags += [
|
image_tags += [
|
||||||
f"{name}:latest",
|
|
||||||
f"{name}:{version_family}",
|
f"{name}:{version_family}",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
suffix = ""
|
suffix = ""
|
||||||
if image_arch and image_arch != "amd64":
|
if image_arch:
|
||||||
suffix = f"-{image_arch}"
|
suffix = f"-{image_arch}"
|
||||||
for name in image_names:
|
for name in image_names:
|
||||||
image_tags += [
|
image_tags += [
|
||||||
@ -70,12 +70,31 @@ def get_attest_image_names(image_with_tags: list[str]):
|
|||||||
return ",".join(set(image_tags))
|
return ",".join(set(image_tags))
|
||||||
|
|
||||||
|
|
||||||
|
# Generate `cache-to` param
|
||||||
|
cache_to = ""
|
||||||
|
if should_push:
|
||||||
|
_cache_tag = "buildcache"
|
||||||
|
if image_arch:
|
||||||
|
_cache_tag += f"-{image_arch}"
|
||||||
|
cache_to = f"type=registry,ref={get_attest_image_names(image_tags)}:{_cache_tag},mode=max"
|
||||||
|
|
||||||
|
|
||||||
|
image_build_args = []
|
||||||
|
if os.getenv("RELEASE", "false").lower() == "true":
|
||||||
|
image_build_args = [f"VERSION={os.getenv('REF')}"]
|
||||||
|
else:
|
||||||
|
image_build_args = [f"GIT_BUILD_HASH={sha}"]
|
||||||
|
image_build_args = "\n".join(image_build_args)
|
||||||
|
|
||||||
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||||
print(f"shouldPush={str(should_push).lower()}", file=_output)
|
print(f"shouldPush={str(should_push).lower()}", file=_output)
|
||||||
print(f"sha={sha}", file=_output)
|
print(f"sha={sha}", file=_output)
|
||||||
print(f"version={version}", file=_output)
|
print(f"version={version}", file=_output)
|
||||||
print(f"prerelease={prerelease}", file=_output)
|
print(f"prerelease={prerelease}", file=_output)
|
||||||
print(f"imageTags={','.join(image_tags)}", file=_output)
|
print(f"imageTags={','.join(image_tags)}", file=_output)
|
||||||
|
print(f"imageTagsJSON={dumps(image_tags)}", file=_output)
|
||||||
print(f"attestImageNames={get_attest_image_names(image_tags)}", file=_output)
|
print(f"attestImageNames={get_attest_image_names(image_tags)}", file=_output)
|
||||||
print(f"imageMainTag={image_main_tag}", file=_output)
|
print(f"imageMainTag={image_main_tag}", file=_output)
|
||||||
print(f"imageMainName={image_tags[0]}", file=_output)
|
print(f"imageMainName={image_tags[0]}", file=_output)
|
||||||
|
print(f"cacheTo={cache_to}", file=_output)
|
||||||
|
print(f"imageBuildArgs={image_build_args}", file=_output)
|
||||||
|
11
.github/actions/docker-push-variables/test.sh
vendored
11
.github/actions/docker-push-variables/test.sh
vendored
@ -1,7 +1,18 @@
|
|||||||
#!/bin/bash -x
|
#!/bin/bash -x
|
||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
# Non-pushing PR
|
||||||
GITHUB_OUTPUT=/dev/stdout \
|
GITHUB_OUTPUT=/dev/stdout \
|
||||||
GITHUB_REF=ref \
|
GITHUB_REF=ref \
|
||||||
GITHUB_SHA=sha \
|
GITHUB_SHA=sha \
|
||||||
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
||||||
|
GITHUB_REPOSITORY=goauthentik/authentik \
|
||||||
|
python $SCRIPT_DIR/push_vars.py
|
||||||
|
|
||||||
|
# Pushing PR/main
|
||||||
|
GITHUB_OUTPUT=/dev/stdout \
|
||||||
|
GITHUB_REF=ref \
|
||||||
|
GITHUB_SHA=sha \
|
||||||
|
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
||||||
|
GITHUB_REPOSITORY=goauthentik/authentik \
|
||||||
|
DOCKER_USERNAME=foo \
|
||||||
python $SCRIPT_DIR/push_vars.py
|
python $SCRIPT_DIR/push_vars.py
|
||||||
|
20
.github/actions/setup/action.yml
vendored
20
.github/actions/setup/action.yml
vendored
@ -9,17 +9,22 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Install poetry & deps
|
- name: Install apt deps
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pipx install poetry || true
|
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
|
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
|
||||||
- name: Setup python and restore poetry
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
- name: Setup python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version-file: "pyproject.toml"
|
python-version-file: "pyproject.toml"
|
||||||
cache: "poetry"
|
- name: Install Python deps
|
||||||
|
shell: bash
|
||||||
|
run: uv sync --all-extras --dev --frozen
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@ -30,15 +35,18 @@ runs:
|
|||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
|
- name: Setup docker cache
|
||||||
|
uses: AndreKurait/docker-cache@0fe76702a40db986d9663c24954fc14c6a6031b7
|
||||||
|
with:
|
||||||
|
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
|
||||||
- name: Setup dependencies
|
- name: Setup dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||||
docker compose -f .github/actions/setup/docker-compose.yml up -d
|
docker compose -f .github/actions/setup/docker-compose.yml up -d
|
||||||
poetry install --sync
|
|
||||||
cd web && npm ci
|
cd web && npm ci
|
||||||
- name: Generate config
|
- name: Generate config
|
||||||
shell: poetry run python {0}
|
shell: uv run python {0}
|
||||||
run: |
|
run: |
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from yaml import safe_dump
|
from yaml import safe_dump
|
||||||
|
2
.github/actions/setup/docker-compose.yml
vendored
2
.github/actions/setup/docker-compose.yml
vendored
@ -11,7 +11,7 @@ services:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
restart: always
|
restart: always
|
||||||
redis:
|
redis:
|
||||||
image: docker.io/library/redis
|
image: docker.io/library/redis:7
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
restart: always
|
restart: always
|
||||||
|
31
.github/codespell-words.txt
vendored
31
.github/codespell-words.txt
vendored
@ -1,7 +1,32 @@
|
|||||||
|
akadmin
|
||||||
|
asgi
|
||||||
|
assertIn
|
||||||
|
authentik
|
||||||
|
authn
|
||||||
|
crate
|
||||||
|
docstrings
|
||||||
|
entra
|
||||||
|
goauthentik
|
||||||
|
gunicorn
|
||||||
|
hass
|
||||||
|
jwe
|
||||||
|
jwks
|
||||||
keypair
|
keypair
|
||||||
keypairs
|
keypairs
|
||||||
hass
|
kubernetes
|
||||||
warmup
|
oidc
|
||||||
ontext
|
ontext
|
||||||
|
openid
|
||||||
|
passwordless
|
||||||
|
plex
|
||||||
|
saml
|
||||||
|
scim
|
||||||
singed
|
singed
|
||||||
assertIn
|
slo
|
||||||
|
sso
|
||||||
|
totp
|
||||||
|
traefik
|
||||||
|
# https://github.com/codespell-project/codespell/issues/1224
|
||||||
|
upToDate
|
||||||
|
warmup
|
||||||
|
webauthn
|
||||||
|
44
.github/dependabot.yml
vendored
44
.github/dependabot.yml
vendored
@ -23,7 +23,13 @@ updates:
|
|||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directories:
|
directories:
|
||||||
- "/web"
|
- "/web"
|
||||||
- "/web/sfe"
|
- "/web/packages/sfe"
|
||||||
|
- "/web/packages/core"
|
||||||
|
- "/web/packages/esbuild-plugin-live-reload"
|
||||||
|
- "/packages/prettier-config"
|
||||||
|
- "/packages/tsconfig"
|
||||||
|
- "/packages/docusaurus-config"
|
||||||
|
- "/packages/eslint-config"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "04:00"
|
time: "04:00"
|
||||||
@ -68,6 +74,9 @@ updates:
|
|||||||
wdio:
|
wdio:
|
||||||
patterns:
|
patterns:
|
||||||
- "@wdio/*"
|
- "@wdio/*"
|
||||||
|
goauthentik:
|
||||||
|
patterns:
|
||||||
|
- "@goauthentik/*"
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/website"
|
directory: "/website"
|
||||||
schedule:
|
schedule:
|
||||||
@ -82,7 +91,26 @@ updates:
|
|||||||
docusaurus:
|
docusaurus:
|
||||||
patterns:
|
patterns:
|
||||||
- "@docusaurus/*"
|
- "@docusaurus/*"
|
||||||
- package-ecosystem: pip
|
build:
|
||||||
|
patterns:
|
||||||
|
- "@swc/*"
|
||||||
|
- "swc-*"
|
||||||
|
- "lightningcss*"
|
||||||
|
- "@rspack/binding*"
|
||||||
|
goauthentik:
|
||||||
|
patterns:
|
||||||
|
- "@goauthentik/*"
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/lifecycle/aws"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "04:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
commit-message:
|
||||||
|
prefix: "lifecycle/aws:"
|
||||||
|
labels:
|
||||||
|
- dependencies
|
||||||
|
- package-ecosystem: uv
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
@ -102,3 +130,15 @@ updates:
|
|||||||
prefix: "core:"
|
prefix: "core:"
|
||||||
labels:
|
labels:
|
||||||
- dependencies
|
- dependencies
|
||||||
|
- package-ecosystem: docker-compose
|
||||||
|
directories:
|
||||||
|
# - /scripts # Maybe
|
||||||
|
- /tests/e2e
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "04:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
commit-message:
|
||||||
|
prefix: "core:"
|
||||||
|
labels:
|
||||||
|
- dependencies
|
||||||
|
96
.github/workflows/_reusable-docker-build-single.yaml
vendored
Normal file
96
.github/workflows/_reusable-docker-build-single.yaml
vendored
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Re-usable workflow for a single-architecture build
|
||||||
|
name: Single-arch Container build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
image_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
image_arch:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
runs-on:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
registry_dockerhub:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
registry_ghcr:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
release:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
outputs:
|
||||||
|
image-digest:
|
||||||
|
value: ${{ jobs.build.outputs.image-digest }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build ${{ inputs.image_arch }}
|
||||||
|
runs-on: ${{ inputs.runs-on }}
|
||||||
|
outputs:
|
||||||
|
image-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
permissions:
|
||||||
|
# Needed to upload container images to ghcr.io
|
||||||
|
packages: write
|
||||||
|
# Needed for attestation
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: docker/setup-qemu-action@v3.6.0
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
- name: prepare variables
|
||||||
|
uses: ./.github/actions/docker-push-variables
|
||||||
|
id: ev
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
with:
|
||||||
|
image-name: ${{ inputs.image_name }}
|
||||||
|
image-arch: ${{ inputs.image_arch }}
|
||||||
|
release: ${{ inputs.release }}
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ inputs.registry_dockerhub }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: ${{ inputs.registry_ghcr }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: make empty clients
|
||||||
|
if: ${{ inputs.release }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ./gen-ts-api
|
||||||
|
mkdir -p ./gen-go-api
|
||||||
|
- name: generate ts client
|
||||||
|
if: ${{ !inputs.release }}
|
||||||
|
run: make gen-client-ts
|
||||||
|
- name: Build Docker Image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
id: push
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||||
|
secrets: |
|
||||||
|
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
||||||
|
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
|
||||||
|
build-args: |
|
||||||
|
${{ steps.ev.outputs.imageBuildArgs }}
|
||||||
|
tags: ${{ steps.ev.outputs.imageTags }}
|
||||||
|
platforms: linux/${{ inputs.image_arch }}
|
||||||
|
cache-from: type=registry,ref=${{ steps.ev.outputs.attestImageNames }}:buildcache-${{ inputs.image_arch }}
|
||||||
|
cache-to: ${{ steps.ev.outputs.cacheTo }}
|
||||||
|
- uses: actions/attest-build-provenance@v2
|
||||||
|
id: attest
|
||||||
|
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||||
|
with:
|
||||||
|
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||||
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
push-to-registry: true
|
104
.github/workflows/_reusable-docker-build.yaml
vendored
Normal file
104
.github/workflows/_reusable-docker-build.yaml
vendored
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Re-usable workflow for a multi-architecture build
|
||||||
|
name: Multi-arch container build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
image_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
registry_dockerhub:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
registry_ghcr:
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
release:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
outputs: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-server-amd64:
|
||||||
|
uses: ./.github/workflows/_reusable-docker-build-single.yaml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
image_name: ${{ inputs.image_name }}
|
||||||
|
image_arch: amd64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
registry_dockerhub: ${{ inputs.registry_dockerhub }}
|
||||||
|
registry_ghcr: ${{ inputs.registry_ghcr }}
|
||||||
|
release: ${{ inputs.release }}
|
||||||
|
build-server-arm64:
|
||||||
|
uses: ./.github/workflows/_reusable-docker-build-single.yaml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
image_name: ${{ inputs.image_name }}
|
||||||
|
image_arch: arm64
|
||||||
|
runs-on: ubuntu-22.04-arm
|
||||||
|
registry_dockerhub: ${{ inputs.registry_dockerhub }}
|
||||||
|
registry_ghcr: ${{ inputs.registry_ghcr }}
|
||||||
|
release: ${{ inputs.release }}
|
||||||
|
get-tags:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build-server-amd64
|
||||||
|
- build-server-arm64
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.ev.outputs.imageTagsJSON }}
|
||||||
|
shouldPush: ${{ steps.ev.outputs.shouldPush }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: prepare variables
|
||||||
|
uses: ./.github/actions/docker-push-variables
|
||||||
|
id: ev
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
with:
|
||||||
|
image-name: ${{ inputs.image_name }}
|
||||||
|
merge-server:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ needs.get-tags.outputs.shouldPush == 'true' }}
|
||||||
|
needs:
|
||||||
|
- get-tags
|
||||||
|
- build-server-amd64
|
||||||
|
- build-server-arm64
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
tag: ${{ fromJson(needs.get-tags.outputs.tags) }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: prepare variables
|
||||||
|
uses: ./.github/actions/docker-push-variables
|
||||||
|
id: ev
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
with:
|
||||||
|
image-name: ${{ inputs.image_name }}
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ inputs.registry_dockerhub }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: ${{ inputs.registry_ghcr }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: int128/docker-manifest-create-action@v2
|
||||||
|
id: build
|
||||||
|
with:
|
||||||
|
tags: ${{ matrix.tag }}
|
||||||
|
sources: |
|
||||||
|
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-amd64.outputs.image-digest }}
|
||||||
|
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-arm64.outputs.image-digest }}
|
||||||
|
- uses: actions/attest-build-provenance@v2
|
||||||
|
id: attest
|
||||||
|
with:
|
||||||
|
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||||
|
subject-digest: ${{ steps.build.outputs.digest }}
|
||||||
|
push-to-registry: true
|
1
.github/workflows/api-py-publish.yml
vendored
1
.github/workflows/api-py-publish.yml
vendored
@ -30,7 +30,6 @@ jobs:
|
|||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version-file: "pyproject.toml"
|
python-version-file: "pyproject.toml"
|
||||||
cache: "poetry"
|
|
||||||
- name: Generate API Client
|
- name: Generate API Client
|
||||||
run: make gen-client-py
|
run: make gen-client-py
|
||||||
- name: Publish package
|
- name: Publish package
|
||||||
|
1
.github/workflows/api-ts-publish.yml
vendored
1
.github/workflows/api-ts-publish.yml
vendored
@ -53,6 +53,7 @@ jobs:
|
|||||||
signoff: true
|
signoff: true
|
||||||
# ID from https://api.github.com/users/authentik-automation[bot]
|
# ID from https://api.github.com/users/authentik-automation[bot]
|
||||||
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
|
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
|
||||||
|
labels: dependencies
|
||||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
8
.github/workflows/ci-aws-cfn.yml
vendored
8
.github/workflows/ci-aws-cfn.yml
vendored
@ -25,15 +25,15 @@ jobs:
|
|||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: website/package.json
|
node-version-file: lifecycle/aws/package.json
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: website/package-lock.json
|
cache-dependency-path: lifecycle/aws/package-lock.json
|
||||||
- working-directory: website/
|
- working-directory: lifecycle/aws/
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
- name: Check changes have been applied
|
- name: Check changes have been applied
|
||||||
run: |
|
run: |
|
||||||
poetry run make aws-cfn
|
uv run make aws-cfn
|
||||||
git diff --exit-code
|
git diff --exit-code
|
||||||
ci-aws-cfn-mark:
|
ci-aws-cfn-mark:
|
||||||
if: always()
|
if: always()
|
||||||
|
28
.github/workflows/ci-main-daily.yml
vendored
Normal file
28
.github/workflows/ci-main-daily.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: authentik-ci-main-daily
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
# Every night at 3am
|
||||||
|
- cron: "0 3 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-container:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
version:
|
||||||
|
- docs
|
||||||
|
- version-2025-2
|
||||||
|
- version-2024-12
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: |
|
||||||
|
current="$(pwd)"
|
||||||
|
dir="/tmp/authentik/${{ matrix.version }}"
|
||||||
|
mkdir -p $dir
|
||||||
|
cd $dir
|
||||||
|
wget https://${{ matrix.version }}.goauthentik.io/docker-compose.yml
|
||||||
|
${current}/scripts/test_docker.sh
|
124
.github/workflows/ci-main.yml
vendored
124
.github/workflows/ci-main.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: run job
|
- name: run job
|
||||||
run: poetry run make ci-${{ matrix.job }}
|
run: uv run make ci-${{ matrix.job }}
|
||||||
test-migrations:
|
test-migrations:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -42,24 +42,34 @@ jobs:
|
|||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: run migrations
|
- name: run migrations
|
||||||
run: poetry run python -m lifecycle.migrate
|
run: uv run python -m lifecycle.migrate
|
||||||
test-migrations-from-stable:
|
test-make-seed:
|
||||||
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- id: seed
|
||||||
|
run: |
|
||||||
|
echo "seed=$(printf "%d\n" "0x$(openssl rand -hex 4)")" >> "$GITHUB_OUTPUT"
|
||||||
|
outputs:
|
||||||
|
seed: ${{ steps.seed.outputs.seed }}
|
||||||
|
test-migrations-from-stable:
|
||||||
|
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
needs: test-make-seed
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
psql:
|
psql:
|
||||||
- 15-alpine
|
- 15-alpine
|
||||||
- 16-alpine
|
- 16-alpine
|
||||||
|
- 17-alpine
|
||||||
|
run_id: [1, 2, 3, 4, 5]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: checkout stable
|
- name: checkout stable
|
||||||
run: |
|
run: |
|
||||||
# Delete all poetry envs
|
|
||||||
rm -rf /home/runner/.cache/pypoetry
|
|
||||||
# Copy current, latest config to local
|
# Copy current, latest config to local
|
||||||
cp authentik/lib/default.yml local.env.yml
|
cp authentik/lib/default.yml local.env.yml
|
||||||
cp -R .github ..
|
cp -R .github ..
|
||||||
@ -72,7 +82,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
postgresql_version: ${{ matrix.psql }}
|
postgresql_version: ${{ matrix.psql }}
|
||||||
- name: run migrations to stable
|
- name: run migrations to stable
|
||||||
run: poetry run python -m lifecycle.migrate
|
run: uv run python -m lifecycle.migrate
|
||||||
- name: checkout current code
|
- name: checkout current code
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
@ -80,31 +90,35 @@ jobs:
|
|||||||
git reset --hard HEAD
|
git reset --hard HEAD
|
||||||
git clean -d -fx .
|
git clean -d -fx .
|
||||||
git checkout $GITHUB_SHA
|
git checkout $GITHUB_SHA
|
||||||
# Delete previous poetry env
|
|
||||||
rm -rf /home/runner/.cache/pypoetry/virtualenvs/*
|
|
||||||
- name: Setup authentik env (ensure latest deps are installed)
|
- name: Setup authentik env (ensure latest deps are installed)
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
postgresql_version: ${{ matrix.psql }}
|
postgresql_version: ${{ matrix.psql }}
|
||||||
- name: migrate to latest
|
- name: migrate to latest
|
||||||
run: |
|
run: |
|
||||||
poetry run python -m lifecycle.migrate
|
uv run python -m lifecycle.migrate
|
||||||
- name: run tests
|
- name: run tests
|
||||||
env:
|
env:
|
||||||
# Test in the main database that we just migrated from the previous stable version
|
# Test in the main database that we just migrated from the previous stable version
|
||||||
AUTHENTIK_POSTGRESQL__TEST__NAME: authentik
|
AUTHENTIK_POSTGRESQL__TEST__NAME: authentik
|
||||||
|
CI_TEST_SEED: ${{ needs.test-make-seed.outputs.seed }}
|
||||||
|
CI_RUN_ID: ${{ matrix.run_id }}
|
||||||
|
CI_TOTAL_RUNS: "5"
|
||||||
run: |
|
run: |
|
||||||
poetry run make test
|
uv run make ci-test
|
||||||
test-unittest:
|
test-unittest:
|
||||||
name: test-unittest - PostgreSQL ${{ matrix.psql }}
|
name: test-unittest - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 20
|
||||||
|
needs: test-make-seed
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
psql:
|
psql:
|
||||||
- 15-alpine
|
- 15-alpine
|
||||||
- 16-alpine
|
- 16-alpine
|
||||||
|
- 17-alpine
|
||||||
|
run_id: [1, 2, 3, 4, 5]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
@ -112,9 +126,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
postgresql_version: ${{ matrix.psql }}
|
postgresql_version: ${{ matrix.psql }}
|
||||||
- name: run unittest
|
- name: run unittest
|
||||||
|
env:
|
||||||
|
CI_TEST_SEED: ${{ needs.test-make-seed.outputs.seed }}
|
||||||
|
CI_RUN_ID: ${{ matrix.run_id }}
|
||||||
|
CI_TOTAL_RUNS: "5"
|
||||||
run: |
|
run: |
|
||||||
poetry run make test
|
uv run make ci-test
|
||||||
poetry run coverage xml
|
|
||||||
- if: ${{ always() }}
|
- if: ${{ always() }}
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
@ -134,11 +151,11 @@ jobs:
|
|||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Create k8s Kind Cluster
|
- name: Create k8s Kind Cluster
|
||||||
uses: helm/kind-action@v1.11.0
|
uses: helm/kind-action@v1.12.0
|
||||||
- name: run integration
|
- name: run integration
|
||||||
run: |
|
run: |
|
||||||
poetry run coverage run manage.py test tests/integration
|
uv run coverage run manage.py test tests/integration
|
||||||
poetry run coverage xml
|
uv run coverage xml
|
||||||
- if: ${{ always() }}
|
- if: ${{ always() }}
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
@ -185,7 +202,7 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: web/dist
|
path: web/dist
|
||||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**') }}
|
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||||
- name: prepare web ui
|
- name: prepare web ui
|
||||||
if: steps.cache-web.outputs.cache-hit != 'true'
|
if: steps.cache-web.outputs.cache-hit != 'true'
|
||||||
working-directory: web
|
working-directory: web
|
||||||
@ -193,10 +210,11 @@ jobs:
|
|||||||
npm ci
|
npm ci
|
||||||
make -C .. gen-client-ts
|
make -C .. gen-client-ts
|
||||||
npm run build
|
npm run build
|
||||||
|
npm run build:sfe
|
||||||
- name: run e2e
|
- name: run e2e
|
||||||
run: |
|
run: |
|
||||||
poetry run coverage run manage.py test ${{ matrix.job.glob }}
|
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||||
poetry run coverage xml
|
uv run coverage xml
|
||||||
- if: ${{ always() }}
|
- if: ${{ always() }}
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
@ -223,68 +241,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
jobs: ${{ toJSON(needs) }}
|
jobs: ${{ toJSON(needs) }}
|
||||||
build:
|
build:
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
arch:
|
|
||||||
- amd64
|
|
||||||
- arm64
|
|
||||||
needs: ci-core-mark
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
permissions:
|
||||||
# Needed to upload contianer images to ghcr.io
|
# Needed to upload container images to ghcr.io
|
||||||
packages: write
|
packages: write
|
||||||
# Needed for attestation
|
# Needed for attestation
|
||||||
id-token: write
|
id-token: write
|
||||||
attestations: write
|
attestations: write
|
||||||
timeout-minutes: 120
|
needs: ci-core-mark
|
||||||
steps:
|
uses: ./.github/workflows/_reusable-docker-build.yaml
|
||||||
- uses: actions/checkout@v4
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
image_name: ghcr.io/goauthentik/dev-server
|
||||||
- name: Set up QEMU
|
release: false
|
||||||
uses: docker/setup-qemu-action@v3.2.0
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: prepare variables
|
|
||||||
uses: ./.github/actions/docker-push-variables
|
|
||||||
id: ev
|
|
||||||
env:
|
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
with:
|
|
||||||
image-name: ghcr.io/goauthentik/dev-server
|
|
||||||
image-arch: ${{ matrix.arch }}
|
|
||||||
- name: Login to Container Registry
|
|
||||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: generate ts client
|
|
||||||
run: make gen-client-ts
|
|
||||||
- name: Build Docker Image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
id: push
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
secrets: |
|
|
||||||
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
|
||||||
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
|
|
||||||
tags: ${{ steps.ev.outputs.imageTags }}
|
|
||||||
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
|
||||||
build-args: |
|
|
||||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
|
||||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache
|
|
||||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache,mode=max' || '' }}
|
|
||||||
platforms: linux/${{ matrix.arch }}
|
|
||||||
- uses: actions/attest-build-provenance@v2
|
|
||||||
id: attest
|
|
||||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
|
||||||
with:
|
|
||||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
pr-comment:
|
pr-comment:
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
|
6
.github/workflows/ci-outpost.yml
vendored
6
.github/workflows/ci-outpost.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: Generate API
|
- name: Generate API
|
||||||
run: make gen-client-go
|
run: make gen-client-go
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v8
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout 5000s --verbose
|
args: --timeout 5000s --verbose
|
||||||
@ -72,7 +72,7 @@ jobs:
|
|||||||
- rac
|
- rac
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
# Needed to upload contianer images to ghcr.io
|
# Needed to upload container images to ghcr.io
|
||||||
packages: write
|
packages: write
|
||||||
# Needed for attestation
|
# Needed for attestation
|
||||||
id-token: write
|
id-token: write
|
||||||
@ -82,7 +82,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.2.0
|
uses: docker/setup-qemu-action@v3.6.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: prepare variables
|
- name: prepare variables
|
||||||
|
@ -2,7 +2,7 @@ name: authentik-gen-update-webauthn-mds
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '30 1 1,15 * *'
|
- cron: "30 1 1,15 * *"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
POSTGRES_DB: authentik
|
POSTGRES_DB: authentik
|
||||||
@ -24,7 +24,7 @@ jobs:
|
|||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- run: poetry run ak update_webauthn_mds
|
- run: uv run ak update_webauthn_mds
|
||||||
- uses: peter-evans/create-pull-request@v7
|
- uses: peter-evans/create-pull-request@v7
|
||||||
id: cpr
|
id: cpr
|
||||||
with:
|
with:
|
||||||
@ -37,6 +37,7 @@ jobs:
|
|||||||
signoff: true
|
signoff: true
|
||||||
# ID from https://api.github.com/users/authentik-automation[bot]
|
# ID from https://api.github.com/users/authentik-automation[bot]
|
||||||
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
|
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
|
||||||
|
labels: dependencies
|
||||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
1
.github/workflows/image-compress.yml
vendored
1
.github/workflows/image-compress.yml
vendored
@ -53,6 +53,7 @@ jobs:
|
|||||||
body: ${{ steps.compress.outputs.markdown }}
|
body: ${{ steps.compress.outputs.markdown }}
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
signoff: true
|
signoff: true
|
||||||
|
labels: dependencies
|
||||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||||
if: "${{ github.event_name != 'pull_request' && steps.compress.outputs.markdown != '' }}"
|
if: "${{ github.event_name != 'pull_request' && steps.compress.outputs.markdown != '' }}"
|
||||||
with:
|
with:
|
||||||
|
47
.github/workflows/packages-npm-publish.yml
vendored
Normal file
47
.github/workflows/packages-npm-publish.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
name: authentik-packages-npm-publish
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- packages/docusaurus-config/**
|
||||||
|
- packages/eslint-config/**
|
||||||
|
- packages/prettier-config/**
|
||||||
|
- packages/tsconfig/**
|
||||||
|
- web/packages/esbuild-plugin-live-reload/**
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
package:
|
||||||
|
- packages/docusaurus-config
|
||||||
|
- packages/eslint-config
|
||||||
|
- packages/prettier-config
|
||||||
|
- packages/tsconfig
|
||||||
|
- web/packages/esbuild-plugin-live-reload
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: ${{ matrix.package }}/package.json
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
${{ matrix.package }}/package.json
|
||||||
|
- name: Publish package
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
working-directory: ${{ matrix.package }}
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
npm publish
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
4
.github/workflows/publish-source-docs.yml
vendored
4
.github/workflows/publish-source-docs.yml
vendored
@ -21,8 +21,8 @@ jobs:
|
|||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: generate docs
|
- name: generate docs
|
||||||
run: |
|
run: |
|
||||||
poetry run make migrate
|
uv run make migrate
|
||||||
poetry run ak build_source_docs
|
uv run ak build_source_docs
|
||||||
- name: Publish
|
- name: Publish
|
||||||
uses: netlify/actions/cli@master
|
uses: netlify/actions/cli@master
|
||||||
with:
|
with:
|
||||||
|
67
.github/workflows/release-publish.yml
vendored
67
.github/workflows/release-publish.yml
vendored
@ -7,64 +7,23 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-server:
|
build-server:
|
||||||
runs-on: ubuntu-latest
|
uses: ./.github/workflows/_reusable-docker-build.yaml
|
||||||
|
secrets: inherit
|
||||||
permissions:
|
permissions:
|
||||||
# Needed to upload contianer images to ghcr.io
|
# Needed to upload container images to ghcr.io
|
||||||
packages: write
|
packages: write
|
||||||
# Needed for attestation
|
# Needed for attestation
|
||||||
id-token: write
|
id-token: write
|
||||||
attestations: write
|
attestations: write
|
||||||
steps:
|
with:
|
||||||
- uses: actions/checkout@v4
|
image_name: ghcr.io/goauthentik/server,beryju/authentik
|
||||||
- name: Set up QEMU
|
release: true
|
||||||
uses: docker/setup-qemu-action@v3.2.0
|
registry_dockerhub: true
|
||||||
- name: Set up Docker Buildx
|
registry_ghcr: true
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: prepare variables
|
|
||||||
uses: ./.github/actions/docker-push-variables
|
|
||||||
id: ev
|
|
||||||
env:
|
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
with:
|
|
||||||
image-name: ghcr.io/goauthentik/server,beryju/authentik
|
|
||||||
- name: Docker Login Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: make empty clients
|
|
||||||
run: |
|
|
||||||
mkdir -p ./gen-ts-api
|
|
||||||
mkdir -p ./gen-go-api
|
|
||||||
- name: Build Docker Image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
id: push
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
secrets: |
|
|
||||||
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
|
||||||
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
|
|
||||||
build-args: |
|
|
||||||
VERSION=${{ github.ref }}
|
|
||||||
tags: ${{ steps.ev.outputs.imageTags }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
- uses: actions/attest-build-provenance@v2
|
|
||||||
id: attest
|
|
||||||
with:
|
|
||||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
build-outpost:
|
build-outpost:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
# Needed to upload contianer images to ghcr.io
|
# Needed to upload container images to ghcr.io
|
||||||
packages: write
|
packages: write
|
||||||
# Needed for attestation
|
# Needed for attestation
|
||||||
id-token: write
|
id-token: write
|
||||||
@ -83,7 +42,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.2.0
|
uses: docker/setup-qemu-action@v3.6.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: prepare variables
|
- name: prepare variables
|
||||||
@ -188,8 +147,8 @@ jobs:
|
|||||||
aws-region: ${{ env.AWS_REGION }}
|
aws-region: ${{ env.AWS_REGION }}
|
||||||
- name: Upload template
|
- name: Upload template
|
||||||
run: |
|
run: |
|
||||||
aws s3 cp website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
|
aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
|
||||||
aws s3 cp website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
|
aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
|
||||||
test-release:
|
test-release:
|
||||||
needs:
|
needs:
|
||||||
- build-server
|
- build-server
|
||||||
@ -227,7 +186,7 @@ jobs:
|
|||||||
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
|
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
|
||||||
docker cp ${container}:web/ .
|
docker cp ${container}:web/ .
|
||||||
- name: Create a Sentry.io release
|
- name: Create a Sentry.io release
|
||||||
uses: getsentry/action-release@v1
|
uses: getsentry/action-release@v3
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
11
.github/workflows/release-tag.yml
vendored
11
.github/workflows/release-tag.yml
vendored
@ -14,16 +14,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Pre-release test
|
- name: Pre-release test
|
||||||
run: |
|
run: |
|
||||||
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
|
make test-docker
|
||||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
|
|
||||||
docker buildx install
|
|
||||||
mkdir -p ./gen-ts-api
|
|
||||||
docker build -t testing:latest .
|
|
||||||
echo "AUTHENTIK_IMAGE=testing" >> .env
|
|
||||||
echo "AUTHENTIK_TAG=latest" >> .env
|
|
||||||
docker compose up --no-start
|
|
||||||
docker compose start postgresql redis
|
|
||||||
docker compose run -u root server test-all
|
|
||||||
- id: generate_token
|
- id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
|
6
.github/workflows/repo-stale.yml
vendored
6
.github/workflows/repo-stale.yml
vendored
@ -1,8 +1,8 @@
|
|||||||
name: 'authentik-repo-stale'
|
name: "authentik-repo-stale"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '30 1 * * *'
|
- cron: "30 1 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@ -25,7 +25,7 @@ jobs:
|
|||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
days-before-close: 7
|
days-before-close: 7
|
||||||
exempt-issue-labels: pinned,security,pr_wanted,enhancement,bug/confirmed,enhancement/confirmed,question,status/reviewing
|
exempt-issue-labels: pinned,security,pr_wanted,enhancement,bug/confirmed,enhancement/confirmed,question,status/reviewing
|
||||||
stale-issue-label: wontfix
|
stale-issue-label: status/stale
|
||||||
stale-issue-message: >
|
stale-issue-message: >
|
||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
27
.github/workflows/semgrep.yml
vendored
Normal file
27
.github/workflows/semgrep.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name: authentik-semgrep
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
pull_request: {}
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- .github/workflows/semgrep.yml
|
||||||
|
schedule:
|
||||||
|
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
|
||||||
|
- cron: '12 15 * * *'
|
||||||
|
jobs:
|
||||||
|
semgrep:
|
||||||
|
name: semgrep/ci
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
env:
|
||||||
|
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
||||||
|
container:
|
||||||
|
image: semgrep/semgrep
|
||||||
|
if: (github.actor != 'dependabot[bot]')
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: semgrep ci
|
@ -1,9 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: authentik-backend-translate-extract-compile
|
name: authentik-translate-extract-compile
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *" # every day at midnight
|
- cron: "0 0 * * *" # every day at midnight
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- version-*
|
||||||
|
|
||||||
env:
|
env:
|
||||||
POSTGRES_DB: authentik
|
POSTGRES_DB: authentik
|
||||||
@ -15,23 +19,30 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- id: generate_token
|
- id: generate_token
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
|
- name: Generate API
|
||||||
|
run: make gen-client-ts
|
||||||
- name: run extract
|
- name: run extract
|
||||||
run: |
|
run: |
|
||||||
poetry run make i18n-extract
|
uv run make i18n-extract
|
||||||
- name: run compile
|
- name: run compile
|
||||||
run: |
|
run: |
|
||||||
poetry run ak compilemessages
|
uv run ak compilemessages
|
||||||
make web-check-compile
|
make web-check-compile
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
@ -41,3 +52,6 @@ jobs:
|
|||||||
body: "core, web: update translations"
|
body: "core, web: update translations"
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
signoff: true
|
signoff: true
|
||||||
|
labels: dependencies
|
||||||
|
# ID from https://api.github.com/users/authentik-automation[bot]
|
||||||
|
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
|
||||||
|
15
.github/workflows/translation-rename.yml
vendored
15
.github/workflows/translation-rename.yml
vendored
@ -15,6 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
|
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- id: generate_token
|
- id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
@ -25,23 +26,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||||
run: |
|
run: |
|
||||||
title=$(curl -q -L \
|
title=$(gh pr view ${{ github.event.pull_request.number }} --json "title" -q ".title")
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "Authorization: Bearer ${GH_TOKEN}" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }} | jq -r .title)
|
|
||||||
echo "title=${title}" >> "$GITHUB_OUTPUT"
|
echo "title=${title}" >> "$GITHUB_OUTPUT"
|
||||||
- name: Rename
|
- name: Rename
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||||
run: |
|
run: |
|
||||||
curl -L \
|
gh pr edit ${{ github.event.pull_request.number }} -t "translate: ${{ steps.title.outputs.title }}" --add-label dependencies
|
||||||
-X PATCH \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "Authorization: Bearer ${GH_TOKEN}" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }} \
|
|
||||||
-d "{\"title\":\"translate: ${{ steps.title.outputs.title }}\"}"
|
|
||||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -11,6 +11,10 @@ local_settings.py
|
|||||||
db.sqlite3
|
db.sqlite3
|
||||||
media
|
media
|
||||||
|
|
||||||
|
# Node
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||||
# in your Git repository. Update and uncomment the following line accordingly.
|
# in your Git repository. Update and uncomment the following line accordingly.
|
||||||
# <django-project-name>/staticfiles/
|
# <django-project-name>/staticfiles/
|
||||||
@ -33,6 +37,7 @@ eggs/
|
|||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
dist/
|
dist/
|
||||||
|
out/
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
@ -209,3 +214,6 @@ source_docs/
|
|||||||
|
|
||||||
### Golang ###
|
### Golang ###
|
||||||
/vendor/
|
/vendor/
|
||||||
|
|
||||||
|
### Docker ###
|
||||||
|
docker-compose.override.yml
|
||||||
|
47
.prettierignore
Normal file
47
.prettierignore
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Prettier Ignorefile
|
||||||
|
|
||||||
|
## Static Files
|
||||||
|
**/LICENSE
|
||||||
|
|
||||||
|
authentik/stages/**/*
|
||||||
|
|
||||||
|
## Build asset directories
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
out
|
||||||
|
.docusaurus
|
||||||
|
website/docs/developer-docs/api/**/*
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
*.env
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
*.secrets
|
||||||
|
|
||||||
|
## Yarn
|
||||||
|
.yarn/**/*
|
||||||
|
|
||||||
|
## Node
|
||||||
|
node_modules
|
||||||
|
coverage
|
||||||
|
|
||||||
|
## Configs
|
||||||
|
*.log
|
||||||
|
*.yaml
|
||||||
|
*.yml
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
# TODO: Rename affected files to *.template.* or similar.
|
||||||
|
*.html
|
||||||
|
*.mdx
|
||||||
|
*.md
|
||||||
|
|
||||||
|
## Import order matters
|
||||||
|
poly.ts
|
||||||
|
src/locale-codes.ts
|
||||||
|
src/locales/
|
||||||
|
|
||||||
|
# Storybook
|
||||||
|
storybook-static/
|
||||||
|
.storybook/css-import-maps*
|
||||||
|
|
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@ -2,6 +2,7 @@
|
|||||||
"recommendations": [
|
"recommendations": [
|
||||||
"bashmish.es6-string-css",
|
"bashmish.es6-string-css",
|
||||||
"bpruitt-goddard.mermaid-markdown-syntax-highlighting",
|
"bpruitt-goddard.mermaid-markdown-syntax-highlighting",
|
||||||
|
"charliermarsh.ruff",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
@ -10,12 +11,12 @@
|
|||||||
"Gruntfuggly.todo-tree",
|
"Gruntfuggly.todo-tree",
|
||||||
"mechatroner.rainbow-csv",
|
"mechatroner.rainbow-csv",
|
||||||
"ms-python.black-formatter",
|
"ms-python.black-formatter",
|
||||||
"charliermarsh.ruff",
|
"ms-python.black-formatter",
|
||||||
|
"ms-python.debugpy",
|
||||||
"ms-python.python",
|
"ms-python.python",
|
||||||
"ms-python.vscode-pylance",
|
"ms-python.vscode-pylance",
|
||||||
"ms-python.black-formatter",
|
|
||||||
"redhat.vscode-yaml",
|
"redhat.vscode-yaml",
|
||||||
"Tobermory.es6-string-html",
|
"Tobermory.es6-string-html",
|
||||||
"unifiedjs.vscode-mdx"
|
"unifiedjs.vscode-mdx",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
66
.vscode/launch.json
vendored
66
.vscode/launch.json
vendored
@ -2,26 +2,76 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python: PDB attach Server",
|
"name": "Debug: Attach Server Core",
|
||||||
"type": "python",
|
"type": "debugpy",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"connect": {
|
"connect": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 6800
|
"port": 9901
|
||||||
},
|
},
|
||||||
"justMyCode": true,
|
"pathMappings": [
|
||||||
|
{
|
||||||
|
"localRoot": "${workspaceFolder}",
|
||||||
|
"remoteRoot": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
"django": true
|
"django": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Python: PDB attach Worker",
|
"name": "Debug: Attach Worker",
|
||||||
"type": "python",
|
"type": "debugpy",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"connect": {
|
"connect": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 6900
|
"port": 9901
|
||||||
},
|
},
|
||||||
"justMyCode": true,
|
"pathMappings": [
|
||||||
|
{
|
||||||
|
"localRoot": "${workspaceFolder}",
|
||||||
|
"remoteRoot": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
"django": true
|
"django": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug: Start Server Router",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/cmd/server",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug: Start LDAP Outpost",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/cmd/ldap",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug: Start Proxy Outpost",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/cmd/proxy",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug: Start RAC Outpost",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/cmd/rac",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug: Start Radius Outpost",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/cmd/radius",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
31
.vscode/settings.json
vendored
31
.vscode/settings.json
vendored
@ -1,26 +1,4 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
|
||||||
"akadmin",
|
|
||||||
"asgi",
|
|
||||||
"authentik",
|
|
||||||
"authn",
|
|
||||||
"entra",
|
|
||||||
"goauthentik",
|
|
||||||
"jwe",
|
|
||||||
"jwks",
|
|
||||||
"kubernetes",
|
|
||||||
"oidc",
|
|
||||||
"openid",
|
|
||||||
"passwordless",
|
|
||||||
"plex",
|
|
||||||
"saml",
|
|
||||||
"scim",
|
|
||||||
"slo",
|
|
||||||
"sso",
|
|
||||||
"totp",
|
|
||||||
"traefik",
|
|
||||||
"webauthn"
|
|
||||||
],
|
|
||||||
"todo-tree.tree.showCountsInTree": true,
|
"todo-tree.tree.showCountsInTree": true,
|
||||||
"todo-tree.tree.showBadges": true,
|
"todo-tree.tree.showBadges": true,
|
||||||
"yaml.customTags": [
|
"yaml.customTags": [
|
||||||
@ -33,11 +11,12 @@
|
|||||||
"!If sequence",
|
"!If sequence",
|
||||||
"!Index scalar",
|
"!Index scalar",
|
||||||
"!KeyOf scalar",
|
"!KeyOf scalar",
|
||||||
"!Value scalar"
|
"!Value scalar",
|
||||||
|
"!AtIndex scalar"
|
||||||
],
|
],
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"typescript.preferences.importModuleSpecifierEnding": "index",
|
"typescript.preferences.importModuleSpecifierEnding": "index",
|
||||||
"typescript.tsdk": "./web/node_modules/typescript/lib",
|
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
"yaml.schemas": {
|
"yaml.schemas": {
|
||||||
"./blueprints/schema.json": "blueprints/**/*.yaml"
|
"./blueprints/schema.json": "blueprints/**/*.yaml"
|
||||||
@ -51,7 +30,5 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"go.testFlags": ["-count=1"],
|
"go.testFlags": ["-count=1"],
|
||||||
"github-actions.workflows.pinned.workflows": [
|
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"]
|
||||||
".github/workflows/ci-main.yml"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
46
.vscode/tasks.json
vendored
46
.vscode/tasks.json
vendored
@ -3,8 +3,13 @@
|
|||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "authentik/core: make",
|
"label": "authentik/core: make",
|
||||||
"command": "poetry",
|
"command": "uv",
|
||||||
"args": ["run", "make", "lint-fix", "lint"],
|
"args": [
|
||||||
|
"run",
|
||||||
|
"make",
|
||||||
|
"lint-fix",
|
||||||
|
"lint"
|
||||||
|
],
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
@ -12,8 +17,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/core: run",
|
"label": "authentik/core: run",
|
||||||
"command": "poetry",
|
"command": "uv",
|
||||||
"args": ["run", "ak", "server"],
|
"args": [
|
||||||
|
"run",
|
||||||
|
"ak",
|
||||||
|
"server"
|
||||||
|
],
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
@ -23,13 +32,17 @@
|
|||||||
{
|
{
|
||||||
"label": "authentik/web: make",
|
"label": "authentik/web: make",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": ["web"],
|
"args": [
|
||||||
|
"web"
|
||||||
|
],
|
||||||
"group": "build"
|
"group": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/web: watch",
|
"label": "authentik/web: watch",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": ["web-watch"],
|
"args": [
|
||||||
|
"web-watch"
|
||||||
|
],
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
@ -39,19 +52,26 @@
|
|||||||
{
|
{
|
||||||
"label": "authentik: install",
|
"label": "authentik: install",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": ["install", "-j4"],
|
"args": [
|
||||||
|
"install",
|
||||||
|
"-j4"
|
||||||
|
],
|
||||||
"group": "build"
|
"group": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/website: make",
|
"label": "authentik/website: make",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": ["website"],
|
"args": [
|
||||||
|
"website"
|
||||||
|
],
|
||||||
"group": "build"
|
"group": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/website: watch",
|
"label": "authentik/website: watch",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": ["website-watch"],
|
"args": [
|
||||||
|
"website-watch"
|
||||||
|
],
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
@ -60,8 +80,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/api: generate",
|
"label": "authentik/api: generate",
|
||||||
"command": "poetry",
|
"command": "uv",
|
||||||
"args": ["run", "make", "gen"],
|
"args": [
|
||||||
|
"run",
|
||||||
|
"make",
|
||||||
|
"gen"
|
||||||
|
],
|
||||||
"group": "build"
|
"group": "build"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -10,11 +10,12 @@ schemas/ @goauthentik/backend
|
|||||||
scripts/ @goauthentik/backend
|
scripts/ @goauthentik/backend
|
||||||
tests/ @goauthentik/backend
|
tests/ @goauthentik/backend
|
||||||
pyproject.toml @goauthentik/backend
|
pyproject.toml @goauthentik/backend
|
||||||
poetry.lock @goauthentik/backend
|
uv.lock @goauthentik/backend
|
||||||
go.mod @goauthentik/backend
|
go.mod @goauthentik/backend
|
||||||
go.sum @goauthentik/backend
|
go.sum @goauthentik/backend
|
||||||
# Infrastructure
|
# Infrastructure
|
||||||
.github/ @goauthentik/infrastructure
|
.github/ @goauthentik/infrastructure
|
||||||
|
lifecycle/aws/ @goauthentik/infrastructure
|
||||||
Dockerfile @goauthentik/infrastructure
|
Dockerfile @goauthentik/infrastructure
|
||||||
*Dockerfile @goauthentik/infrastructure
|
*Dockerfile @goauthentik/infrastructure
|
||||||
.dockerignore @goauthentik/infrastructure
|
.dockerignore @goauthentik/infrastructure
|
||||||
@ -22,9 +23,14 @@ docker-compose.yml @goauthentik/infrastructure
|
|||||||
Makefile @goauthentik/infrastructure
|
Makefile @goauthentik/infrastructure
|
||||||
.editorconfig @goauthentik/infrastructure
|
.editorconfig @goauthentik/infrastructure
|
||||||
CODEOWNERS @goauthentik/infrastructure
|
CODEOWNERS @goauthentik/infrastructure
|
||||||
|
# Web packages
|
||||||
|
packages/ @goauthentik/frontend
|
||||||
# Web
|
# Web
|
||||||
web/ @goauthentik/frontend
|
web/ @goauthentik/frontend
|
||||||
tests/wdio/ @goauthentik/frontend
|
tests/wdio/ @goauthentik/frontend
|
||||||
|
# Locale
|
||||||
|
locale/ @goauthentik/backend @goauthentik/frontend
|
||||||
|
web/xliff/ @goauthentik/backend @goauthentik/frontend
|
||||||
# Docs & Website
|
# Docs & Website
|
||||||
website/ @goauthentik/docs
|
website/ @goauthentik/docs
|
||||||
CODE_OF_CONDUCT.md @goauthentik/docs
|
CODE_OF_CONDUCT.md @goauthentik/docs
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
We as members, contributors, and leaders pledge to make participation in our
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
identity and expression, level of experience, education, socioeconomic status,
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
and orientation.
|
and orientation.
|
||||||
|
|
||||||
|
91
Dockerfile
91
Dockerfile
@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# Stage 1: Build website
|
# Stage 1: Build website
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
|
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24 AS website-builder
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ COPY ./SECURITY.md /work/
|
|||||||
RUN npm run build-bundled
|
RUN npm run build-bundled
|
||||||
|
|
||||||
# Stage 2: Build webui
|
# Stage 2: Build webui
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS web-builder
|
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24 AS web-builder
|
||||||
|
|
||||||
ARG GIT_BUILD_HASH
|
ARG GIT_BUILD_HASH
|
||||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||||
@ -40,10 +40,11 @@ COPY ./web /work/web/
|
|||||||
COPY ./website /work/website/
|
COPY ./website /work/website/
|
||||||
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build && \
|
||||||
|
npm run build:sfe
|
||||||
|
|
||||||
# Stage 3: Build go proxy
|
# Stage 3: Build go proxy
|
||||||
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder
|
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -76,7 +77,7 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/authentik ./cmd/server
|
go build -o /go/authentik ./cmd/server
|
||||||
|
|
||||||
# Stage 4: MaxMind GeoIP
|
# Stage 4: MaxMind GeoIP
|
||||||
@ -85,46 +86,66 @@ FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip
|
|||||||
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
|
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
|
||||||
ENV GEOIPUPDATE_VERBOSE="1"
|
ENV GEOIPUPDATE_VERBOSE="1"
|
||||||
ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID"
|
ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID"
|
||||||
ENV GEOIPUPDATE_LICENSE_KEY_FILE="/run/secrets/GEOIPUPDATE_LICENSE_KEY"
|
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||||
--mount=type=secret,id=GEOIPUPDATE_LICENSE_KEY \
|
--mount=type=secret,id=GEOIPUPDATE_LICENSE_KEY \
|
||||||
mkdir -p /usr/share/GeoIP && \
|
mkdir -p /usr/share/GeoIP && \
|
||||||
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||||
|
|
||||||
# Stage 5: Python dependencies
|
# Stage 5: Download uv
|
||||||
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips-full AS python-deps
|
FROM ghcr.io/astral-sh/uv:0.7.10 AS uv
|
||||||
|
# Stage 6: Base python image
|
||||||
|
FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base
|
||||||
|
|
||||||
|
ENV VENV_PATH="/ak-root/.venv" \
|
||||||
|
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||||
|
UV_COMPILE_BYTECODE=1 \
|
||||||
|
UV_LINK_MODE=copy \
|
||||||
|
UV_NATIVE_TLS=1 \
|
||||||
|
UV_PYTHON_DOWNLOADS=0
|
||||||
|
|
||||||
|
WORKDIR /ak-root/
|
||||||
|
|
||||||
|
COPY --from=uv /uv /uvx /bin/
|
||||||
|
|
||||||
|
# Stage 7: Python dependencies
|
||||||
|
FROM python-base AS python-deps
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
|
|
||||||
WORKDIR /ak-root/poetry
|
|
||||||
|
|
||||||
ENV VENV_PATH="/ak-root/venv" \
|
|
||||||
POETRY_VIRTUALENVS_CREATE=false \
|
|
||||||
PATH="/ak-root/venv/bin:$PATH"
|
|
||||||
|
|
||||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||||
|
|
||||||
|
ENV PATH="/root/.cargo/bin:$PATH"
|
||||||
|
|
||||||
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
# Required for installing pip packages
|
# Required for installing pip packages
|
||||||
apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev libkrb5-dev
|
apt-get install -y --no-install-recommends \
|
||||||
|
# Build essentials
|
||||||
|
build-essential pkg-config libffi-dev git \
|
||||||
|
# cryptography
|
||||||
|
curl \
|
||||||
|
# libxml
|
||||||
|
libxslt-dev zlib1g-dev \
|
||||||
|
# postgresql
|
||||||
|
libpq-dev \
|
||||||
|
# python-kadmin-rs
|
||||||
|
clang libkrb5-dev sccache \
|
||||||
|
# xmlsec
|
||||||
|
libltdl-dev && \
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
|
||||||
RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
|
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec"
|
||||||
--mount=type=bind,target=./poetry.lock,src=./poetry.lock \
|
|
||||||
--mount=type=cache,target=/root/.cache/pip \
|
|
||||||
--mount=type=cache,target=/root/.cache/pypoetry \
|
|
||||||
python -m venv /ak-root/venv/ && \
|
|
||||||
bash -c "source ${VENV_PATH}/bin/activate && \
|
|
||||||
pip3 install --upgrade pip && \
|
|
||||||
pip3 install poetry && \
|
|
||||||
poetry install --only=main --no-ansi --no-interaction --no-root && \
|
|
||||||
pip install --force-reinstall /wheels/*"
|
|
||||||
|
|
||||||
# Stage 6: Run
|
RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
|
||||||
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips-full AS final-image
|
--mount=type=bind,target=uv.lock,src=uv.lock \
|
||||||
|
--mount=type=cache,target=/root/.cache/uv \
|
||||||
|
uv sync --frozen --no-install-project --no-dev
|
||||||
|
|
||||||
|
# Stage 8: Run
|
||||||
|
FROM python-base AS final-image
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
ARG GIT_BUILD_HASH
|
ARG GIT_BUILD_HASH
|
||||||
@ -140,10 +161,12 @@ WORKDIR /
|
|||||||
|
|
||||||
# We cannot cache this layer otherwise we'll end up with a bigger image
|
# We cannot cache this layer otherwise we'll end up with a bigger image
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
|
apt-get upgrade -y && \
|
||||||
# Required for runtime
|
# Required for runtime
|
||||||
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 && \
|
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 libltdl7 libxslt1.1 && \
|
||||||
# Required for bootstrap & healtcheck
|
# Required for bootstrap & healtcheck
|
||||||
apt-get install -y --no-install-recommends runit && \
|
apt-get install -y --no-install-recommends runit && \
|
||||||
|
pip3 install --no-cache-dir --upgrade pip && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
||||||
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
||||||
@ -154,7 +177,7 @@ RUN apt-get update && \
|
|||||||
|
|
||||||
COPY ./authentik/ /authentik
|
COPY ./authentik/ /authentik
|
||||||
COPY ./pyproject.toml /
|
COPY ./pyproject.toml /
|
||||||
COPY ./poetry.lock /
|
COPY ./uv.lock /
|
||||||
COPY ./schemas /schemas
|
COPY ./schemas /schemas
|
||||||
COPY ./locale /locale
|
COPY ./locale /locale
|
||||||
COPY ./tests /tests
|
COPY ./tests /tests
|
||||||
@ -163,7 +186,7 @@ COPY ./blueprints /blueprints
|
|||||||
COPY ./lifecycle/ /lifecycle
|
COPY ./lifecycle/ /lifecycle
|
||||||
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
|
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
|
||||||
COPY --from=go-builder /go/authentik /bin/authentik
|
COPY --from=go-builder /go/authentik /bin/authentik
|
||||||
COPY --from=python-deps /ak-root/venv /ak-root/venv
|
COPY --from=python-deps /ak-root/.venv /ak-root/.venv
|
||||||
COPY --from=web-builder /work/web/dist/ /web/dist/
|
COPY --from=web-builder /work/web/dist/ /web/dist/
|
||||||
COPY --from=web-builder /work/web/authentik/ /web/authentik/
|
COPY --from=web-builder /work/web/authentik/ /web/authentik/
|
||||||
COPY --from=website-builder /work/website/build/ /website/help/
|
COPY --from=website-builder /work/website/build/ /website/help/
|
||||||
@ -174,11 +197,7 @@ USER 1000
|
|||||||
ENV TMPDIR=/dev/shm/ \
|
ENV TMPDIR=/dev/shm/ \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PATH="/ak-root/venv/bin:/lifecycle:$PATH" \
|
GOFIPS=1
|
||||||
VENV_PATH="/ak-root/venv" \
|
|
||||||
POETRY_VIRTUALENVS_CREATE=false
|
|
||||||
|
|
||||||
ENV GOFIPS=1
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
|
||||||
|
|
||||||
|
139
Makefile
139
Makefile
@ -1,34 +1,21 @@
|
|||||||
.PHONY: gen dev-reset all clean test web website
|
.PHONY: gen dev-reset all clean test web website
|
||||||
|
|
||||||
.SHELLFLAGS += ${SHELLFLAGS} -e
|
SHELL := /usr/bin/env bash
|
||||||
|
.SHELLFLAGS += ${SHELLFLAGS} -e -o pipefail
|
||||||
PWD = $(shell pwd)
|
PWD = $(shell pwd)
|
||||||
UID = $(shell id -u)
|
UID = $(shell id -u)
|
||||||
GID = $(shell id -g)
|
GID = $(shell id -g)
|
||||||
NPM_VERSION = $(shell python -m scripts.npm_version)
|
NPM_VERSION = $(shell python -m scripts.generate_semver)
|
||||||
PY_SOURCES = authentik tests scripts lifecycle .github website/docs/install-config/install/aws
|
PY_SOURCES = authentik tests scripts lifecycle .github
|
||||||
DOCKER_IMAGE ?= "authentik:test"
|
DOCKER_IMAGE ?= "authentik:test"
|
||||||
|
|
||||||
GEN_API_TS = "gen-ts-api"
|
GEN_API_TS = gen-ts-api
|
||||||
GEN_API_PY = "gen-py-api"
|
GEN_API_PY = gen-py-api
|
||||||
GEN_API_GO = "gen-go-api"
|
GEN_API_GO = gen-go-api
|
||||||
|
|
||||||
pg_user := $(shell python -m authentik.lib.config postgresql.user 2>/dev/null)
|
pg_user := $(shell uv run python -m authentik.lib.config postgresql.user 2>/dev/null)
|
||||||
pg_host := $(shell python -m authentik.lib.config postgresql.host 2>/dev/null)
|
pg_host := $(shell uv run python -m authentik.lib.config postgresql.host 2>/dev/null)
|
||||||
pg_name := $(shell python -m authentik.lib.config postgresql.name 2>/dev/null)
|
pg_name := $(shell uv run python -m authentik.lib.config postgresql.name 2>/dev/null)
|
||||||
|
|
||||||
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
|
|
||||||
-I .github/codespell-words.txt \
|
|
||||||
-S 'web/src/locales/**' \
|
|
||||||
-S 'website/docs/developer-docs/api/reference/**' \
|
|
||||||
authentik \
|
|
||||||
internal \
|
|
||||||
cmd \
|
|
||||||
web/src \
|
|
||||||
website/src \
|
|
||||||
website/blog \
|
|
||||||
website/docs \
|
|
||||||
website/integrations \
|
|
||||||
website/src
|
|
||||||
|
|
||||||
all: lint-fix lint test gen web ## Lint, build, and test everything
|
all: lint-fix lint test gen web ## Lint, build, and test everything
|
||||||
|
|
||||||
@ -45,41 +32,38 @@ help: ## Show this help
|
|||||||
go-test:
|
go-test:
|
||||||
go test -timeout 0 -v -race -cover ./...
|
go test -timeout 0 -v -race -cover ./...
|
||||||
|
|
||||||
test-docker: ## Run all tests in a docker-compose
|
|
||||||
echo "PG_PASS=$(shell openssl rand 32 | base64 -w 0)" >> .env
|
|
||||||
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64 -w 0)" >> .env
|
|
||||||
docker compose pull -q
|
|
||||||
docker compose up --no-start
|
|
||||||
docker compose start postgresql redis
|
|
||||||
docker compose run -u root server test-all
|
|
||||||
rm -f .env
|
|
||||||
|
|
||||||
test: ## Run the server tests and produce a coverage report (locally)
|
test: ## Run the server tests and produce a coverage report (locally)
|
||||||
coverage run manage.py test --keepdb authentik
|
uv run coverage run manage.py test --keepdb authentik
|
||||||
coverage html
|
uv run coverage html
|
||||||
coverage report
|
uv run coverage report
|
||||||
|
|
||||||
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||||
black $(PY_SOURCES)
|
uv run black $(PY_SOURCES)
|
||||||
ruff check --fix $(PY_SOURCES)
|
uv run ruff check --fix $(PY_SOURCES)
|
||||||
|
|
||||||
lint-codespell: ## Reports spelling errors.
|
lint-codespell: ## Reports spelling errors.
|
||||||
codespell -w $(CODESPELL_ARGS)
|
uv run codespell -w
|
||||||
|
|
||||||
lint: ## Lint the python and golang sources
|
lint: ## Lint the python and golang sources
|
||||||
bandit -r $(PY_SOURCES) -x web/node_modules -x tests/wdio/node_modules -x website/node_modules
|
uv run bandit -c pyproject.toml -r $(PY_SOURCES)
|
||||||
golangci-lint run -v
|
golangci-lint run -v
|
||||||
|
|
||||||
core-install:
|
core-install:
|
||||||
poetry install
|
uv sync --frozen
|
||||||
|
|
||||||
migrate: ## Run the Authentik Django server's migrations
|
migrate: ## Run the Authentik Django server's migrations
|
||||||
python -m lifecycle.migrate
|
uv run python -m lifecycle.migrate
|
||||||
|
|
||||||
i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that require translation into files to send to a translation service
|
i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that require translation into files to send to a translation service
|
||||||
|
|
||||||
|
aws-cfn:
|
||||||
|
cd lifecycle/aws && npm run aws-cfn
|
||||||
|
|
||||||
|
run: ## Run the main authentik server process
|
||||||
|
uv run ak server
|
||||||
|
|
||||||
core-i18n-extract:
|
core-i18n-extract:
|
||||||
ak makemessages \
|
uv run ak makemessages \
|
||||||
--add-location file \
|
--add-location file \
|
||||||
--no-obsolete \
|
--no-obsolete \
|
||||||
--ignore web \
|
--ignore web \
|
||||||
@ -110,11 +94,11 @@ gen-build: ## Extract the schema from the database
|
|||||||
AUTHENTIK_DEBUG=true \
|
AUTHENTIK_DEBUG=true \
|
||||||
AUTHENTIK_TENANTS__ENABLED=true \
|
AUTHENTIK_TENANTS__ENABLED=true \
|
||||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||||
ak make_blueprint_schema > blueprints/schema.json
|
uv run ak make_blueprint_schema > blueprints/schema.json
|
||||||
AUTHENTIK_DEBUG=true \
|
AUTHENTIK_DEBUG=true \
|
||||||
AUTHENTIK_TENANTS__ENABLED=true \
|
AUTHENTIK_TENANTS__ENABLED=true \
|
||||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||||
ak spectacular --file schema.yml
|
uv run ak spectacular --file schema.yml
|
||||||
|
|
||||||
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
|
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
|
||||||
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
|
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
|
||||||
@ -134,14 +118,19 @@ gen-diff: ## (Release) generate the changelog diff between the current schema a
|
|||||||
npx prettier --write diff.md
|
npx prettier --write diff.md
|
||||||
|
|
||||||
gen-clean-ts: ## Remove generated API client for Typescript
|
gen-clean-ts: ## Remove generated API client for Typescript
|
||||||
rm -rf ./${GEN_API_TS}/
|
rm -rf ${PWD}/${GEN_API_TS}/
|
||||||
rm -rf ./web/node_modules/@goauthentik/api/
|
rm -rf ${PWD}/web/node_modules/@goauthentik/api/
|
||||||
|
|
||||||
gen-clean-go: ## Remove generated API client for Go
|
gen-clean-go: ## Remove generated API client for Go
|
||||||
rm -rf ./${GEN_API_GO}/
|
mkdir -p ${PWD}/${GEN_API_GO}
|
||||||
|
ifneq ($(wildcard ${PWD}/${GEN_API_GO}/.*),)
|
||||||
|
make -C ${PWD}/${GEN_API_GO} clean
|
||||||
|
else
|
||||||
|
rm -rf ${PWD}/${GEN_API_GO}
|
||||||
|
endif
|
||||||
|
|
||||||
gen-clean-py: ## Remove generated API client for Python
|
gen-clean-py: ## Remove generated API client for Python
|
||||||
rm -rf ./${GEN_API_PY}/
|
rm -rf ${PWD}/${GEN_API_PY}/
|
||||||
|
|
||||||
gen-clean: gen-clean-ts gen-clean-go gen-clean-py ## Remove generated API clients
|
gen-clean: gen-clean-ts gen-clean-go gen-clean-py ## Remove generated API clients
|
||||||
|
|
||||||
@ -149,7 +138,7 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
|
|||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \
|
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g typescript-fetch \
|
-g typescript-fetch \
|
||||||
-o /local/${GEN_API_TS} \
|
-o /local/${GEN_API_TS} \
|
||||||
@ -158,14 +147,14 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
|
|||||||
--git-repo-id authentik \
|
--git-repo-id authentik \
|
||||||
--git-user-id goauthentik
|
--git-user-id goauthentik
|
||||||
mkdir -p web/node_modules/@goauthentik/api
|
mkdir -p web/node_modules/@goauthentik/api
|
||||||
cd ./${GEN_API_TS} && npm i
|
cd ${PWD}/${GEN_API_TS} && npm i
|
||||||
\cp -rf ./${GEN_API_TS}/* web/node_modules/@goauthentik/api
|
\cp -rf ${PWD}/${GEN_API_TS}/* web/node_modules/@goauthentik/api
|
||||||
|
|
||||||
gen-client-py: gen-clean-py ## Build and install the authentik API for Python
|
gen-client-py: gen-clean-py ## Build and install the authentik API for Python
|
||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
docker.io/openapitools/openapi-generator-cli:v7.4.0 generate \
|
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g python \
|
-g python \
|
||||||
-o /local/${GEN_API_PY} \
|
-o /local/${GEN_API_PY} \
|
||||||
@ -173,27 +162,20 @@ gen-client-py: gen-clean-py ## Build and install the authentik API for Python
|
|||||||
--additional-properties=packageVersion=${NPM_VERSION} \
|
--additional-properties=packageVersion=${NPM_VERSION} \
|
||||||
--git-repo-id authentik \
|
--git-repo-id authentik \
|
||||||
--git-user-id goauthentik
|
--git-user-id goauthentik
|
||||||
pip install ./${GEN_API_PY}
|
|
||||||
|
|
||||||
gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
|
gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
|
||||||
mkdir -p ./${GEN_API_GO} ./${GEN_API_GO}/templates
|
mkdir -p ${PWD}/${GEN_API_GO}
|
||||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O ./${GEN_API_GO}/config.yaml
|
ifeq ($(wildcard ${PWD}/${GEN_API_GO}/.*),)
|
||||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O ./${GEN_API_GO}/templates/README.mustache
|
git clone --depth 1 https://github.com/goauthentik/client-go.git ${PWD}/${GEN_API_GO}
|
||||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O ./${GEN_API_GO}/templates/go.mod.mustache
|
else
|
||||||
cp schema.yml ./${GEN_API_GO}/
|
cd ${PWD}/${GEN_API_GO} && git pull
|
||||||
docker run \
|
endif
|
||||||
--rm -v ${PWD}/${GEN_API_GO}:/local \
|
cp ${PWD}/schema.yml ${PWD}/${GEN_API_GO}
|
||||||
--user ${UID}:${GID} \
|
make -C ${PWD}/${GEN_API_GO} build
|
||||||
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \
|
|
||||||
-i /local/schema.yml \
|
|
||||||
-g go \
|
|
||||||
-o /local/ \
|
|
||||||
-c /local/config.yaml
|
|
||||||
go mod edit -replace goauthentik.io/api/v3=./${GEN_API_GO}
|
go mod edit -replace goauthentik.io/api/v3=./${GEN_API_GO}
|
||||||
rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/
|
|
||||||
|
|
||||||
gen-dev-config: ## Generate a local development config file
|
gen-dev-config: ## Generate a local development config file
|
||||||
python -m scripts.generate_config
|
uv run scripts/generate_config.py
|
||||||
|
|
||||||
gen: gen-build gen-client-ts
|
gen: gen-build gen-client-ts
|
||||||
|
|
||||||
@ -252,9 +234,6 @@ website-build:
|
|||||||
website-watch: ## Build and watch the documentation website, updating automatically
|
website-watch: ## Build and watch the documentation website, updating automatically
|
||||||
cd website && npm run watch
|
cd website && npm run watch
|
||||||
|
|
||||||
aws-cfn:
|
|
||||||
cd website && npm run aws-cfn
|
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
## Docker
|
## Docker
|
||||||
#########################
|
#########################
|
||||||
@ -263,6 +242,9 @@ docker: ## Build a docker image of the current source tree
|
|||||||
mkdir -p ${GEN_API_TS}
|
mkdir -p ${GEN_API_TS}
|
||||||
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
|
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
|
||||||
|
|
||||||
|
test-docker:
|
||||||
|
BUILD=true ${PWD}/scripts/test_docker.sh
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
## CI
|
## CI
|
||||||
#########################
|
#########################
|
||||||
@ -274,16 +256,21 @@ ci--meta-debug:
|
|||||||
node --version
|
node --version
|
||||||
|
|
||||||
ci-black: ci--meta-debug
|
ci-black: ci--meta-debug
|
||||||
black --check $(PY_SOURCES)
|
uv run black --check $(PY_SOURCES)
|
||||||
|
|
||||||
ci-ruff: ci--meta-debug
|
ci-ruff: ci--meta-debug
|
||||||
ruff check $(PY_SOURCES)
|
uv run ruff check $(PY_SOURCES)
|
||||||
|
|
||||||
ci-codespell: ci--meta-debug
|
ci-codespell: ci--meta-debug
|
||||||
codespell $(CODESPELL_ARGS) -s
|
uv run codespell -s
|
||||||
|
|
||||||
ci-bandit: ci--meta-debug
|
ci-bandit: ci--meta-debug
|
||||||
bandit -r $(PY_SOURCES)
|
uv run bandit -r $(PY_SOURCES)
|
||||||
|
|
||||||
ci-pending-migrations: ci--meta-debug
|
ci-pending-migrations: ci--meta-debug
|
||||||
ak makemigrations --check
|
uv run ak makemigrations --check
|
||||||
|
|
||||||
|
ci-test: ci--meta-debug
|
||||||
|
uv run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
||||||
|
uv run coverage report
|
||||||
|
uv run coverage xml
|
||||||
|
@ -42,4 +42,4 @@ See [SECURITY.md](SECURITY.md)
|
|||||||
|
|
||||||
## Adoption and Contributions
|
## Adoption and Contributions
|
||||||
|
|
||||||
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [CONTRIBUTING.md file](./CONTRIBUTING.md).
|
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [contribution guide](https://docs.goauthentik.io/docs/developer-docs?utm_source=github).
|
||||||
|
@ -2,7 +2,7 @@ authentik takes security very seriously. We follow the rules of [responsible di
|
|||||||
|
|
||||||
## Independent audits and pentests
|
## Independent audits and pentests
|
||||||
|
|
||||||
We are committed to engaging in regular pentesting and security audits of authentik. Defining and adhering to a cadence of external testing ensures a stronger probability that our code base, our features, and our architecture is as secure and non-exploitable as possible. For more details about specfic audits and pentests, refer to "Audits and Certificates" in our [Security documentation](https://docs.goauthentik.io/docs/security).
|
We are committed to engaging in regular pentesting and security audits of authentik. Defining and adhering to a cadence of external testing ensures a stronger probability that our code base, our features, and our architecture is as secure and non-exploitable as possible. For more details about specific audits and pentests, refer to "Audits and Certificates" in our [Security documentation](https://docs.goauthentik.io/docs/security).
|
||||||
|
|
||||||
## What authentik classifies as a CVE
|
## What authentik classifies as a CVE
|
||||||
|
|
||||||
@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| --------- | --------- |
|
| --------- | --------- |
|
||||||
| 2024.8.x | ✅ |
|
| 2025.4.x | ✅ |
|
||||||
| 2024.10.x | ✅ |
|
| 2025.6.x | ✅ |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2024.10.5"
|
__version__ = "2025.6.0"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
@ -16,5 +16,5 @@ def get_full_version() -> str:
|
|||||||
"""Get full version, with build hash appended"""
|
"""Get full version, with build hash appended"""
|
||||||
version = __version__
|
version = __version__
|
||||||
if (build_hash := get_build_hash()) != "":
|
if (build_hash := get_build_hash()) != "":
|
||||||
version += "." + build_hash
|
return f"{version}+{build_hash}"
|
||||||
return version
|
return version
|
||||||
|
@ -7,7 +7,9 @@ from sys import version as python_version
|
|||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
from cryptography.hazmat.backends.openssl.backend import backend
|
from cryptography.hazmat.backends.openssl.backend import backend
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from django.views.debug import SafeExceptionReporterFilter
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
@ -52,10 +54,16 @@ class SystemInfoSerializer(PassiveSerializer):
|
|||||||
def get_http_headers(self, request: Request) -> dict[str, str]:
|
def get_http_headers(self, request: Request) -> dict[str, str]:
|
||||||
"""Get HTTP Request headers"""
|
"""Get HTTP Request headers"""
|
||||||
headers = {}
|
headers = {}
|
||||||
|
raw_session = request._request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
||||||
for key, value in request.META.items():
|
for key, value in request.META.items():
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
continue
|
continue
|
||||||
headers[key] = value
|
actual_value = value
|
||||||
|
if raw_session is not None and raw_session in actual_value:
|
||||||
|
actual_value = actual_value.replace(
|
||||||
|
raw_session, SafeExceptionReporterFilter.cleansed_substitute
|
||||||
|
)
|
||||||
|
headers[key] = actual_value
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
def get_http_host(self, request: Request) -> str:
|
def get_http_host(self, request: Request) -> str:
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
"""authentik administration overview"""
|
"""authentik administration overview"""
|
||||||
|
|
||||||
|
from socket import gethostname
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||||
from rest_framework.fields import IntegerField
|
from packaging.version import parse
|
||||||
|
from rest_framework.fields import BooleanField, CharField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from authentik import get_full_version
|
||||||
from authentik.rbac.permissions import HasPermission
|
from authentik.rbac.permissions import HasPermission
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
@ -16,11 +20,38 @@ class WorkerView(APIView):
|
|||||||
|
|
||||||
permission_classes = [HasPermission("authentik_rbac.view_system_info")]
|
permission_classes = [HasPermission("authentik_rbac.view_system_info")]
|
||||||
|
|
||||||
@extend_schema(responses=inline_serializer("Workers", fields={"count": IntegerField()}))
|
@extend_schema(
|
||||||
|
responses=inline_serializer(
|
||||||
|
"Worker",
|
||||||
|
fields={
|
||||||
|
"worker_id": CharField(),
|
||||||
|
"version": CharField(),
|
||||||
|
"version_matching": BooleanField(),
|
||||||
|
},
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
def get(self, request: Request) -> Response:
|
def get(self, request: Request) -> Response:
|
||||||
"""Get currently connected worker count."""
|
"""Get currently connected worker count."""
|
||||||
count = len(CELERY_APP.control.ping(timeout=0.5))
|
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5)
|
||||||
|
our_version = parse(get_full_version())
|
||||||
|
response = []
|
||||||
|
for worker in raw:
|
||||||
|
key = list(worker.keys())[0]
|
||||||
|
version = worker[key].get("version")
|
||||||
|
version_matching = False
|
||||||
|
if version:
|
||||||
|
version_matching = parse(version) == our_version
|
||||||
|
response.append(
|
||||||
|
{"worker_id": key, "version": version, "version_matching": version_matching}
|
||||||
|
)
|
||||||
# In debug we run with `task_always_eager`, so tasks are ran on the main process
|
# In debug we run with `task_always_eager`, so tasks are ran on the main process
|
||||||
if settings.DEBUG: # pragma: no cover
|
if settings.DEBUG: # pragma: no cover
|
||||||
count += 1
|
response.append(
|
||||||
return Response({"count": count})
|
{
|
||||||
|
"worker_id": f"authentik-debug@{gethostname()}",
|
||||||
|
"version": get_full_version(),
|
||||||
|
"version_matching": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return Response(response)
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
"""authentik admin app config"""
|
"""authentik admin app config"""
|
||||||
|
|
||||||
from prometheus_client import Gauge, Info
|
from prometheus_client import Info
|
||||||
|
|
||||||
from authentik.blueprints.apps import ManagedAppConfig
|
from authentik.blueprints.apps import ManagedAppConfig
|
||||||
|
|
||||||
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
||||||
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")
|
|
||||||
|
|
||||||
|
|
||||||
class AuthentikAdminConfig(ManagedAppConfig):
|
class AuthentikAdminConfig(ManagedAppConfig):
|
||||||
|
@ -1,14 +1,35 @@
|
|||||||
"""admin signals"""
|
"""admin signals"""
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from packaging.version import parse
|
||||||
|
from prometheus_client import Gauge
|
||||||
|
|
||||||
from authentik.admin.apps import GAUGE_WORKERS
|
from authentik import get_full_version
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
from authentik.root.monitoring import monitoring_set
|
from authentik.root.monitoring import monitoring_set
|
||||||
|
|
||||||
|
GAUGE_WORKERS = Gauge(
|
||||||
|
"authentik_admin_workers",
|
||||||
|
"Currently connected workers, their versions and if they are the same version as authentik",
|
||||||
|
["version", "version_matched"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_version = parse(get_full_version())
|
||||||
|
|
||||||
|
|
||||||
@receiver(monitoring_set)
|
@receiver(monitoring_set)
|
||||||
def monitoring_set_workers(sender, **kwargs):
|
def monitoring_set_workers(sender, **kwargs):
|
||||||
"""Set worker gauge"""
|
"""Set worker gauge"""
|
||||||
count = len(CELERY_APP.control.ping(timeout=0.5))
|
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5)
|
||||||
GAUGE_WORKERS.set(count)
|
worker_version_count = {}
|
||||||
|
for worker in raw:
|
||||||
|
key = list(worker.keys())[0]
|
||||||
|
version = worker[key].get("version")
|
||||||
|
version_matching = False
|
||||||
|
if version:
|
||||||
|
version_matching = parse(version) == _version
|
||||||
|
worker_version_count.setdefault(version, {"count": 0, "matching": version_matching})
|
||||||
|
worker_version_count[version]["count"] += 1
|
||||||
|
for version, stats in worker_version_count.items():
|
||||||
|
GAUGE_WORKERS.labels(version, stats["matching"]).set(stats["count"])
|
||||||
|
@ -34,7 +34,7 @@ class TestAdminAPI(TestCase):
|
|||||||
response = self.client.get(reverse("authentik_api:admin_workers"))
|
response = self.client.get(reverse("authentik_api:admin_workers"))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
body = loads(response.content)
|
body = loads(response.content)
|
||||||
self.assertEqual(body["count"], 0)
|
self.assertEqual(len(body), 0)
|
||||||
|
|
||||||
def test_metrics(self):
|
def test_metrics(self):
|
||||||
"""Test metrics API"""
|
"""Test metrics API"""
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
"""API Authentication"""
|
"""API Authentication"""
|
||||||
|
|
||||||
from hmac import compare_digest
|
from hmac import compare_digest
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import gettempdir
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from drf_spectacular.extensions import OpenApiAuthenticationExtension
|
from drf_spectacular.extensions import OpenApiAuthenticationExtension
|
||||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
@ -11,11 +14,17 @@ from rest_framework.request import Request
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.middleware import CTX_AUTH_VIA
|
from authentik.core.middleware import CTX_AUTH_VIA
|
||||||
from authentik.core.models import Token, TokenIntents, User
|
from authentik.core.models import Token, TokenIntents, User, UserTypes
|
||||||
from authentik.outposts.models import Outpost
|
from authentik.outposts.models import Outpost
|
||||||
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
|
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
_tmp = Path(gettempdir())
|
||||||
|
try:
|
||||||
|
with open(_tmp / "authentik-core-ipc.key") as _f:
|
||||||
|
ipc_key = _f.read()
|
||||||
|
except OSError:
|
||||||
|
ipc_key = None
|
||||||
|
|
||||||
|
|
||||||
def validate_auth(header: bytes) -> str | None:
|
def validate_auth(header: bytes) -> str | None:
|
||||||
@ -73,6 +82,11 @@ def auth_user_lookup(raw_header: bytes) -> User | None:
|
|||||||
if user:
|
if user:
|
||||||
CTX_AUTH_VIA.set("secret_key")
|
CTX_AUTH_VIA.set("secret_key")
|
||||||
return user
|
return user
|
||||||
|
# then try to auth via secret key (for embedded outpost/etc)
|
||||||
|
user = token_ipc(auth_credentials)
|
||||||
|
if user:
|
||||||
|
CTX_AUTH_VIA.set("ipc")
|
||||||
|
return user
|
||||||
raise AuthenticationFailed("Token invalid/expired")
|
raise AuthenticationFailed("Token invalid/expired")
|
||||||
|
|
||||||
|
|
||||||
@ -90,6 +104,43 @@ def token_secret_key(value: str) -> User | None:
|
|||||||
return outpost.user
|
return outpost.user
|
||||||
|
|
||||||
|
|
||||||
|
class IPCUser(AnonymousUser):
|
||||||
|
"""'Virtual' user for IPC communication between authentik core and the authentik router"""
|
||||||
|
|
||||||
|
username = "authentik:system"
|
||||||
|
is_active = True
|
||||||
|
is_superuser = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
return UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||||
|
|
||||||
|
def has_perm(self, perm, obj=None):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_perms(self, perm_list, obj=None):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_module_perms(self, module):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_anonymous(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def token_ipc(value: str) -> User | None:
|
||||||
|
"""Check if the token is the secret key
|
||||||
|
and return the service account for the managed outpost"""
|
||||||
|
if not ipc_key or not compare_digest(value, ipc_key):
|
||||||
|
return None
|
||||||
|
return IPCUser()
|
||||||
|
|
||||||
|
|
||||||
class TokenAuthentication(BaseAuthentication):
|
class TokenAuthentication(BaseAuthentication):
|
||||||
"""Token-based authentication using HTTP Bearer authentication"""
|
"""Token-based authentication using HTTP Bearer authentication"""
|
||||||
|
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
"""API Authorization"""
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db.models import Model
|
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
|
||||||
from rest_framework.authentication import get_authorization_header
|
|
||||||
from rest_framework.filters import BaseFilterBackend
|
|
||||||
from rest_framework.permissions import BasePermission
|
|
||||||
from rest_framework.request import Request
|
|
||||||
|
|
||||||
from authentik.api.authentication import validate_auth
|
|
||||||
from authentik.rbac.filters import ObjectFilter
|
|
||||||
|
|
||||||
|
|
||||||
class OwnerFilter(BaseFilterBackend):
|
|
||||||
"""Filter objects by their owner"""
|
|
||||||
|
|
||||||
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})
|
|
||||||
|
|
||||||
|
|
||||||
class SecretKeyFilter(DjangoFilterBackend):
|
|
||||||
"""Allow access to all objects when authenticated with secret key as token.
|
|
||||||
|
|
||||||
Replaces both DjangoFilterBackend and ObjectFilter"""
|
|
||||||
|
|
||||||
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
|
|
||||||
auth_header = get_authorization_header(request)
|
|
||||||
token = validate_auth(auth_header)
|
|
||||||
if token and token == settings.SECRET_KEY:
|
|
||||||
return queryset
|
|
||||||
queryset = ObjectFilter().filter_queryset(request, queryset, view)
|
|
||||||
return super().filter_queryset(request, queryset, view)
|
|
||||||
|
|
||||||
|
|
||||||
class OwnerPermissions(BasePermission):
|
|
||||||
"""Authorize requests by an object's owner matching the requesting user"""
|
|
||||||
|
|
||||||
owner_key = "user"
|
|
||||||
|
|
||||||
def has_permission(self, request: Request, view) -> bool:
|
|
||||||
"""If the user is authenticated, we allow all requests here. For listing, the
|
|
||||||
object-level permissions are done by the filter backend"""
|
|
||||||
return request.user.is_authenticated
|
|
||||||
|
|
||||||
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
|
|
||||||
"""Check if the object's owner matches the currently logged in user"""
|
|
||||||
if not hasattr(obj, self.owner_key):
|
|
||||||
return False
|
|
||||||
owner = getattr(obj, self.owner_key)
|
|
||||||
if owner != request.user:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class OwnerSuperuserPermissions(OwnerPermissions):
|
|
||||||
"""Similar to OwnerPermissions, except always allow access for superusers"""
|
|
||||||
|
|
||||||
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
|
|
||||||
if request.user.is_superuser:
|
|
||||||
return True
|
|
||||||
return super().has_object_permission(request, view, obj)
|
|
@ -54,7 +54,7 @@ def create_component(generator: SchemaGenerator, name, schema, type_=ResolvedCom
|
|||||||
return component
|
return component
|
||||||
|
|
||||||
|
|
||||||
def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs): # noqa: W0613
|
def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs):
|
||||||
"""Workaround to set a default response for endpoints.
|
"""Workaround to set a default response for endpoints.
|
||||||
Workaround suggested at
|
Workaround suggested at
|
||||||
<https://github.com/tfranzel/drf-spectacular/issues/119#issuecomment-656970357>
|
<https://github.com/tfranzel/drf-spectacular/issues/119#issuecomment-656970357>
|
||||||
|
@ -7,7 +7,7 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_framework.fields import CharField, DateTimeField
|
from rest_framework.fields import CharField, DateTimeField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ListSerializer, ModelSerializer
|
from rest_framework.serializers import ListSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.blueprints.models import BlueprintInstance
|
from authentik.blueprints.models import BlueprintInstance
|
||||||
@ -15,7 +15,7 @@ from authentik.blueprints.v1.importer import Importer
|
|||||||
from authentik.blueprints.v1.oci import OCI_PREFIX
|
from authentik.blueprints.v1.oci import OCI_PREFIX
|
||||||
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
|
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import JSONDictField, PassiveSerializer
|
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
|
|
||||||
|
|
||||||
|
68
authentik/blueprints/management/commands/blueprint_shell.py
Normal file
68
authentik/blueprints/management/commands/blueprint_shell.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"""Test and debug Blueprints"""
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import readline
|
||||||
|
from pathlib import Path
|
||||||
|
from pprint import pformat
|
||||||
|
from sys import exit as sysexit
|
||||||
|
from textwrap import indent
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, no_translations
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
from yaml import load
|
||||||
|
|
||||||
|
from authentik.blueprints.v1.common import BlueprintLoader, EntryInvalidError
|
||||||
|
from authentik.core.management.commands.shell import get_banner_text
|
||||||
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Test and debug Blueprints"""
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
histfolder = Path("~").expanduser() / Path(".local/share/authentik")
|
||||||
|
histfolder.mkdir(parents=True, exist_ok=True)
|
||||||
|
histfile = histfolder / Path("blueprint_shell_history")
|
||||||
|
readline.parse_and_bind("tab: complete")
|
||||||
|
readline.parse_and_bind("set editing-mode vi")
|
||||||
|
|
||||||
|
try:
|
||||||
|
readline.read_history_file(str(histfile))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
atexit.register(readline.write_history_file, str(histfile))
|
||||||
|
|
||||||
|
@no_translations
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""Interactively debug blueprint files"""
|
||||||
|
self.stdout.write(get_banner_text("Blueprint shell"))
|
||||||
|
self.stdout.write("Type '.eval' to evaluate previously entered statement(s).")
|
||||||
|
|
||||||
|
def do_eval():
|
||||||
|
yaml_input = "\n".join([line for line in self.lines if line])
|
||||||
|
data = load(yaml_input, BlueprintLoader)
|
||||||
|
self.stdout.write(pformat(data))
|
||||||
|
self.lines = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = input("> ")
|
||||||
|
if line == ".eval":
|
||||||
|
do_eval()
|
||||||
|
else:
|
||||||
|
self.lines.append(line)
|
||||||
|
except EntryInvalidError as exc:
|
||||||
|
self.stdout.write("Failed to evaluate expression:")
|
||||||
|
self.stdout.write(indent(exception_to_string(exc), prefix=" "))
|
||||||
|
except EOFError:
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.stdout.write()
|
||||||
|
sysexit(0)
|
||||||
|
self.stdout.write()
|
@ -126,7 +126,7 @@ class Command(BaseCommand):
|
|||||||
def_name_perm = f"model_{model_path}_permissions"
|
def_name_perm = f"model_{model_path}_permissions"
|
||||||
def_path_perm = f"#/$defs/{def_name_perm}"
|
def_path_perm = f"#/$defs/{def_name_perm}"
|
||||||
self.schema["$defs"][def_name_perm] = self.model_permissions(model)
|
self.schema["$defs"][def_name_perm] = self.model_permissions(model)
|
||||||
return {
|
template = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["model", "identifiers"],
|
"required": ["model", "identifiers"],
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -143,6 +143,11 @@ class Command(BaseCommand):
|
|||||||
"identifiers": {"$ref": def_path},
|
"identifiers": {"$ref": def_path},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
# Meta models don't require identifiers, as there's no matching database model to find
|
||||||
|
if issubclass(model, BaseMetaModel):
|
||||||
|
del template["properties"]["identifiers"]
|
||||||
|
template["required"].remove("identifiers")
|
||||||
|
return template
|
||||||
|
|
||||||
def field_to_jsonschema(self, field: Field) -> dict:
|
def field_to_jsonschema(self, field: Field) -> dict:
|
||||||
"""Convert a single field to json schema"""
|
"""Convert a single field to json schema"""
|
||||||
|
@ -146,6 +146,10 @@ entries:
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
nested_context: !Context context2
|
nested_context: !Context context2
|
||||||
|
at_index_sequence: !AtIndex [!Context sequence, 0]
|
||||||
|
at_index_sequence_default: !AtIndex [!Context sequence, 100, "non existent"]
|
||||||
|
at_index_mapping: !AtIndex [!Context mapping, "key2"]
|
||||||
|
at_index_mapping_default: !AtIndex [!Context mapping, "invalid", "non existent"]
|
||||||
identifiers:
|
identifiers:
|
||||||
name: test
|
name: test
|
||||||
conditions:
|
conditions:
|
||||||
|
@ -215,6 +215,10 @@ class TestBlueprintsV1(TransactionTestCase):
|
|||||||
},
|
},
|
||||||
"nested_context": "context-nested-value",
|
"nested_context": "context-nested-value",
|
||||||
"env_null": None,
|
"env_null": None,
|
||||||
|
"at_index_sequence": "foo",
|
||||||
|
"at_index_sequence_default": "non existent",
|
||||||
|
"at_index_mapping": 2,
|
||||||
|
"at_index_mapping_default": "non existent",
|
||||||
}
|
}
|
||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
|
@ -24,6 +24,10 @@ from authentik.lib.sentry import SentryIgnoredException
|
|||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
|
|
||||||
|
|
||||||
|
class UNSET:
|
||||||
|
"""Used to test whether a key has not been set."""
|
||||||
|
|
||||||
|
|
||||||
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
||||||
"""Get object's attributes via their serializer, and convert it to a normal dict"""
|
"""Get object's attributes via their serializer, and convert it to a normal dict"""
|
||||||
serializer: Serializer = obj.serializer(obj)
|
serializer: Serializer = obj.serializer(obj)
|
||||||
@ -160,9 +164,7 @@ class BlueprintEntry:
|
|||||||
"""Get the blueprint model, with yaml tags resolved if present"""
|
"""Get the blueprint model, with yaml tags resolved if present"""
|
||||||
return str(self.tag_resolver(self.model, blueprint))
|
return str(self.tag_resolver(self.model, blueprint))
|
||||||
|
|
||||||
def get_permissions(
|
def get_permissions(self, blueprint: "Blueprint") -> Generator[BlueprintEntryPermission]:
|
||||||
self, blueprint: "Blueprint"
|
|
||||||
) -> Generator[BlueprintEntryPermission, None, None]:
|
|
||||||
"""Get permissions of this entry, with all yaml tags resolved"""
|
"""Get permissions of this entry, with all yaml tags resolved"""
|
||||||
for perm in self.permissions:
|
for perm in self.permissions:
|
||||||
yield BlueprintEntryPermission(
|
yield BlueprintEntryPermission(
|
||||||
@ -198,6 +200,9 @@ class Blueprint:
|
|||||||
class YAMLTag:
|
class YAMLTag:
|
||||||
"""Base class for all YAML Tags"""
|
"""Base class for all YAML Tags"""
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return str(self.resolve(BlueprintEntry(""), Blueprint()))
|
||||||
|
|
||||||
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
||||||
"""Implement yaml tag logic"""
|
"""Implement yaml tag logic"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -556,6 +561,53 @@ class Value(EnumeratedItem):
|
|||||||
raise EntryInvalidError.from_entry(f"Empty/invalid context: {context}", entry) from exc
|
raise EntryInvalidError.from_entry(f"Empty/invalid context: {context}", entry) from exc
|
||||||
|
|
||||||
|
|
||||||
|
class AtIndex(YAMLTag):
|
||||||
|
"""Get value at index of a sequence or mapping"""
|
||||||
|
|
||||||
|
obj: YAMLTag | dict | list | tuple
|
||||||
|
attribute: int | str | YAMLTag
|
||||||
|
default: Any | UNSET
|
||||||
|
|
||||||
|
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.obj = loader.construct_object(node.value[0])
|
||||||
|
self.attribute = loader.construct_object(node.value[1])
|
||||||
|
if len(node.value) == 2: # noqa: PLR2004
|
||||||
|
self.default = UNSET
|
||||||
|
else:
|
||||||
|
self.default = loader.construct_object(node.value[2])
|
||||||
|
|
||||||
|
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
||||||
|
if isinstance(self.obj, YAMLTag):
|
||||||
|
obj = self.obj.resolve(entry, blueprint)
|
||||||
|
else:
|
||||||
|
obj = self.obj
|
||||||
|
if isinstance(self.attribute, YAMLTag):
|
||||||
|
attribute = self.attribute.resolve(entry, blueprint)
|
||||||
|
else:
|
||||||
|
attribute = self.attribute
|
||||||
|
|
||||||
|
if isinstance(obj, list | tuple):
|
||||||
|
try:
|
||||||
|
return obj[attribute]
|
||||||
|
except TypeError as exc:
|
||||||
|
raise EntryInvalidError.from_entry(
|
||||||
|
f"Invalid index for list: {attribute}", entry
|
||||||
|
) from exc
|
||||||
|
except IndexError as exc:
|
||||||
|
if self.default is UNSET:
|
||||||
|
raise EntryInvalidError.from_entry(
|
||||||
|
f"Index out of range: {attribute}", entry
|
||||||
|
) from exc
|
||||||
|
return self.default
|
||||||
|
if attribute in obj:
|
||||||
|
return obj[attribute]
|
||||||
|
else:
|
||||||
|
if self.default is UNSET:
|
||||||
|
raise EntryInvalidError.from_entry(f"Key does not exist: {attribute}", entry)
|
||||||
|
return self.default
|
||||||
|
|
||||||
|
|
||||||
class BlueprintDumper(SafeDumper):
|
class BlueprintDumper(SafeDumper):
|
||||||
"""Dump dataclasses to yaml"""
|
"""Dump dataclasses to yaml"""
|
||||||
|
|
||||||
@ -606,6 +658,7 @@ class BlueprintLoader(SafeLoader):
|
|||||||
self.add_constructor("!Enumerate", Enumerate)
|
self.add_constructor("!Enumerate", Enumerate)
|
||||||
self.add_constructor("!Value", Value)
|
self.add_constructor("!Value", Value)
|
||||||
self.add_constructor("!Index", Index)
|
self.add_constructor("!Index", Index)
|
||||||
|
self.add_constructor("!AtIndex", AtIndex)
|
||||||
|
|
||||||
|
|
||||||
class EntryInvalidError(SentryIgnoredException):
|
class EntryInvalidError(SentryIgnoredException):
|
||||||
|
@ -36,6 +36,7 @@ from authentik.core.models import (
|
|||||||
GroupSourceConnection,
|
GroupSourceConnection,
|
||||||
PropertyMapping,
|
PropertyMapping,
|
||||||
Provider,
|
Provider,
|
||||||
|
Session,
|
||||||
Source,
|
Source,
|
||||||
User,
|
User,
|
||||||
UserSourceConnection,
|
UserSourceConnection,
|
||||||
@ -50,7 +51,7 @@ from authentik.enterprise.providers.microsoft_entra.models import (
|
|||||||
MicrosoftEntraProviderGroup,
|
MicrosoftEntraProviderGroup,
|
||||||
MicrosoftEntraProviderUser,
|
MicrosoftEntraProviderUser,
|
||||||
)
|
)
|
||||||
from authentik.enterprise.providers.rac.models import ConnectionToken
|
from authentik.enterprise.providers.ssf.models import StreamEvent
|
||||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
||||||
EndpointDevice,
|
EndpointDevice,
|
||||||
EndpointDeviceConnection,
|
EndpointDeviceConnection,
|
||||||
@ -71,6 +72,7 @@ from authentik.providers.oauth2.models import (
|
|||||||
DeviceToken,
|
DeviceToken,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
)
|
)
|
||||||
|
from authentik.providers.rac.models import ConnectionToken
|
||||||
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
|
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
|
||||||
from authentik.rbac.models import Role
|
from authentik.rbac.models import Role
|
||||||
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
|
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
|
||||||
@ -107,6 +109,7 @@ def excluded_models() -> list[type[Model]]:
|
|||||||
Policy,
|
Policy,
|
||||||
PolicyBindingModel,
|
PolicyBindingModel,
|
||||||
# Classes that have other dependencies
|
# Classes that have other dependencies
|
||||||
|
Session,
|
||||||
AuthenticatedSession,
|
AuthenticatedSession,
|
||||||
# Classes which are only internally managed
|
# Classes which are only internally managed
|
||||||
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
|
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
|
||||||
@ -131,6 +134,7 @@ def excluded_models() -> list[type[Model]]:
|
|||||||
EndpointDevice,
|
EndpointDevice,
|
||||||
EndpointDeviceConnection,
|
EndpointDeviceConnection,
|
||||||
DeviceToken,
|
DeviceToken,
|
||||||
|
StreamEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,10 +14,10 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.validators import UniqueValidator
|
from rest_framework.validators import UniqueValidator
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.api.authorization import SecretKeyFilter
|
|
||||||
from authentik.brands.models import Brand
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||||
|
from authentik.rbac.filters import SecretKeyFilter
|
||||||
from authentik.tenants.utils import get_current_tenant
|
from authentik.tenants.utils import get_current_tenant
|
||||||
|
|
||||||
|
|
||||||
@ -49,6 +49,8 @@ class BrandSerializer(ModelSerializer):
|
|||||||
"branding_title",
|
"branding_title",
|
||||||
"branding_logo",
|
"branding_logo",
|
||||||
"branding_favicon",
|
"branding_favicon",
|
||||||
|
"branding_custom_css",
|
||||||
|
"branding_default_flow_background",
|
||||||
"flow_authentication",
|
"flow_authentication",
|
||||||
"flow_invalidation",
|
"flow_invalidation",
|
||||||
"flow_recovery",
|
"flow_recovery",
|
||||||
@ -57,6 +59,7 @@ class BrandSerializer(ModelSerializer):
|
|||||||
"flow_device_code",
|
"flow_device_code",
|
||||||
"default_application",
|
"default_application",
|
||||||
"web_certificate",
|
"web_certificate",
|
||||||
|
"client_certificates",
|
||||||
"attributes",
|
"attributes",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
@ -86,6 +89,7 @@ class CurrentBrandSerializer(PassiveSerializer):
|
|||||||
branding_title = CharField()
|
branding_title = CharField()
|
||||||
branding_logo = CharField(source="branding_logo_url")
|
branding_logo = CharField(source="branding_logo_url")
|
||||||
branding_favicon = CharField(source="branding_favicon_url")
|
branding_favicon = CharField(source="branding_favicon_url")
|
||||||
|
branding_custom_css = CharField()
|
||||||
ui_footer_links = ListField(
|
ui_footer_links = ListField(
|
||||||
child=FooterLinkSerializer(),
|
child=FooterLinkSerializer(),
|
||||||
read_only=True,
|
read_only=True,
|
||||||
@ -117,6 +121,7 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"domain",
|
"domain",
|
||||||
"branding_title",
|
"branding_title",
|
||||||
"web_certificate__name",
|
"web_certificate__name",
|
||||||
|
"client_certificates__name",
|
||||||
]
|
]
|
||||||
filterset_fields = [
|
filterset_fields = [
|
||||||
"brand_uuid",
|
"brand_uuid",
|
||||||
@ -125,6 +130,7 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"branding_title",
|
"branding_title",
|
||||||
"branding_logo",
|
"branding_logo",
|
||||||
"branding_favicon",
|
"branding_favicon",
|
||||||
|
"branding_default_flow_background",
|
||||||
"flow_authentication",
|
"flow_authentication",
|
||||||
"flow_invalidation",
|
"flow_invalidation",
|
||||||
"flow_recovery",
|
"flow_recovery",
|
||||||
@ -132,6 +138,7 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"flow_user_settings",
|
"flow_user_settings",
|
||||||
"flow_device_code",
|
"flow_device_code",
|
||||||
"web_certificate",
|
"web_certificate",
|
||||||
|
"client_certificates",
|
||||||
]
|
]
|
||||||
ordering = ["domain"]
|
ordering = ["domain"]
|
||||||
|
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 5.0.12 on 2025-02-22 01:51
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_custom_css(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
Brand = apps.get_model("authentik_brands", "brand")
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
path = Path("/web/dist/custom.css")
|
||||||
|
if not path.exists():
|
||||||
|
return
|
||||||
|
css = path.read_text()
|
||||||
|
Brand.objects.using(db_alias).all().update(branding_custom_css=css)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_brands", "0007_brand_default_application"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="brand",
|
||||||
|
name="branding_custom_css",
|
||||||
|
field=models.TextField(blank=True, default=""),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_custom_css),
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.13 on 2025-03-19 22:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_brands", "0008_brand_branding_custom_css"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="brand",
|
||||||
|
name="branding_default_flow_background",
|
||||||
|
field=models.TextField(default="/static/dist/assets/images/flow_background.jpg"),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 5.1.9 on 2025-05-19 15:09
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_brands", "0009_brand_branding_default_flow_background"),
|
||||||
|
("authentik_crypto", "0004_alter_certificatekeypair_name"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="brand",
|
||||||
|
name="client_certificates",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
help_text="Certificates used for client authentication.",
|
||||||
|
to="authentik_crypto.certificatekeypair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="brand",
|
||||||
|
name="web_certificate",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
help_text="Web Certificate used by the authentik Core webserver.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||||
|
related_name="+",
|
||||||
|
to="authentik_crypto.certificatekeypair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -33,6 +33,10 @@ class Brand(SerializerModel):
|
|||||||
|
|
||||||
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
|
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
|
||||||
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
|
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
|
||||||
|
branding_custom_css = models.TextField(default="", blank=True)
|
||||||
|
branding_default_flow_background = models.TextField(
|
||||||
|
default="/static/dist/assets/images/flow_background.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
flow_authentication = models.ForeignKey(
|
flow_authentication = models.ForeignKey(
|
||||||
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
|
||||||
@ -69,6 +73,13 @@ class Brand(SerializerModel):
|
|||||||
default=None,
|
default=None,
|
||||||
on_delete=models.SET_DEFAULT,
|
on_delete=models.SET_DEFAULT,
|
||||||
help_text=_("Web Certificate used by the authentik Core webserver."),
|
help_text=_("Web Certificate used by the authentik Core webserver."),
|
||||||
|
related_name="+",
|
||||||
|
)
|
||||||
|
client_certificates = models.ManyToManyField(
|
||||||
|
CertificateKeyPair,
|
||||||
|
default=None,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Certificates used for client authentication."),
|
||||||
)
|
)
|
||||||
attributes = models.JSONField(default=dict, blank=True)
|
attributes = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
@ -84,6 +95,12 @@ class Brand(SerializerModel):
|
|||||||
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
|
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
|
||||||
return self.branding_favicon
|
return self.branding_favicon
|
||||||
|
|
||||||
|
def branding_default_flow_background_url(self) -> str:
|
||||||
|
"""Get branding_default_flow_background with the correct prefix"""
|
||||||
|
if self.branding_default_flow_background.startswith("/static"):
|
||||||
|
return CONFIG.get("web.path", "/")[:-1] + self.branding_default_flow_background
|
||||||
|
return self.branding_default_flow_background
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Serializer:
|
def serializer(self) -> Serializer:
|
||||||
from authentik.brands.api import BrandSerializer
|
from authentik.brands.api import BrandSerializer
|
||||||
|
@ -24,6 +24,7 @@ class TestBrands(APITestCase):
|
|||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "authentik",
|
"branding_title": "authentik",
|
||||||
|
"branding_custom_css": "",
|
||||||
"matched_domain": brand.domain,
|
"matched_domain": brand.domain,
|
||||||
"ui_footer_links": [],
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
@ -43,6 +44,7 @@ class TestBrands(APITestCase):
|
|||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "custom",
|
"branding_title": "custom",
|
||||||
|
"branding_custom_css": "",
|
||||||
"matched_domain": "bar.baz",
|
"matched_domain": "bar.baz",
|
||||||
"ui_footer_links": [],
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
@ -59,6 +61,7 @@ class TestBrands(APITestCase):
|
|||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "authentik",
|
"branding_title": "authentik",
|
||||||
|
"branding_custom_css": "",
|
||||||
"matched_domain": "fallback",
|
"matched_domain": "fallback",
|
||||||
"ui_footer_links": [],
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
@ -121,3 +124,27 @@ class TestBrands(APITestCase):
|
|||||||
"subject": None,
|
"subject": None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_branding_url(self):
|
||||||
|
"""Test branding attributes return correct values"""
|
||||||
|
brand = create_test_brand()
|
||||||
|
brand.branding_default_flow_background = "https://goauthentik.io/img/icon.png"
|
||||||
|
brand.branding_favicon = "https://goauthentik.io/img/icon.png"
|
||||||
|
brand.branding_logo = "https://goauthentik.io/img/icon.png"
|
||||||
|
brand.save()
|
||||||
|
self.assertEqual(
|
||||||
|
brand.branding_default_flow_background_url(), "https://goauthentik.io/img/icon.png"
|
||||||
|
)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
|
||||||
|
{
|
||||||
|
"branding_logo": "https://goauthentik.io/img/icon.png",
|
||||||
|
"branding_favicon": "https://goauthentik.io/img/icon.png",
|
||||||
|
"branding_title": "authentik",
|
||||||
|
"branding_custom_css": "",
|
||||||
|
"matched_domain": brand.domain,
|
||||||
|
"ui_footer_links": [],
|
||||||
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
|
"default_locale": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@ -5,10 +5,10 @@ from typing import Any
|
|||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.db.models import Value as V
|
from django.db.models import Value as V
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from sentry_sdk import get_current_span
|
|
||||||
|
|
||||||
from authentik import get_full_version
|
from authentik import get_full_version
|
||||||
from authentik.brands.models import Brand
|
from authentik.brands.models import Brand
|
||||||
|
from authentik.lib.sentry import get_http_meta
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
_q_default = Q(default=True)
|
_q_default = Q(default=True)
|
||||||
@ -32,13 +32,9 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
|
|||||||
"""Context Processor that injects brand object into every template"""
|
"""Context Processor that injects brand object into every template"""
|
||||||
brand = getattr(request, "brand", DEFAULT_BRAND)
|
brand = getattr(request, "brand", DEFAULT_BRAND)
|
||||||
tenant = getattr(request, "tenant", Tenant())
|
tenant = getattr(request, "tenant", Tenant())
|
||||||
trace = ""
|
|
||||||
span = get_current_span()
|
|
||||||
if span:
|
|
||||||
trace = span.to_traceparent()
|
|
||||||
return {
|
return {
|
||||||
"brand": brand,
|
"brand": brand,
|
||||||
"footer_links": tenant.footer_links,
|
"footer_links": tenant.footer_links,
|
||||||
"sentry_trace": trace,
|
"html_meta": {**get_http_meta()},
|
||||||
"version": get_full_version(),
|
"version": get_full_version(),
|
||||||
}
|
}
|
||||||
|
58
authentik/core/api/application_entitlements.py
Normal file
58
authentik/core/api/application_entitlements.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"""Application Roles API Viewset"""
|
||||||
|
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
from authentik.core.api.utils import ModelSerializer
|
||||||
|
from authentik.core.models import (
|
||||||
|
Application,
|
||||||
|
ApplicationEntitlement,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationEntitlementSerializer(ModelSerializer):
|
||||||
|
"""ApplicationEntitlement Serializer"""
|
||||||
|
|
||||||
|
def validate_app(self, app: Application) -> Application:
|
||||||
|
"""Ensure user has permission to view"""
|
||||||
|
request: HttpRequest = self.context.get("request")
|
||||||
|
if not request and SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
||||||
|
return app
|
||||||
|
user = request.user
|
||||||
|
if user.has_perm("view_application", app) or user.has_perm(
|
||||||
|
"authentik_core.view_application"
|
||||||
|
):
|
||||||
|
return app
|
||||||
|
raise ValidationError(_("User does not have access to application."), code="invalid")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ApplicationEntitlement
|
||||||
|
fields = [
|
||||||
|
"pbm_uuid",
|
||||||
|
"name",
|
||||||
|
"app",
|
||||||
|
"attributes",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationEntitlementViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
"""ApplicationEntitlement Viewset"""
|
||||||
|
|
||||||
|
queryset = ApplicationEntitlement.objects.all()
|
||||||
|
serializer_class = ApplicationEntitlementSerializer
|
||||||
|
search_fields = [
|
||||||
|
"pbm_uuid",
|
||||||
|
"name",
|
||||||
|
"app",
|
||||||
|
"attributes",
|
||||||
|
]
|
||||||
|
filterset_fields = [
|
||||||
|
"pbm_uuid",
|
||||||
|
"name",
|
||||||
|
"app",
|
||||||
|
]
|
||||||
|
ordering = ["name"]
|
@ -46,7 +46,7 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
|
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
|
||||||
"""Cache key where application list for user is saved"""
|
"""Cache key where application list for user is saved"""
|
||||||
key = f"{CACHE_PREFIX}/app_access/{user_pk}"
|
key = f"{CACHE_PREFIX}app_access/{user_pk}"
|
||||||
if page_number:
|
if page_number:
|
||||||
key += f"/{page_number}"
|
key += f"/{page_number}"
|
||||||
return key
|
return key
|
||||||
|
@ -2,16 +2,13 @@
|
|||||||
|
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
|
||||||
from guardian.utils import get_anonymous_user
|
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.serializers import CharField, DateTimeField, IPAddressField
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from ua_parser import user_agent_parser
|
from ua_parser import user_agent_parser
|
||||||
|
|
||||||
from authentik.api.authorization import OwnerSuperuserPermissions
|
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import ModelSerializer
|
from authentik.core.api.utils import ModelSerializer
|
||||||
from authentik.core.models import AuthenticatedSession
|
from authentik.core.models import AuthenticatedSession
|
||||||
@ -58,6 +55,11 @@ class UserAgentDict(TypedDict):
|
|||||||
class AuthenticatedSessionSerializer(ModelSerializer):
|
class AuthenticatedSessionSerializer(ModelSerializer):
|
||||||
"""AuthenticatedSession Serializer"""
|
"""AuthenticatedSession Serializer"""
|
||||||
|
|
||||||
|
expires = DateTimeField(source="session.expires", read_only=True)
|
||||||
|
last_ip = IPAddressField(source="session.last_ip", read_only=True)
|
||||||
|
last_user_agent = CharField(source="session.last_user_agent", read_only=True)
|
||||||
|
last_used = DateTimeField(source="session.last_used", read_only=True)
|
||||||
|
|
||||||
current = SerializerMethodField()
|
current = SerializerMethodField()
|
||||||
user_agent = SerializerMethodField()
|
user_agent = SerializerMethodField()
|
||||||
geo_ip = SerializerMethodField()
|
geo_ip = SerializerMethodField()
|
||||||
@ -66,19 +68,19 @@ class AuthenticatedSessionSerializer(ModelSerializer):
|
|||||||
def get_current(self, instance: AuthenticatedSession) -> bool:
|
def get_current(self, instance: AuthenticatedSession) -> bool:
|
||||||
"""Check if session is currently active session"""
|
"""Check if session is currently active session"""
|
||||||
request: Request = self.context["request"]
|
request: Request = self.context["request"]
|
||||||
return request._request.session.session_key == instance.session_key
|
return request._request.session.session_key == instance.session.session_key
|
||||||
|
|
||||||
def get_user_agent(self, instance: AuthenticatedSession) -> UserAgentDict:
|
def get_user_agent(self, instance: AuthenticatedSession) -> UserAgentDict:
|
||||||
"""Get parsed user agent"""
|
"""Get parsed user agent"""
|
||||||
return user_agent_parser.Parse(instance.last_user_agent)
|
return user_agent_parser.Parse(instance.session.last_user_agent)
|
||||||
|
|
||||||
def get_geo_ip(self, instance: AuthenticatedSession) -> GeoIPDict | None: # pragma: no cover
|
def get_geo_ip(self, instance: AuthenticatedSession) -> GeoIPDict | None: # pragma: no cover
|
||||||
"""Get GeoIP Data"""
|
"""Get GeoIP Data"""
|
||||||
return GEOIP_CONTEXT_PROCESSOR.city_dict(instance.last_ip)
|
return GEOIP_CONTEXT_PROCESSOR.city_dict(instance.session.last_ip)
|
||||||
|
|
||||||
def get_asn(self, instance: AuthenticatedSession) -> ASNDict | None: # pragma: no cover
|
def get_asn(self, instance: AuthenticatedSession) -> ASNDict | None: # pragma: no cover
|
||||||
"""Get ASN Data"""
|
"""Get ASN Data"""
|
||||||
return ASN_CONTEXT_PROCESSOR.asn_dict(instance.last_ip)
|
return ASN_CONTEXT_PROCESSOR.asn_dict(instance.session.last_ip)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AuthenticatedSession
|
model = AuthenticatedSession
|
||||||
@ -94,6 +96,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
|
|||||||
"last_used",
|
"last_used",
|
||||||
"expires",
|
"expires",
|
||||||
]
|
]
|
||||||
|
extra_args = {"uuid": {"read_only": True}}
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatedSessionViewSet(
|
class AuthenticatedSessionViewSet(
|
||||||
@ -105,16 +108,10 @@ class AuthenticatedSessionViewSet(
|
|||||||
):
|
):
|
||||||
"""AuthenticatedSession Viewset"""
|
"""AuthenticatedSession Viewset"""
|
||||||
|
|
||||||
queryset = AuthenticatedSession.objects.all()
|
lookup_field = "uuid"
|
||||||
|
queryset = AuthenticatedSession.objects.select_related("session").all()
|
||||||
serializer_class = AuthenticatedSessionSerializer
|
serializer_class = AuthenticatedSessionSerializer
|
||||||
search_fields = ["user__username", "last_ip", "last_user_agent"]
|
search_fields = ["user__username", "session__last_ip", "session__last_user_agent"]
|
||||||
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
|
filterset_fields = ["user__username", "session__last_ip", "session__last_user_agent"]
|
||||||
ordering = ["user__username"]
|
ordering = ["user__username"]
|
||||||
permission_classes = [OwnerSuperuserPermissions]
|
owner_field = "user"
|
||||||
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
|
||||||
if user.is_superuser:
|
|
||||||
return super().get_queryset()
|
|
||||||
return super().get_queryset().filter(user=user.pk)
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||||
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.fields import (
|
from rest_framework.fields import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
CharField,
|
CharField,
|
||||||
@ -16,7 +17,6 @@ from rest_framework.viewsets import ViewSet
|
|||||||
|
|
||||||
from authentik.core.api.utils import MetaNameSerializer
|
from authentik.core.api.utils import MetaNameSerializer
|
||||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
|
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
|
||||||
from authentik.rbac.decorators import permission_required
|
|
||||||
from authentik.stages.authenticator import device_classes, devices_for_user
|
from authentik.stages.authenticator import device_classes, devices_for_user
|
||||||
from authentik.stages.authenticator.models import Device
|
from authentik.stages.authenticator.models import Device
|
||||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||||
@ -73,7 +73,9 @@ class AdminDeviceViewSet(ViewSet):
|
|||||||
def get_devices(self, **kwargs):
|
def get_devices(self, **kwargs):
|
||||||
"""Get all devices in all child classes"""
|
"""Get all devices in all child classes"""
|
||||||
for model in device_classes():
|
for model in device_classes():
|
||||||
device_set = model.objects.filter(**kwargs)
|
device_set = get_objects_for_user(
|
||||||
|
self.request.user, f"{model._meta.app_label}.view_{model._meta.model_name}", model
|
||||||
|
).filter(**kwargs)
|
||||||
yield from device_set
|
yield from device_set
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
@ -86,10 +88,6 @@ class AdminDeviceViewSet(ViewSet):
|
|||||||
],
|
],
|
||||||
responses={200: DeviceSerializer(many=True)},
|
responses={200: DeviceSerializer(many=True)},
|
||||||
)
|
)
|
||||||
@permission_required(
|
|
||||||
None,
|
|
||||||
[f"{model._meta.app_label}.view_{model._meta.model_name}" for model in device_classes()],
|
|
||||||
)
|
|
||||||
def list(self, request: Request) -> Response:
|
def list(self, request: Request) -> Response:
|
||||||
"""Get all devices for current user"""
|
"""Get all devices for current user"""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
@ -4,6 +4,7 @@ from json import loads
|
|||||||
|
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
|
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from drf_spectacular.utils import (
|
from drf_spectacular.utils import (
|
||||||
@ -81,9 +82,36 @@ class GroupSerializer(ModelSerializer):
|
|||||||
if not self.instance or not parent:
|
if not self.instance or not parent:
|
||||||
return parent
|
return parent
|
||||||
if str(parent.group_uuid) == str(self.instance.group_uuid):
|
if str(parent.group_uuid) == str(self.instance.group_uuid):
|
||||||
raise ValidationError("Cannot set group as parent of itself.")
|
raise ValidationError(_("Cannot set group as parent of itself."))
|
||||||
return parent
|
return parent
|
||||||
|
|
||||||
|
def validate_is_superuser(self, superuser: bool):
|
||||||
|
"""Ensure that the user creating this group has permissions to set the superuser flag"""
|
||||||
|
request: Request = self.context.get("request", None)
|
||||||
|
if not request:
|
||||||
|
return superuser
|
||||||
|
# If we're updating an instance, and the state hasn't changed, we don't need to check perms
|
||||||
|
if self.instance and superuser == self.instance.is_superuser:
|
||||||
|
return superuser
|
||||||
|
user: User = request.user
|
||||||
|
perm = (
|
||||||
|
"authentik_core.enable_group_superuser"
|
||||||
|
if superuser
|
||||||
|
else "authentik_core.disable_group_superuser"
|
||||||
|
)
|
||||||
|
if self.instance or superuser:
|
||||||
|
has_perm = user.has_perm(perm) or user.has_perm(perm, self.instance)
|
||||||
|
if not has_perm:
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
(
|
||||||
|
"User does not have permission to set "
|
||||||
|
"superuser status to {superuser_status}."
|
||||||
|
).format_map({"superuser_status": superuser})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return superuser
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Group
|
model = Group
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -2,19 +2,17 @@
|
|||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
|
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
|
|
||||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||||
from authentik.core.api.object_types import TypesMixin
|
from authentik.core.api.object_types import TypesMixin
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
@ -88,7 +86,7 @@ class SourceViewSet(
|
|||||||
serializer_class = SourceSerializer
|
serializer_class = SourceSerializer
|
||||||
lookup_field = "slug"
|
lookup_field = "slug"
|
||||||
search_fields = ["slug", "name"]
|
search_fields = ["slug", "name"]
|
||||||
filterset_fields = ["slug", "name", "managed"]
|
filterset_fields = ["slug", "name", "managed", "pbm_uuid"]
|
||||||
|
|
||||||
def get_queryset(self): # pragma: no cover
|
def get_queryset(self): # pragma: no cover
|
||||||
return Source.objects.select_subclasses()
|
return Source.objects.select_subclasses()
|
||||||
@ -157,11 +155,22 @@ class SourceViewSet(
|
|||||||
matching_sources.append(source_settings.validated_data)
|
matching_sources.append(source_settings.validated_data)
|
||||||
return Response(matching_sources)
|
return Response(matching_sources)
|
||||||
|
|
||||||
|
def destroy(self, request: Request, *args, **kwargs):
|
||||||
|
"""Prevent deletion of built-in sources"""
|
||||||
|
instance: Source = self.get_object()
|
||||||
|
|
||||||
|
if instance.managed == Source.MANAGED_INBUILT:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "Built-in sources cannot be deleted"}, code="protected"
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class UserSourceConnectionSerializer(SourceSerializer):
|
class UserSourceConnectionSerializer(SourceSerializer):
|
||||||
"""OAuth Source Serializer"""
|
"""User source connection"""
|
||||||
|
|
||||||
source = SourceSerializer(read_only=True)
|
source_obj = SourceSerializer(read_only=True, source="source")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserSourceConnection
|
model = UserSourceConnection
|
||||||
@ -169,11 +178,14 @@ class UserSourceConnectionSerializer(SourceSerializer):
|
|||||||
"pk",
|
"pk",
|
||||||
"user",
|
"user",
|
||||||
"source",
|
"source",
|
||||||
|
"source_obj",
|
||||||
|
"identifier",
|
||||||
"created",
|
"created",
|
||||||
|
"last_updated",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"user": {"read_only": True},
|
|
||||||
"created": {"read_only": True},
|
"created": {"read_only": True},
|
||||||
|
"last_updated": {"read_only": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -189,17 +201,16 @@ class UserSourceConnectionViewSet(
|
|||||||
|
|
||||||
queryset = UserSourceConnection.objects.all()
|
queryset = UserSourceConnection.objects.all()
|
||||||
serializer_class = UserSourceConnectionSerializer
|
serializer_class = UserSourceConnectionSerializer
|
||||||
permission_classes = [OwnerSuperuserPermissions]
|
|
||||||
filterset_fields = ["user", "source__slug"]
|
filterset_fields = ["user", "source__slug"]
|
||||||
search_fields = ["source__slug"]
|
search_fields = ["user__username", "source__slug", "identifier"]
|
||||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
|
||||||
ordering = ["source__slug", "pk"]
|
ordering = ["source__slug", "pk"]
|
||||||
|
owner_field = "user"
|
||||||
|
|
||||||
|
|
||||||
class GroupSourceConnectionSerializer(SourceSerializer):
|
class GroupSourceConnectionSerializer(SourceSerializer):
|
||||||
"""Group Source Connection Serializer"""
|
"""Group Source Connection"""
|
||||||
|
|
||||||
source = SourceSerializer(read_only=True)
|
source_obj = SourceSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GroupSourceConnection
|
model = GroupSourceConnection
|
||||||
@ -207,13 +218,14 @@ class GroupSourceConnectionSerializer(SourceSerializer):
|
|||||||
"pk",
|
"pk",
|
||||||
"group",
|
"group",
|
||||||
"source",
|
"source",
|
||||||
|
"source_obj",
|
||||||
"identifier",
|
"identifier",
|
||||||
"created",
|
"created",
|
||||||
|
"last_updated",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"group": {"read_only": True},
|
|
||||||
"identifier": {"read_only": True},
|
|
||||||
"created": {"read_only": True},
|
"created": {"read_only": True},
|
||||||
|
"last_updated": {"read_only": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -229,8 +241,6 @@ class GroupSourceConnectionViewSet(
|
|||||||
|
|
||||||
queryset = GroupSourceConnection.objects.all()
|
queryset = GroupSourceConnection.objects.all()
|
||||||
serializer_class = GroupSourceConnectionSerializer
|
serializer_class = GroupSourceConnectionSerializer
|
||||||
permission_classes = [OwnerSuperuserPermissions]
|
|
||||||
filterset_fields = ["group", "source__slug"]
|
filterset_fields = ["group", "source__slug"]
|
||||||
search_fields = ["source__slug"]
|
search_fields = ["group__name", "source__slug", "identifier"]
|
||||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
|
||||||
ordering = ["source__slug", "pk"]
|
ordering = ["source__slug", "pk"]
|
||||||
|
@ -3,18 +3,15 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
||||||
from guardian.shortcuts import assign_perm, get_anonymous_user
|
from guardian.shortcuts import assign_perm, get_anonymous_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.api.authorization import OwnerSuperuserPermissions
|
|
||||||
from authentik.blueprints.api import ManagedSerializer
|
from authentik.blueprints.api import ManagedSerializer
|
||||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
@ -138,8 +135,8 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"managed",
|
"managed",
|
||||||
]
|
]
|
||||||
ordering = ["identifier", "expires"]
|
ordering = ["identifier", "expires"]
|
||||||
permission_classes = [OwnerSuperuserPermissions]
|
owner_field = "user"
|
||||||
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
rbac_allow_create_without_perm = True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
user = self.request.user if self.request else get_anonymous_user()
|
||||||
|
@ -22,7 +22,7 @@ from authentik.blueprints.v1.common import (
|
|||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
from authentik.core.api.applications import ApplicationSerializer
|
from authentik.core.api.applications import ApplicationSerializer
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Application, Provider
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
from authentik.policies.api.bindings import PolicyBindingSerializer
|
from authentik.policies.api.bindings import PolicyBindingSerializer
|
||||||
|
|
||||||
@ -51,6 +51,13 @@ class TransactionProviderField(DictField):
|
|||||||
class TransactionPolicyBindingSerializer(PolicyBindingSerializer):
|
class TransactionPolicyBindingSerializer(PolicyBindingSerializer):
|
||||||
"""PolicyBindingSerializer which does not require target as target is set implicitly"""
|
"""PolicyBindingSerializer which does not require target as target is set implicitly"""
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
# As the PolicyBindingSerializer checks that the correct things can be bound to a target
|
||||||
|
# but we don't have a target here as that's set by the blueprint, pass in an empty app
|
||||||
|
# which will have the correct allowed combination of group/user/policy.
|
||||||
|
attrs["target"] = Application()
|
||||||
|
return super().validate(attrs)
|
||||||
|
|
||||||
class Meta(PolicyBindingSerializer.Meta):
|
class Meta(PolicyBindingSerializer.Meta):
|
||||||
fields = [x for x in PolicyBindingSerializer.Meta.fields if x != "target"]
|
fields = [x for x in PolicyBindingSerializer.Meta.fields if x != "target"]
|
||||||
|
|
||||||
|
@ -6,8 +6,6 @@ from typing import Any
|
|||||||
|
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.db.models.functions import ExtractHour
|
from django.db.models.functions import ExtractHour
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
@ -71,8 +69,8 @@ from authentik.core.middleware import (
|
|||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||||
USER_PATH_SERVICE_ACCOUNT,
|
USER_PATH_SERVICE_ACCOUNT,
|
||||||
AuthenticatedSession,
|
|
||||||
Group,
|
Group,
|
||||||
|
Session,
|
||||||
Token,
|
Token,
|
||||||
TokenIntents,
|
TokenIntents,
|
||||||
User,
|
User,
|
||||||
@ -86,6 +84,7 @@ from authentik.flows.views.executor import QS_KEY_TOKEN
|
|||||||
from authentik.lib.avatars import get_avatar
|
from authentik.lib.avatars import get_avatar
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
from authentik.rbac.models import get_permission_choices
|
from authentik.rbac.models import get_permission_choices
|
||||||
|
from authentik.stages.email.flow import pickle_flow_token_for_email
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.stages.email.tasks import send_mails
|
from authentik.stages.email.tasks import send_mails
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
@ -226,6 +225,7 @@ class UserSerializer(ModelSerializer):
|
|||||||
"name",
|
"name",
|
||||||
"is_active",
|
"is_active",
|
||||||
"last_login",
|
"last_login",
|
||||||
|
"date_joined",
|
||||||
"is_superuser",
|
"is_superuser",
|
||||||
"groups",
|
"groups",
|
||||||
"groups_obj",
|
"groups_obj",
|
||||||
@ -236,9 +236,12 @@ class UserSerializer(ModelSerializer):
|
|||||||
"path",
|
"path",
|
||||||
"type",
|
"type",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"password_change_date",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"name": {"allow_blank": True},
|
"name": {"allow_blank": True},
|
||||||
|
"date_joined": {"read_only": True},
|
||||||
|
"password_change_date": {"read_only": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -371,7 +374,7 @@ class UsersFilter(FilterSet):
|
|||||||
method="filter_attributes",
|
method="filter_attributes",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
||||||
uuid = UUIDFilter(field_name="uuid")
|
uuid = UUIDFilter(field_name="uuid")
|
||||||
|
|
||||||
path = CharFilter(field_name="path")
|
path = CharFilter(field_name="path")
|
||||||
@ -389,6 +392,11 @@ class UsersFilter(FilterSet):
|
|||||||
queryset=Group.objects.all().order_by("name"),
|
queryset=Group.objects.all().order_by("name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def filter_is_superuser(self, queryset, name, value):
|
||||||
|
if value:
|
||||||
|
return queryset.filter(ak_groups__is_superuser=True).distinct()
|
||||||
|
return queryset.exclude(ak_groups__is_superuser=True).distinct()
|
||||||
|
|
||||||
def filter_attributes(self, queryset, name, value):
|
def filter_attributes(self, queryset, name, value):
|
||||||
"""Filter attributes by query args"""
|
"""Filter attributes by query args"""
|
||||||
try:
|
try:
|
||||||
@ -427,7 +435,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
queryset = User.objects.none()
|
queryset = User.objects.none()
|
||||||
ordering = ["username"]
|
ordering = ["username"]
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
search_fields = ["username", "name", "is_active", "email", "uuid"]
|
search_fields = ["username", "name", "is_active", "email", "uuid", "attributes"]
|
||||||
filterset_class = UsersFilter
|
filterset_class = UsersFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -444,7 +452,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
def _create_recovery_link(self) -> tuple[str, Token]:
|
def _create_recovery_link(self, for_email=False) -> tuple[str, Token]:
|
||||||
"""Create a recovery link (when the current brand has a recovery flow set),
|
"""Create a recovery link (when the current brand has a recovery flow set),
|
||||||
that can either be shown to an admin or sent to the user directly"""
|
that can either be shown to an admin or sent to the user directly"""
|
||||||
brand: Brand = self.request._request.brand
|
brand: Brand = self.request._request.brand
|
||||||
@ -466,12 +474,16 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{"non_field_errors": "Recovery flow not applicable to user"}
|
{"non_field_errors": "Recovery flow not applicable to user"}
|
||||||
) from None
|
) from None
|
||||||
|
_plan = FlowToken.pickle(plan)
|
||||||
|
if for_email:
|
||||||
|
_plan = pickle_flow_token_for_email(plan)
|
||||||
token, __ = FlowToken.objects.update_or_create(
|
token, __ = FlowToken.objects.update_or_create(
|
||||||
identifier=f"{user.uid}-password-reset",
|
identifier=f"{user.uid}-password-reset",
|
||||||
defaults={
|
defaults={
|
||||||
"user": user,
|
"user": user,
|
||||||
"flow": flow,
|
"flow": flow,
|
||||||
"_plan": FlowToken.pickle(plan),
|
"_plan": _plan,
|
||||||
|
"revoke_on_execution": not for_email,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
querystring = urlencode({QS_KEY_TOKEN: token.key})
|
querystring = urlencode({QS_KEY_TOKEN: token.key})
|
||||||
@ -585,7 +597,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""Set password for user"""
|
"""Set password for user"""
|
||||||
user: User = self.get_object()
|
user: User = self.get_object()
|
||||||
try:
|
try:
|
||||||
user.set_password(request.data.get("password"))
|
user.set_password(request.data.get("password"), request=request)
|
||||||
user.save()
|
user.save()
|
||||||
except (ValidationError, IntegrityError) as exc:
|
except (ValidationError, IntegrityError) as exc:
|
||||||
LOGGER.debug("Failed to set password", exc=exc)
|
LOGGER.debug("Failed to set password", exc=exc)
|
||||||
@ -641,7 +653,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
if for_user.email == "":
|
if for_user.email == "":
|
||||||
LOGGER.debug("User doesn't have an email address")
|
LOGGER.debug("User doesn't have an email address")
|
||||||
raise ValidationError({"non_field_errors": "User does not have an email address set."})
|
raise ValidationError({"non_field_errors": "User does not have an email address set."})
|
||||||
link, token = self._create_recovery_link()
|
link, token = self._create_recovery_link(for_email=True)
|
||||||
# Lookup the email stage to assure the current user can access it
|
# Lookup the email stage to assure the current user can access it
|
||||||
stages = get_objects_for_user(
|
stages = get_objects_for_user(
|
||||||
request.user, "authentik_stages_email.view_emailstage"
|
request.user, "authentik_stages_email.view_emailstage"
|
||||||
@ -765,9 +777,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
response = super().partial_update(request, *args, **kwargs)
|
response = super().partial_update(request, *args, **kwargs)
|
||||||
instance: User = self.get_object()
|
instance: User = self.get_object()
|
||||||
if not instance.is_active:
|
if not instance.is_active:
|
||||||
sessions = AuthenticatedSession.objects.filter(user=instance)
|
Session.objects.filter(authenticatedsession__user=instance).delete()
|
||||||
session_ids = sessions.values_list("session_key", flat=True)
|
|
||||||
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
|
|
||||||
sessions.delete()
|
|
||||||
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
||||||
return response
|
return response
|
||||||
|
@ -20,6 +20,8 @@ from rest_framework.serializers import (
|
|||||||
raise_errors_on_nested_writes,
|
raise_errors_on_nested_writes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from authentik.rbac.permissions import assign_initial_permissions
|
||||||
|
|
||||||
|
|
||||||
def is_dict(value: Any):
|
def is_dict(value: Any):
|
||||||
"""Ensure a value is a dictionary, useful for JSONFields"""
|
"""Ensure a value is a dictionary, useful for JSONFields"""
|
||||||
@ -29,6 +31,14 @@ def is_dict(value: Any):
|
|||||||
|
|
||||||
|
|
||||||
class ModelSerializer(BaseModelSerializer):
|
class ModelSerializer(BaseModelSerializer):
|
||||||
|
def create(self, validated_data):
|
||||||
|
instance = super().create(validated_data)
|
||||||
|
|
||||||
|
request = self.context.get("request")
|
||||||
|
if request and hasattr(request, "user") and not request.user.is_anonymous:
|
||||||
|
assign_initial_permissions(request.user, instance)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data):
|
def update(self, instance: Model, validated_data):
|
||||||
raise_errors_on_nested_writes("update", self, validated_data)
|
raise_errors_on_nested_writes("update", self, validated_data)
|
||||||
|
@ -32,5 +32,5 @@ class AuthentikCoreConfig(ManagedAppConfig):
|
|||||||
"name": "authentik Built-in",
|
"name": "authentik Built-in",
|
||||||
"slug": "authentik-built-in",
|
"slug": "authentik-built-in",
|
||||||
},
|
},
|
||||||
managed="goauthentik.io/sources/inbuilt",
|
managed=Source.MANAGED_INBUILT,
|
||||||
)
|
)
|
||||||
|
@ -24,6 +24,15 @@ class InbuiltBackend(ModelBackend):
|
|||||||
self.set_method("password", request)
|
self.set_method("password", request)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
async def aauthenticate(
|
||||||
|
self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
|
||||||
|
) -> User | None:
|
||||||
|
user = await super().aauthenticate(request, username=username, password=password, **kwargs)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
self.set_method("password", request)
|
||||||
|
return user
|
||||||
|
|
||||||
def set_method(self, method: str, request: HttpRequest | None, **kwargs):
|
def set_method(self, method: str, request: HttpRequest | None, **kwargs):
|
||||||
"""Set method data on current flow, if possbiel"""
|
"""Set method data on current flow, if possbiel"""
|
||||||
if not request:
|
if not request:
|
||||||
@ -44,13 +53,12 @@ class TokenBackend(InbuiltBackend):
|
|||||||
self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
|
self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
user = User._default_manager.get_by_natural_key(username)
|
user = User._default_manager.get_by_natural_key(username)
|
||||||
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
# Run the default password hasher once to reduce the timing
|
# Run the default password hasher once to reduce the timing
|
||||||
# difference between an existing and a nonexistent user (#20760).
|
# difference between an existing and a nonexistent user (#20760).
|
||||||
User().set_password(password)
|
User().set_password(password, request=request)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tokens = Token.filter_not_expired(
|
tokens = Token.filter_not_expired(
|
||||||
|
@ -58,6 +58,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
|||||||
self._context["user"] = user
|
self._context["user"] = user
|
||||||
if request:
|
if request:
|
||||||
req.http_request = request
|
req.http_request = request
|
||||||
|
self._context["http_request"] = request
|
||||||
req.context.update(**kwargs)
|
req.context.update(**kwargs)
|
||||||
self._context["request"] = req
|
self._context["request"] = req
|
||||||
self._context.update(**kwargs)
|
self._context.update(**kwargs)
|
||||||
|
15
authentik/core/management/commands/clearsessions.py
Normal file
15
authentik/core/management/commands/clearsessions.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""Change user type"""
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from authentik.tenants.management import TenantCommand
|
||||||
|
|
||||||
|
|
||||||
|
class Command(TenantCommand):
|
||||||
|
"""Delete all sessions"""
|
||||||
|
|
||||||
|
def handle_per_tenant(self, **options):
|
||||||
|
engine = import_module(settings.SESSION_ENGINE)
|
||||||
|
engine.SessionStore.clear_expired()
|
@ -5,6 +5,7 @@ from typing import TextIO
|
|||||||
from daphne.management.commands.runserver import Command as RunServer
|
from daphne.management.commands.runserver import Command as RunServer
|
||||||
from daphne.server import Server
|
from daphne.server import Server
|
||||||
|
|
||||||
|
from authentik.lib.debug import start_debug_server
|
||||||
from authentik.root.signals import post_startup, pre_startup, startup
|
from authentik.root.signals import post_startup, pre_startup, startup
|
||||||
|
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ class SignalServer(Server):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
start_debug_server()
|
||||||
|
|
||||||
def ready_callable():
|
def ready_callable():
|
||||||
pre_startup.send(sender=self)
|
pre_startup.send(sender=self)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.auth.management import create_permissions
|
from django.contrib.auth.management import create_permissions
|
||||||
|
from django.core.management import call_command
|
||||||
from django.core.management.base import BaseCommand, no_translations
|
from django.core.management.base import BaseCommand, no_translations
|
||||||
from guardian.management import create_anonymous_user
|
from guardian.management import create_anonymous_user
|
||||||
|
|
||||||
@ -16,6 +17,10 @@ class Command(BaseCommand):
|
|||||||
"""Check permissions for all apps"""
|
"""Check permissions for all apps"""
|
||||||
for tenant in Tenant.objects.filter(ready=True):
|
for tenant in Tenant.objects.filter(ready=True):
|
||||||
with tenant:
|
with tenant:
|
||||||
|
# See https://code.djangoproject.com/ticket/28417
|
||||||
|
# Remove potential lingering old permissions
|
||||||
|
call_command("remove_stale_contenttypes", "--no-input")
|
||||||
|
|
||||||
for app in apps.get_app_configs():
|
for app in apps.get_app_configs():
|
||||||
self.stdout.write(f"Checking app {app.name} ({app.label})\n")
|
self.stdout.write(f"Checking app {app.name} ({app.label})\n")
|
||||||
create_permissions(app, verbosity=0)
|
create_permissions(app, verbosity=0)
|
||||||
|
@ -17,7 +17,9 @@ from authentik.events.middleware import should_log_model
|
|||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.events.utils import model_to_dict
|
from authentik.events.utils import model_to_dict
|
||||||
|
|
||||||
BANNER_TEXT = f"""### authentik shell ({get_full_version()})
|
|
||||||
|
def get_banner_text(shell_type="shell") -> str:
|
||||||
|
return f"""### authentik {shell_type} ({get_full_version()})
|
||||||
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
|
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
|
||||||
|
|
||||||
|
|
||||||
@ -114,4 +116,4 @@ class Command(BaseCommand):
|
|||||||
readline.parse_and_bind("tab: complete")
|
readline.parse_and_bind("tab: complete")
|
||||||
|
|
||||||
# Run interactive shell
|
# Run interactive shell
|
||||||
code.interact(banner=BANNER_TEXT, local=namespace)
|
code.interact(banner=get_banner_text(), local=namespace)
|
||||||
|
@ -9,6 +9,7 @@ from django.db import close_old_connections
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
from authentik.lib.debug import start_debug_server
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -28,10 +29,7 @@ class Command(BaseCommand):
|
|||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
LOGGER.debug("Celery options", **options)
|
LOGGER.debug("Celery options", **options)
|
||||||
close_old_connections()
|
close_old_connections()
|
||||||
if CONFIG.get_bool("remote_debug"):
|
start_debug_server()
|
||||||
import debugpy
|
|
||||||
|
|
||||||
debugpy.listen(("0.0.0.0", 6900)) # nosec
|
|
||||||
worker: Worker = CELERY_APP.Worker(
|
worker: Worker = CELERY_APP.Worker(
|
||||||
no_color=False,
|
no_color=False,
|
||||||
quiet=True,
|
quiet=True,
|
||||||
|
@ -2,9 +2,14 @@
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
|
from functools import partial
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.utils.translation import override
|
from django.utils.translation import override
|
||||||
from sentry_sdk.api import set_tag
|
from sentry_sdk.api import set_tag
|
||||||
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
||||||
@ -20,6 +25,40 @@ CTX_HOST = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + "host", default=None)
|
|||||||
CTX_AUTH_VIA = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + KEY_AUTH_VIA, default=None)
|
CTX_AUTH_VIA = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + KEY_AUTH_VIA, default=None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user(request):
|
||||||
|
if not hasattr(request, "_cached_user"):
|
||||||
|
user = None
|
||||||
|
if (authenticated_session := request.session.get("authenticatedsession", None)) is not None:
|
||||||
|
user = authenticated_session.user
|
||||||
|
request._cached_user = user or AnonymousUser()
|
||||||
|
return request._cached_user
|
||||||
|
|
||||||
|
|
||||||
|
async def aget_user(request):
|
||||||
|
if not hasattr(request, "_cached_user"):
|
||||||
|
user = None
|
||||||
|
if (
|
||||||
|
authenticated_session := await request.session.aget("authenticatedsession", None)
|
||||||
|
) is not None:
|
||||||
|
user = authenticated_session.user
|
||||||
|
request._cached_user = user or AnonymousUser()
|
||||||
|
return request._cached_user
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationMiddleware(MiddlewareMixin):
|
||||||
|
def process_request(self, request):
|
||||||
|
if not hasattr(request, "session"):
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"The Django authentication middleware requires session "
|
||||||
|
"middleware to be installed. Edit your MIDDLEWARE setting to "
|
||||||
|
"insert "
|
||||||
|
"'authentik.root.middleware.SessionMiddleware' before "
|
||||||
|
"'authentik.core.middleware.AuthenticationMiddleware'."
|
||||||
|
)
|
||||||
|
request.user = SimpleLazyObject(lambda: get_user(request))
|
||||||
|
request.auser = partial(aget_user, request)
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateMiddleware:
|
class ImpersonateMiddleware:
|
||||||
"""Middleware to impersonate users"""
|
"""Middleware to impersonate users"""
|
||||||
|
|
||||||
|
45
authentik/core/migrations/0041_applicationentitlement.py
Normal file
45
authentik/core/migrations/0041_applicationentitlement.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-11-20 15:16
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0040_provider_invalidation_flow"),
|
||||||
|
("authentik_policies", "0011_policybinding_failure_result_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ApplicationEntitlement",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"policybindingmodel_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="authentik_policies.policybindingmodel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("attributes", models.JSONField(blank=True, default=dict)),
|
||||||
|
("name", models.TextField()),
|
||||||
|
(
|
||||||
|
"app",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.application"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Application Entitlement",
|
||||||
|
"verbose_name_plural": "Application Entitlements",
|
||||||
|
"unique_together": {("app", "name")},
|
||||||
|
},
|
||||||
|
bases=("authentik_policies.policybindingmodel", models.Model),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2025-01-13 18:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0041_applicationentitlement"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="authenticatedsession",
|
||||||
|
index=models.Index(fields=["expires"], name="authentik_c_expires_08251d_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="authenticatedsession",
|
||||||
|
index=models.Index(fields=["expiring"], name="authentik_c_expirin_9cd839_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="authenticatedsession",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["expiring", "expires"], name="authentik_c_expirin_195a84_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="authenticatedsession",
|
||||||
|
index=models.Index(fields=["session_key"], name="authentik_c_session_d0f005_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="token",
|
||||||
|
index=models.Index(fields=["expires"], name="authentik_c_expires_a62b4b_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="token",
|
||||||
|
index=models.Index(fields=["expiring"], name="authentik_c_expirin_a1b838_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="token",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["expiring", "expires"], name="authentik_c_expirin_ba04d9_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
26
authentik/core/migrations/0043_alter_group_options.py
Normal file
26
authentik/core/migrations/0043_alter_group_options.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.0.11 on 2025-01-30 23:55
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="group",
|
||||||
|
options={
|
||||||
|
"permissions": [
|
||||||
|
("add_user_to_group", "Add user to group"),
|
||||||
|
("remove_user_from_group", "Remove user from group"),
|
||||||
|
("enable_group_superuser", "Enable superuser status"),
|
||||||
|
("disable_group_superuser", "Disable superuser status"),
|
||||||
|
],
|
||||||
|
"verbose_name": "Group",
|
||||||
|
"verbose_name_plural": "Groups",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.0.13 on 2025-04-07 14:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0043_alter_group_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="usersourceconnection",
|
||||||
|
name="new_identifier",
|
||||||
|
field=models.TextField(default=""),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,30 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||||
|
("authentik_sources_kerberos", "0003_migrate_userkerberossourceconnection_identifier"),
|
||||||
|
("authentik_sources_oauth", "0009_migrate_useroauthsourceconnection_identifier"),
|
||||||
|
("authentik_sources_plex", "0005_migrate_userplexsourceconnection_identifier"),
|
||||||
|
("authentik_sources_saml", "0019_migrate_usersamlsourceconnection_identifier"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="usersourceconnection",
|
||||||
|
old_name="new_identifier",
|
||||||
|
new_name="identifier",
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="usersourceconnection",
|
||||||
|
index=models.Index(fields=["identifier"], name="authentik_c_identif_59226f_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="usersourceconnection",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["source", "identifier"], name="authentik_c_source__649e04_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
242
authentik/core/migrations/0046_session_and_more.py
Normal file
242
authentik/core/migrations/0046_session_and_more.py
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
# Generated by Django 5.0.11 on 2025-01-27 12:58
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
import pickle # nosec
|
||||||
|
from django.core import signing
|
||||||
|
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
|
from django.utils.timezone import now, timedelta
|
||||||
|
from authentik.lib.migrations import progress_bar
|
||||||
|
from authentik.root.middleware import ClientIPMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
SESSION_CACHE_ALIAS = "default"
|
||||||
|
|
||||||
|
|
||||||
|
class PickleSerializer:
|
||||||
|
"""
|
||||||
|
Simple wrapper around pickle to be used in signing.dumps()/loads() and
|
||||||
|
cache backends.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, protocol=None):
|
||||||
|
self.protocol = pickle.HIGHEST_PROTOCOL if protocol is None else protocol
|
||||||
|
|
||||||
|
def dumps(self, obj):
|
||||||
|
"""Pickle data to be stored in redis"""
|
||||||
|
return pickle.dumps(obj, self.protocol)
|
||||||
|
|
||||||
|
def loads(self, data):
|
||||||
|
"""Unpickle data to be loaded from redis"""
|
||||||
|
try:
|
||||||
|
return pickle.loads(data) # nosec
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_session(
|
||||||
|
apps,
|
||||||
|
db_alias,
|
||||||
|
session_key,
|
||||||
|
session_data,
|
||||||
|
expires,
|
||||||
|
):
|
||||||
|
Session = apps.get_model("authentik_core", "Session")
|
||||||
|
OldAuthenticatedSession = apps.get_model("authentik_core", "OldAuthenticatedSession")
|
||||||
|
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
|
||||||
|
|
||||||
|
old_auth_session = (
|
||||||
|
OldAuthenticatedSession.objects.using(db_alias).filter(session_key=session_key).first()
|
||||||
|
)
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"session_key": session_key,
|
||||||
|
"expires": expires,
|
||||||
|
"last_ip": ClientIPMiddleware.default_ip,
|
||||||
|
"last_user_agent": "",
|
||||||
|
"session_data": {},
|
||||||
|
}
|
||||||
|
for k, v in session_data.items():
|
||||||
|
if k == "authentik/stages/user_login/last_ip":
|
||||||
|
args["last_ip"] = v
|
||||||
|
elif k in ["last_user_agent", "last_used"]:
|
||||||
|
args[k] = v
|
||||||
|
elif args in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY]:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
args["session_data"][k] = v
|
||||||
|
if old_auth_session:
|
||||||
|
args["last_user_agent"] = old_auth_session.last_user_agent
|
||||||
|
args["last_used"] = old_auth_session.last_used
|
||||||
|
|
||||||
|
args["session_data"] = pickle.dumps(args["session_data"])
|
||||||
|
session = Session.objects.using(db_alias).create(**args)
|
||||||
|
|
||||||
|
if old_auth_session:
|
||||||
|
AuthenticatedSession.objects.using(db_alias).create(
|
||||||
|
session=session,
|
||||||
|
user=old_auth_session.user,
|
||||||
|
uuid=old_auth_session.uuid,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_redis_sessions(apps, schema_editor):
|
||||||
|
from django.core.cache import caches
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
cache = caches[SESSION_CACHE_ALIAS]
|
||||||
|
|
||||||
|
# Not a redis cache, skipping
|
||||||
|
if not hasattr(cache, "keys"):
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\nMigrating Redis sessions to database, this might take a couple of minutes...")
|
||||||
|
for key, session_data in progress_bar(cache.get_many(cache.keys(f"{KEY_PREFIX}*")).items()):
|
||||||
|
_migrate_session(
|
||||||
|
apps=apps,
|
||||||
|
db_alias=db_alias,
|
||||||
|
session_key=key.removeprefix(KEY_PREFIX),
|
||||||
|
session_data=session_data,
|
||||||
|
expires=now() + timedelta(seconds=cache.ttl(key)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_database_sessions(apps, schema_editor):
|
||||||
|
DjangoSession = apps.get_model("sessions", "Session")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
print("\nMigration database sessions, this might take a couple of minutes...")
|
||||||
|
for django_session in progress_bar(DjangoSession.objects.using(db_alias).all()):
|
||||||
|
session_data = signing.loads(
|
||||||
|
django_session.session_data,
|
||||||
|
salt="django.contrib.sessions.SessionStore",
|
||||||
|
serializer=PickleSerializer,
|
||||||
|
)
|
||||||
|
_migrate_session(
|
||||||
|
apps=apps,
|
||||||
|
db_alias=db_alias,
|
||||||
|
session_key=django_session.session_key,
|
||||||
|
session_data=session_data,
|
||||||
|
expires=django_session.expire_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("sessions", "0001_initial"),
|
||||||
|
("authentik_core", "0045_rename_new_identifier_usersourceconnection_identifier_and_more"),
|
||||||
|
("authentik_providers_oauth2", "0027_accesstoken_authentik_p_expires_9f24a5_idx_and_more"),
|
||||||
|
("authentik_providers_rac", "0006_connectiontoken_authentik_p_expires_91f148_idx_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Rename AuthenticatedSession to OldAuthenticatedSession
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name="AuthenticatedSession",
|
||||||
|
new_name="OldAuthenticatedSession",
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name="oldauthenticatedsession",
|
||||||
|
new_name="authentik_c_expires_cf4f72_idx",
|
||||||
|
old_name="authentik_c_expires_08251d_idx",
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name="oldauthenticatedsession",
|
||||||
|
new_name="authentik_c_expirin_c1f17f_idx",
|
||||||
|
old_name="authentik_c_expirin_9cd839_idx",
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name="oldauthenticatedsession",
|
||||||
|
new_name="authentik_c_expirin_e04f5d_idx",
|
||||||
|
old_name="authentik_c_expirin_195a84_idx",
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name="oldauthenticatedsession",
|
||||||
|
new_name="authentik_c_session_a44819_idx",
|
||||||
|
old_name="authentik_c_session_d0f005_idx",
|
||||||
|
),
|
||||||
|
migrations.RunSQL(
|
||||||
|
sql="ALTER INDEX authentik_core_authenticatedsession_user_id_5055b6cf RENAME TO authentik_core_oldauthenticatedsession_user_id_5055b6cf",
|
||||||
|
reverse_sql="ALTER INDEX authentik_core_oldauthenticatedsession_user_id_5055b6cf RENAME TO authentik_core_authenticatedsession_user_id_5055b6cf",
|
||||||
|
),
|
||||||
|
# Create new Session and AuthenticatedSession models
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Session",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"session_key",
|
||||||
|
models.CharField(
|
||||||
|
max_length=40, primary_key=True, serialize=False, verbose_name="session key"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("expires", models.DateTimeField(default=None, null=True)),
|
||||||
|
("expiring", models.BooleanField(default=True)),
|
||||||
|
("session_data", models.BinaryField(verbose_name="session data")),
|
||||||
|
("last_ip", models.GenericIPAddressField()),
|
||||||
|
("last_user_agent", models.TextField(blank=True)),
|
||||||
|
("last_used", models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"default_permissions": [],
|
||||||
|
"verbose_name": "Session",
|
||||||
|
"verbose_name_plural": "Sessions",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="session",
|
||||||
|
index=models.Index(fields=["expires"], name="authentik_c_expires_d2f607_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="session",
|
||||||
|
index=models.Index(fields=["expiring"], name="authentik_c_expirin_7c2cfb_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="session",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["expiring", "expires"], name="authentik_c_expirin_1ab2e4_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="session",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["expires", "session_key"], name="authentik_c_expires_c49143_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="AuthenticatedSession",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"session",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="authentik_core.session",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("uuid", models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Authenticated Session",
|
||||||
|
"verbose_name_plural": "Authenticated Sessions",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=migrate_redis_sessions,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=migrate_database_sessions,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.11 on 2025-01-27 13:02
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0046_session_and_more"),
|
||||||
|
("authentik_providers_rac", "0007_migrate_session"),
|
||||||
|
("authentik_providers_oauth2", "0028_migrate_session"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="OldAuthenticatedSession",
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,103 @@
|
|||||||
|
# Generated by Django 5.1.9 on 2025-05-14 11:15
|
||||||
|
|
||||||
|
from django.apps.registry import Apps, apps as global_apps
|
||||||
|
from django.db import migrations
|
||||||
|
from django.contrib.contenttypes.management import create_contenttypes
|
||||||
|
from django.contrib.auth.management import create_permissions
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_authenticated_session_permissions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
"""Migrate permissions from OldAuthenticatedSession to AuthenticatedSession"""
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
# `apps` here is just an instance of `django.db.migrations.state.AppConfigStub`, we need the
|
||||||
|
# real config for creating permissions and content types
|
||||||
|
authentik_core_config = global_apps.get_app_config("authentik_core")
|
||||||
|
# These are only ran by django after all migrations, but we need them right now.
|
||||||
|
# `global_apps` is needed,
|
||||||
|
create_permissions(authentik_core_config, using=db_alias, verbosity=1)
|
||||||
|
create_contenttypes(authentik_core_config, using=db_alias, verbosity=1)
|
||||||
|
|
||||||
|
# But from now on, this is just a regular migration, so use `apps`
|
||||||
|
Permission = apps.get_model("auth", "Permission")
|
||||||
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_ct = ContentType.objects.using(db_alias).get(
|
||||||
|
app_label="authentik_core", model="oldauthenticatedsession"
|
||||||
|
)
|
||||||
|
new_ct = ContentType.objects.using(db_alias).get(
|
||||||
|
app_label="authentik_core", model="authenticatedsession"
|
||||||
|
)
|
||||||
|
except ContentType.DoesNotExist:
|
||||||
|
# This should exist at this point, but if not, let's cut our losses
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get all permissions for the old content type
|
||||||
|
old_perms = Permission.objects.using(db_alias).filter(content_type=old_ct)
|
||||||
|
|
||||||
|
# Create equivalent permissions for the new content type
|
||||||
|
for old_perm in old_perms:
|
||||||
|
new_perm = (
|
||||||
|
Permission.objects.using(db_alias)
|
||||||
|
.filter(
|
||||||
|
content_type=new_ct,
|
||||||
|
codename=old_perm.codename,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not new_perm:
|
||||||
|
# This should exist at this point, but if not, let's cut our losses
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Global user permissions
|
||||||
|
User = apps.get_model("authentik_core", "User")
|
||||||
|
User.user_permissions.through.objects.using(db_alias).filter(
|
||||||
|
permission=old_perm
|
||||||
|
).all().update(permission=new_perm)
|
||||||
|
|
||||||
|
# Global role permissions
|
||||||
|
DjangoGroup = apps.get_model("auth", "Group")
|
||||||
|
DjangoGroup.permissions.through.objects.using(db_alias).filter(
|
||||||
|
permission=old_perm
|
||||||
|
).all().update(permission=new_perm)
|
||||||
|
|
||||||
|
# Object user permissions
|
||||||
|
UserObjectPermission = apps.get_model("guardian", "UserObjectPermission")
|
||||||
|
UserObjectPermission.objects.using(db_alias).filter(permission=old_perm).all().update(
|
||||||
|
permission=new_perm, content_type=new_ct
|
||||||
|
)
|
||||||
|
|
||||||
|
# Object role permissions
|
||||||
|
GroupObjectPermission = apps.get_model("guardian", "GroupObjectPermission")
|
||||||
|
GroupObjectPermission.objects.using(db_alias).filter(permission=old_perm).all().update(
|
||||||
|
permission=new_perm, content_type=new_ct
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_old_authenticated_session_content_type(
|
||||||
|
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
||||||
|
):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
|
|
||||||
|
ContentType.objects.using(db_alias).filter(model="oldauthenticatedsession").delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0047_delete_oldauthenticatedsession"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=migrate_authenticated_session_permissions,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=remove_old_authenticated_session_content_type,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
]
|
@ -1,6 +1,7 @@
|
|||||||
"""authentik core models"""
|
"""authentik core models"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from enum import StrEnum
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from typing import Any, Optional, Self
|
from typing import Any, Optional, Self
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@ -9,6 +10,7 @@ from deepmerge import always_merger
|
|||||||
from django.contrib.auth.hashers import check_password
|
from django.contrib.auth.hashers import check_password
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||||
|
from django.contrib.sessions.base_session import AbstractBaseSession
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, QuerySet, options
|
from django.db.models import Q, QuerySet, options
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
@ -204,6 +206,8 @@ class Group(SerializerModel, AttributesMixin):
|
|||||||
permissions = [
|
permissions = [
|
||||||
("add_user_to_group", _("Add user to group")),
|
("add_user_to_group", _("Add user to group")),
|
||||||
("remove_user_from_group", _("Remove user from group")),
|
("remove_user_from_group", _("Remove user from group")),
|
||||||
|
("enable_group_superuser", _("Enable superuser status")),
|
||||||
|
("disable_group_superuser", _("Disable superuser status")),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -314,6 +318,32 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
|||||||
always_merger.merge(final_attributes, self.attributes)
|
always_merger.merge(final_attributes, self.attributes)
|
||||||
return final_attributes
|
return final_attributes
|
||||||
|
|
||||||
|
def app_entitlements(self, app: "Application | None") -> QuerySet["ApplicationEntitlement"]:
|
||||||
|
"""Get all entitlements this user has for `app`."""
|
||||||
|
if not app:
|
||||||
|
return []
|
||||||
|
all_groups = self.all_groups()
|
||||||
|
qs = app.applicationentitlement_set.filter(
|
||||||
|
Q(
|
||||||
|
Q(bindings__user=self) | Q(bindings__group__in=all_groups),
|
||||||
|
bindings__negate=False,
|
||||||
|
)
|
||||||
|
| Q(
|
||||||
|
Q(~Q(bindings__user=self), bindings__user__isnull=False)
|
||||||
|
| Q(~Q(bindings__group__in=all_groups), bindings__group__isnull=False),
|
||||||
|
bindings__negate=True,
|
||||||
|
),
|
||||||
|
bindings__enabled=True,
|
||||||
|
).order_by("name")
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def app_entitlements_attributes(self, app: "Application | None") -> dict:
|
||||||
|
"""Get a dictionary containing all merged attributes from app entitlements for `app`."""
|
||||||
|
final_attributes = {}
|
||||||
|
for attrs in self.app_entitlements(app).values_list("attributes", flat=True):
|
||||||
|
always_merger.merge(final_attributes, attrs)
|
||||||
|
return final_attributes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Serializer:
|
def serializer(self) -> Serializer:
|
||||||
from authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
@ -330,13 +360,13 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
|||||||
"""superuser == staff user"""
|
"""superuser == staff user"""
|
||||||
return self.is_superuser # type: ignore
|
return self.is_superuser # type: ignore
|
||||||
|
|
||||||
def set_password(self, raw_password, signal=True, sender=None):
|
def set_password(self, raw_password, signal=True, sender=None, request=None):
|
||||||
if self.pk and signal:
|
if self.pk and signal:
|
||||||
from authentik.core.signals import password_changed
|
from authentik.core.signals import password_changed
|
||||||
|
|
||||||
if not sender:
|
if not sender:
|
||||||
sender = self
|
sender = self
|
||||||
password_changed.send(sender=sender, user=self, password=raw_password)
|
password_changed.send(sender=sender, user=self, password=raw_password, request=request)
|
||||||
self.password_change_date = now()
|
self.password_change_date = now()
|
||||||
return super().set_password(raw_password)
|
return super().set_password(raw_password)
|
||||||
|
|
||||||
@ -573,6 +603,14 @@ class Application(SerializerModel, PolicyBindingModel):
|
|||||||
return None
|
return None
|
||||||
return candidates[-1]
|
return candidates[-1]
|
||||||
|
|
||||||
|
def backchannel_provider_for[T: Provider](self, provider_type: type[T], **kwargs) -> T | None:
|
||||||
|
"""Get Backchannel provider for a specific type"""
|
||||||
|
providers = self.backchannel_providers.filter(
|
||||||
|
**{f"{provider_type._meta.model_name}__isnull": False},
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
return getattr(providers.first(), provider_type._meta.model_name)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
@ -581,23 +619,59 @@ class Application(SerializerModel, PolicyBindingModel):
|
|||||||
verbose_name_plural = _("Applications")
|
verbose_name_plural = _("Applications")
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationEntitlement(AttributesMixin, SerializerModel, PolicyBindingModel):
|
||||||
|
"""Application-scoped entitlement to control authorization in an application"""
|
||||||
|
|
||||||
|
name = models.TextField()
|
||||||
|
|
||||||
|
app = models.ForeignKey(Application, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Application Entitlement")
|
||||||
|
verbose_name_plural = _("Application Entitlements")
|
||||||
|
unique_together = (("app", "name"),)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Application Entitlement {self.name} for app {self.app_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serializer(self) -> type[Serializer]:
|
||||||
|
from authentik.core.api.application_entitlements import ApplicationEntitlementSerializer
|
||||||
|
|
||||||
|
return ApplicationEntitlementSerializer
|
||||||
|
|
||||||
|
def supported_policy_binding_targets(self):
|
||||||
|
return ["group", "user"]
|
||||||
|
|
||||||
|
|
||||||
class SourceUserMatchingModes(models.TextChoices):
|
class SourceUserMatchingModes(models.TextChoices):
|
||||||
"""Different modes a source can handle new/returning users"""
|
"""Different modes a source can handle new/returning users"""
|
||||||
|
|
||||||
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
||||||
EMAIL_LINK = "email_link", _(
|
EMAIL_LINK = (
|
||||||
"Link to a user with identical email address. Can have security implications "
|
"email_link",
|
||||||
"when a source doesn't validate email addresses."
|
_(
|
||||||
|
"Link to a user with identical email address. Can have security implications "
|
||||||
|
"when a source doesn't validate email addresses."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
EMAIL_DENY = "email_deny", _(
|
EMAIL_DENY = (
|
||||||
"Use the user's email address, but deny enrollment when the email address already exists."
|
"email_deny",
|
||||||
|
_(
|
||||||
|
"Use the user's email address, but deny enrollment when the email address already "
|
||||||
|
"exists."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
USERNAME_LINK = "username_link", _(
|
USERNAME_LINK = (
|
||||||
"Link to a user with identical username. Can have security implications "
|
"username_link",
|
||||||
"when a username is used with another source."
|
_(
|
||||||
|
"Link to a user with identical username. Can have security implications "
|
||||||
|
"when a username is used with another source."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
USERNAME_DENY = "username_deny", _(
|
USERNAME_DENY = (
|
||||||
"Use the user's username, but deny enrollment when the username already exists."
|
"username_deny",
|
||||||
|
_("Use the user's username, but deny enrollment when the username already exists."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -605,18 +679,24 @@ class SourceGroupMatchingModes(models.TextChoices):
|
|||||||
"""Different modes a source can handle new/returning groups"""
|
"""Different modes a source can handle new/returning groups"""
|
||||||
|
|
||||||
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
||||||
NAME_LINK = "name_link", _(
|
NAME_LINK = (
|
||||||
"Link to a group with identical name. Can have security implications "
|
"name_link",
|
||||||
"when a group name is used with another source."
|
_(
|
||||||
|
"Link to a group with identical name. Can have security implications "
|
||||||
|
"when a group name is used with another source."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
NAME_DENY = "name_deny", _(
|
NAME_DENY = (
|
||||||
"Use the group name, but deny enrollment when the name already exists."
|
"name_deny",
|
||||||
|
_("Use the group name, but deny enrollment when the name already exists."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||||
|
|
||||||
|
MANAGED_INBUILT = "goauthentik.io/sources/inbuilt"
|
||||||
|
|
||||||
name = models.TextField(help_text=_("Source's display Name."))
|
name = models.TextField(help_text=_("Source's display Name."))
|
||||||
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
|
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
|
||||||
|
|
||||||
@ -667,8 +747,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
choices=SourceGroupMatchingModes.choices,
|
choices=SourceGroupMatchingModes.choices,
|
||||||
default=SourceGroupMatchingModes.IDENTIFIER,
|
default=SourceGroupMatchingModes.IDENTIFIER,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"How the source determines if an existing group should be used or "
|
"How the source determines if an existing group should be used or a new group created."
|
||||||
"a new group created."
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -698,11 +777,17 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
@property
|
@property
|
||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
"""Return component used to edit this object"""
|
"""Return component used to edit this object"""
|
||||||
|
if self.managed == self.MANAGED_INBUILT:
|
||||||
|
return ""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def property_mapping_type(self) -> "type[PropertyMapping]":
|
def property_mapping_type(self) -> "type[PropertyMapping]":
|
||||||
"""Return property mapping type used by this object"""
|
"""Return property mapping type used by this object"""
|
||||||
|
if self.managed == self.MANAGED_INBUILT:
|
||||||
|
from authentik.core.models import PropertyMapping
|
||||||
|
|
||||||
|
return PropertyMapping
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
||||||
@ -717,10 +802,14 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
|
|
||||||
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||||
"""Get base properties for a user to build final properties upon."""
|
"""Get base properties for a user to build final properties upon."""
|
||||||
|
if self.managed == self.MANAGED_INBUILT:
|
||||||
|
return {}
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||||
"""Get base properties for a group to build final properties upon."""
|
"""Get base properties for a group to build final properties upon."""
|
||||||
|
if self.managed == self.MANAGED_INBUILT:
|
||||||
|
return {}
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -751,6 +840,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
|||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||||||
|
identifier = models.TextField()
|
||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
@ -764,6 +854,10 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (("user", "source"),)
|
unique_together = (("user", "source"),)
|
||||||
|
indexes = (
|
||||||
|
models.Index(fields=("identifier",)),
|
||||||
|
models.Index(fields=("source", "identifier")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
|
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||||
@ -795,6 +889,11 @@ class ExpiringModel(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["expires"]),
|
||||||
|
models.Index(fields=["expiring"]),
|
||||||
|
models.Index(fields=["expiring", "expires"]),
|
||||||
|
]
|
||||||
|
|
||||||
def expire_action(self, *args, **kwargs):
|
def expire_action(self, *args, **kwargs):
|
||||||
"""Handler which is called when this object is expired. By
|
"""Handler which is called when this object is expired. By
|
||||||
@ -850,7 +949,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Token")
|
verbose_name = _("Token")
|
||||||
verbose_name_plural = _("Tokens")
|
verbose_name_plural = _("Tokens")
|
||||||
indexes = [
|
indexes = ExpiringModel.Meta.indexes + [
|
||||||
models.Index(fields=["identifier"]),
|
models.Index(fields=["identifier"]),
|
||||||
models.Index(fields=["key"]),
|
models.Index(fields=["key"]),
|
||||||
]
|
]
|
||||||
@ -929,42 +1028,75 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
|||||||
verbose_name_plural = _("Property Mappings")
|
verbose_name_plural = _("Property Mappings")
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatedSession(ExpiringModel):
|
class Session(ExpiringModel, AbstractBaseSession):
|
||||||
"""Additional session class for authenticated users. Augments the standard django session
|
"""User session with extra fields for fast access"""
|
||||||
to achieve the following:
|
|
||||||
- Make it queryable by user
|
|
||||||
- Have a direct connection to user objects
|
|
||||||
- Allow users to view their own sessions and terminate them
|
|
||||||
- Save structured and well-defined information.
|
|
||||||
"""
|
|
||||||
|
|
||||||
uuid = models.UUIDField(default=uuid4, primary_key=True)
|
# Remove upstream field because we're using our own ExpiringModel
|
||||||
|
expire_date = None
|
||||||
|
session_data = models.BinaryField(_("session data"))
|
||||||
|
|
||||||
session_key = models.CharField(max_length=40)
|
# Keep in sync with Session.Keys
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
last_ip = models.GenericIPAddressField()
|
||||||
|
|
||||||
last_ip = models.TextField()
|
|
||||||
last_user_agent = models.TextField(blank=True)
|
last_user_agent = models.TextField(blank=True)
|
||||||
last_used = models.DateTimeField(auto_now=True)
|
last_used = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Session")
|
||||||
|
verbose_name_plural = _("Sessions")
|
||||||
|
indexes = ExpiringModel.Meta.indexes + [
|
||||||
|
models.Index(fields=["expires", "session_key"]),
|
||||||
|
]
|
||||||
|
default_permissions = []
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.session_key
|
||||||
|
|
||||||
|
class Keys(StrEnum):
|
||||||
|
"""
|
||||||
|
Keys to be set with the session interface for the fields above to be updated.
|
||||||
|
|
||||||
|
If a field is added here that needs to be initialized when the session is initialized,
|
||||||
|
it must also be reflected in authentik.root.middleware.SessionMiddleware.process_request
|
||||||
|
and in authentik.core.sessions.SessionStore.__init__
|
||||||
|
"""
|
||||||
|
|
||||||
|
LAST_IP = "last_ip"
|
||||||
|
LAST_USER_AGENT = "last_user_agent"
|
||||||
|
LAST_USED = "last_used"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_session_store_class(cls):
|
||||||
|
from authentik.core.sessions import SessionStore
|
||||||
|
|
||||||
|
return SessionStore
|
||||||
|
|
||||||
|
def get_decoded(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticatedSession(SerializerModel):
|
||||||
|
session = models.OneToOneField(Session, on_delete=models.CASCADE, primary_key=True)
|
||||||
|
# We use the session as primary key, but we need the API to be able to reference
|
||||||
|
# this object uniquely without exposing the session key
|
||||||
|
uuid = models.UUIDField(default=uuid4, unique=True)
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Authenticated Session")
|
verbose_name = _("Authenticated Session")
|
||||||
verbose_name_plural = _("Authenticated Sessions")
|
verbose_name_plural = _("Authenticated Sessions")
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Authenticated Session {self.session_key[:10]}"
|
return f"Authenticated Session {str(self.pk)[:10]}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_request(request: HttpRequest, user: User) -> Optional["AuthenticatedSession"]:
|
def from_request(request: HttpRequest, user: User) -> Optional["AuthenticatedSession"]:
|
||||||
"""Create a new session from a http request"""
|
"""Create a new session from a http request"""
|
||||||
from authentik.root.middleware import ClientIPMiddleware
|
if not hasattr(request, "session") or not request.session.exists(
|
||||||
|
request.session.session_key
|
||||||
if not hasattr(request, "session") or not request.session.session_key:
|
):
|
||||||
return None
|
return None
|
||||||
return AuthenticatedSession(
|
return AuthenticatedSession(
|
||||||
session_key=request.session.session_key,
|
session=Session.objects.filter(session_key=request.session.session_key).first(),
|
||||||
user=user,
|
user=user,
|
||||||
last_ip=ClientIPMiddleware.get_client_ip(request),
|
|
||||||
last_user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
|
||||||
expires=request.session.get_expiry_date(),
|
|
||||||
)
|
)
|
||||||
|
168
authentik/core/sessions.py
Normal file
168
authentik/core/sessions.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
"""authentik sessions engine"""
|
||||||
|
|
||||||
|
import pickle # nosec
|
||||||
|
|
||||||
|
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
|
||||||
|
from django.contrib.sessions.backends.db import SessionStore as SessionBase
|
||||||
|
from django.core.exceptions import SuspiciousOperation
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.root.middleware import ClientIPMiddleware
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class SessionStore(SessionBase):
|
||||||
|
def __init__(self, session_key=None, last_ip=None, last_user_agent=""):
|
||||||
|
super().__init__(session_key)
|
||||||
|
self._create_kwargs = {
|
||||||
|
"last_ip": last_ip or ClientIPMiddleware.default_ip,
|
||||||
|
"last_user_agent": last_user_agent,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_model_class(cls):
|
||||||
|
from authentik.core.models import Session
|
||||||
|
|
||||||
|
return Session
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def model_fields(self):
|
||||||
|
return [k.value for k in self.model.Keys]
|
||||||
|
|
||||||
|
def _get_session_from_db(self):
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
self.model.objects.select_related(
|
||||||
|
"authenticatedsession",
|
||||||
|
"authenticatedsession__user",
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
"authenticatedsession__user__groups",
|
||||||
|
"authenticatedsession__user__user_permissions",
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
session_key=self.session_key,
|
||||||
|
expires__gt=timezone.now(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except (self.model.DoesNotExist, SuspiciousOperation) as exc:
|
||||||
|
if isinstance(exc, SuspiciousOperation):
|
||||||
|
LOGGER.warning(str(exc))
|
||||||
|
self._session_key = None
|
||||||
|
|
||||||
|
async def _aget_session_from_db(self):
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
await self.model.objects.select_related(
|
||||||
|
"authenticatedsession",
|
||||||
|
"authenticatedsession__user",
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
"authenticatedsession__user__groups",
|
||||||
|
"authenticatedsession__user__user_permissions",
|
||||||
|
)
|
||||||
|
.aget(
|
||||||
|
session_key=self.session_key,
|
||||||
|
expires__gt=timezone.now(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except (self.model.DoesNotExist, SuspiciousOperation) as exc:
|
||||||
|
if isinstance(exc, SuspiciousOperation):
|
||||||
|
LOGGER.warning(str(exc))
|
||||||
|
self._session_key = None
|
||||||
|
|
||||||
|
def encode(self, session_dict):
|
||||||
|
return pickle.dumps(session_dict, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
|
|
||||||
|
def decode(self, session_data):
|
||||||
|
try:
|
||||||
|
return pickle.loads(session_data) # nosec
|
||||||
|
except pickle.PickleError:
|
||||||
|
# ValueError, unpickling exceptions. If any of these happen, just return an empty
|
||||||
|
# dictionary (an empty session)
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
s = self._get_session_from_db()
|
||||||
|
if s:
|
||||||
|
return {
|
||||||
|
"authenticatedsession": getattr(s, "authenticatedsession", None),
|
||||||
|
**{k: getattr(s, k) for k in self.model_fields},
|
||||||
|
**self.decode(s.session_data),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def aload(self):
|
||||||
|
s = await self._aget_session_from_db()
|
||||||
|
if s:
|
||||||
|
return {
|
||||||
|
"authenticatedsession": getattr(s, "authenticatedsession", None),
|
||||||
|
**{k: getattr(s, k) for k in self.model_fields},
|
||||||
|
**self.decode(s.session_data),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def create_model_instance(self, data):
|
||||||
|
args = {
|
||||||
|
"session_key": self._get_or_create_session_key(),
|
||||||
|
"expires": self.get_expiry_date(),
|
||||||
|
"session_data": {},
|
||||||
|
**self._create_kwargs,
|
||||||
|
}
|
||||||
|
for k, v in data.items():
|
||||||
|
# Don't save:
|
||||||
|
# - unused auth data
|
||||||
|
# - related models
|
||||||
|
if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
|
||||||
|
pass
|
||||||
|
elif k in self.model_fields:
|
||||||
|
args[k] = v
|
||||||
|
else:
|
||||||
|
args["session_data"][k] = v
|
||||||
|
args["session_data"] = self.encode(args["session_data"])
|
||||||
|
return self.model(**args)
|
||||||
|
|
||||||
|
async def acreate_model_instance(self, data):
|
||||||
|
args = {
|
||||||
|
"session_key": await self._aget_or_create_session_key(),
|
||||||
|
"expires": await self.aget_expiry_date(),
|
||||||
|
"session_data": {},
|
||||||
|
**self._create_kwargs,
|
||||||
|
}
|
||||||
|
for k, v in data.items():
|
||||||
|
# Don't save:
|
||||||
|
# - unused auth data
|
||||||
|
# - related models
|
||||||
|
if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
|
||||||
|
pass
|
||||||
|
elif k in self.model_fields:
|
||||||
|
args[k] = v
|
||||||
|
else:
|
||||||
|
args["session_data"][k] = v
|
||||||
|
args["session_data"] = self.encode(args["session_data"])
|
||||||
|
return self.model(**args)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear_expired(cls):
|
||||||
|
cls.get_model_class().objects.filter(expires__lt=timezone.now()).delete()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def aclear_expired(cls):
|
||||||
|
await cls.get_model_class().objects.filter(expires__lt=timezone.now()).adelete()
|
||||||
|
|
||||||
|
def cycle_key(self):
|
||||||
|
data = self._session
|
||||||
|
key = self.session_key
|
||||||
|
self.create()
|
||||||
|
self._session_cache = data
|
||||||
|
if key:
|
||||||
|
self.delete(key)
|
||||||
|
if (authenticated_session := data.get("authenticatedsession")) is not None:
|
||||||
|
authenticated_session.session_id = self.session_key
|
||||||
|
authenticated_session.save(force_insert=True)
|
@ -1,11 +1,10 @@
|
|||||||
"""authentik core signals"""
|
"""authentik core signals"""
|
||||||
|
|
||||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
from django.contrib.auth.signals import user_logged_in
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.signals import Signal
|
from django.core.signals import Signal
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save, pre_delete, pre_save
|
from django.db.models.signals import post_delete, post_save, pre_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
@ -15,6 +14,7 @@ from authentik.core.models import (
|
|||||||
AuthenticatedSession,
|
AuthenticatedSession,
|
||||||
BackchannelProvider,
|
BackchannelProvider,
|
||||||
ExpiringModel,
|
ExpiringModel,
|
||||||
|
Session,
|
||||||
User,
|
User,
|
||||||
default_token_duration,
|
default_token_duration,
|
||||||
)
|
)
|
||||||
@ -49,19 +49,10 @@ def user_logged_in_session(sender, request: HttpRequest, user: User, **_):
|
|||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
@receiver(post_delete, sender=AuthenticatedSession)
|
||||||
def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
|
||||||
"""Delete AuthenticatedSession if it exists"""
|
|
||||||
if not request.session or not request.session.session_key:
|
|
||||||
return
|
|
||||||
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
|
||||||
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"""
|
"""Delete session when authenticated session is deleted"""
|
||||||
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
Session.objects.filter(session_key=instance.pk).delete()
|
||||||
cache.delete(cache_key)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save)
|
@receiver(pre_save)
|
||||||
|
@ -35,8 +35,7 @@ from authentik.flows.planner import (
|
|||||||
FlowPlanner,
|
FlowPlanner,
|
||||||
)
|
)
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
||||||
from authentik.lib.utils.urls import redirect_with_qs
|
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
from authentik.policies.utils import delete_none_values
|
from authentik.policies.utils import delete_none_values
|
||||||
@ -47,8 +46,10 @@ from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH
|
|||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
|
||||||
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
||||||
|
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
|
||||||
|
SESSION_KEY_SOURCE_FLOW_CONTEXT = "authentik/flows/source_flow_context"
|
||||||
|
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
||||||
|
|
||||||
|
|
||||||
class MessageStage(StageView):
|
class MessageStage(StageView):
|
||||||
@ -219,34 +220,28 @@ class SourceFlowManager:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
flow_context.update(self.policy_context)
|
flow_context.update(self.policy_context)
|
||||||
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect)
|
||||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
|
||||||
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
|
||||||
plan = token.plan
|
|
||||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
|
||||||
plan.context.update(flow_context)
|
|
||||||
for stage in self.get_stages_to_append(flow):
|
|
||||||
plan.append_stage(stage)
|
|
||||||
if stages:
|
|
||||||
for stage in stages:
|
|
||||||
plan.append_stage(stage)
|
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
|
||||||
flow_slug = token.flow.slug
|
|
||||||
token.delete()
|
|
||||||
return redirect_with_qs(
|
|
||||||
"authentik_core:if-flow",
|
|
||||||
self.request.GET,
|
|
||||||
flow_slug=flow_slug,
|
|
||||||
)
|
|
||||||
# Ensure redirect is carried through when user was trying to
|
|
||||||
# authorize application
|
|
||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
|
||||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
|
||||||
)
|
|
||||||
if PLAN_CONTEXT_REDIRECT not in flow_context:
|
|
||||||
flow_context[PLAN_CONTEXT_REDIRECT] = final_redirect
|
|
||||||
|
|
||||||
if not flow:
|
if not flow:
|
||||||
|
# We only check for the flow token here if we don't have a flow, otherwise we rely on
|
||||||
|
# SESSION_KEY_SOURCE_FLOW_STAGES to delegate the usage of this token and dynamically add
|
||||||
|
# stages that deal with this token to return to another flow
|
||||||
|
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||||
|
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||||
|
self._logger.info(
|
||||||
|
"Replacing source flow with overridden flow", flow=token.flow.slug
|
||||||
|
)
|
||||||
|
plan = token.plan
|
||||||
|
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||||
|
plan.context.update(flow_context)
|
||||||
|
for stage in self.get_stages_to_append(flow):
|
||||||
|
plan.append_stage(stage)
|
||||||
|
if stages:
|
||||||
|
for stage in stages:
|
||||||
|
plan.append_stage(stage)
|
||||||
|
redirect = plan.to_redirect(self.request, token.flow)
|
||||||
|
token.delete()
|
||||||
|
return redirect
|
||||||
return bad_request_message(
|
return bad_request_message(
|
||||||
self.request,
|
self.request,
|
||||||
_("Configured flow does not exist."),
|
_("Configured flow does not exist."),
|
||||||
@ -265,6 +260,9 @@ class SourceFlowManager:
|
|||||||
if stages:
|
if stages:
|
||||||
for stage in stages:
|
for stage in stages:
|
||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
|
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
|
||||||
|
plan.append_stage(stage)
|
||||||
|
plan.context.update(self.request.session.get(SESSION_KEY_SOURCE_FLOW_CONTEXT, {}))
|
||||||
return plan.to_redirect(self.request, flow)
|
return plan.to_redirect(self.request, flow)
|
||||||
|
|
||||||
def handle_auth(
|
def handle_auth(
|
||||||
@ -301,6 +299,8 @@ class SourceFlowManager:
|
|||||||
# When request isn't authenticated we jump straight to auth
|
# When request isn't authenticated we jump straight to auth
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return self.handle_auth(connection)
|
return self.handle_auth(connection)
|
||||||
|
# When an override flow token exists we actually still use a flow for link
|
||||||
|
# to continue the existing flow we came from
|
||||||
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||||
return self._prepare_flow(None, connection)
|
return self._prepare_flow(None, connection)
|
||||||
connection.save()
|
connection.save()
|
||||||
|
@ -2,22 +2,16 @@
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.conf import ImproperlyConfigured
|
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
|
||||||
from django.contrib.sessions.backends.db import SessionStore as DBSessionStore
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
USER_ATTRIBUTE_EXPIRES,
|
USER_ATTRIBUTE_EXPIRES,
|
||||||
USER_ATTRIBUTE_GENERATED,
|
USER_ATTRIBUTE_GENERATED,
|
||||||
AuthenticatedSession,
|
|
||||||
ExpiringModel,
|
ExpiringModel,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
|
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -38,38 +32,6 @@ def clean_expired_models(self: SystemTask):
|
|||||||
obj.expire_action()
|
obj.expire_action()
|
||||||
LOGGER.debug("Expired models", model=cls, amount=amount)
|
LOGGER.debug("Expired models", model=cls, amount=amount)
|
||||||
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
|
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
|
||||||
# Special case
|
|
||||||
amount = 0
|
|
||||||
|
|
||||||
for session in AuthenticatedSession.objects.all():
|
|
||||||
match CONFIG.get("session_storage", "cache"):
|
|
||||||
case "cache":
|
|
||||||
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
|
||||||
value = None
|
|
||||||
try:
|
|
||||||
value = cache.get(cache_key)
|
|
||||||
|
|
||||||
except Exception as exc:
|
|
||||||
LOGGER.debug("Failed to get session from cache", exc=exc)
|
|
||||||
if not value:
|
|
||||||
session.delete()
|
|
||||||
amount += 1
|
|
||||||
case "db":
|
|
||||||
if not (
|
|
||||||
DBSessionStore.get_model_class()
|
|
||||||
.objects.filter(session_key=session.session_key, expire_date__gt=now())
|
|
||||||
.exists()
|
|
||||||
):
|
|
||||||
session.delete()
|
|
||||||
amount += 1
|
|
||||||
case _:
|
|
||||||
# Should never happen, as we check for other values in authentik/root/settings.py
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
"Invalid session_storage setting, allowed values are db and cache"
|
|
||||||
)
|
|
||||||
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
|
||||||
|
|
||||||
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
|
||||||
self.set_status(TaskStatus.SUCCESSFUL, *messages)
|
self.set_status(TaskStatus.SUCCESSFUL, *messages)
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
build: "{{ build }}",
|
build: "{{ build }}",
|
||||||
api: {
|
api: {
|
||||||
base: "{{ base_url }}",
|
base: "{{ base_url }}",
|
||||||
|
relBase: "{{ base_url_rel }}",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
window.addEventListener("DOMContentLoaded", function () {
|
window.addEventListener("DOMContentLoaded", function () {
|
||||||
|
@ -8,18 +8,22 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
{# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #}
|
||||||
|
<meta name="darkreader-lock">
|
||||||
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
||||||
<link rel="icon" href="{{ brand.branding_favicon_url }}">
|
<link rel="icon" href="{{ brand.branding_favicon_url }}">
|
||||||
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
|
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
<style>{{ brand.branding_custom_css }}</style>
|
||||||
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||||
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
{% for key, value in html_meta.items %}
|
||||||
|
<meta name="{{key}}" content="{{ value }}" />
|
||||||
|
{% endfor %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
<link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" />
|
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
||||||
{% include "base/header_js.html" %}
|
{% include "base/header_js.html" %}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}");
|
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
|
||||||
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
||||||
--pf-c-background-image--BackgroundImage-2x: 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: var(--ak-flow-background);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user