Compare commits
162 Commits
version-20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
7da90ff7e4 | |||
61b5714652 | |||
d2df426489 | |||
e6c75ed173 | |||
a353c6956e | |||
a367d8515f | |||
2b7a22a29a | |||
e6712a50d2 | |||
c621f62d92 | |||
a0648cd925 | |||
2650e672bb | |||
53b9376789 | |||
d15e50025c | |||
0af66a26ab | |||
bf754369d9 | |||
02dc112f8f | |||
2d4e7ebab5 | |||
a7d0a50859 | |||
71c9108f89 | |||
f8bcdb26b3 | |||
45f1d95bf9 | |||
5dab198c47 | |||
ad91abe9de | |||
fa30755241 | |||
552f8c6a9a | |||
101f916247 | |||
2acdcf74e1 | |||
ddb8610032 | |||
22ad850e6c | |||
57925ed60a | |||
48cc2f17c1 | |||
448108fca0 | |||
c1254f6212 | |||
c8120c0d3e | |||
52016e0806 | |||
e555bdd42b | |||
1a619c90de | |||
18faf30b0c | |||
b3bd979ecd | |||
db113c5e8f | |||
78bcb90a1e | |||
b64ecbde22 | |||
43bab840ec | |||
f020b79384 | |||
820f658b49 | |||
5d460a2537 | |||
efc46f52e6 | |||
9fac51f8c7 | |||
fe4b2d1a34 | |||
f8abe3e210 | |||
3ced67b151 | |||
cd5631ec76 | |||
95df7c7f30 | |||
1e934aa5d5 | |||
d93927755a | |||
ddb3b71dce | |||
bf9826873e | |||
6869b3c16a | |||
9b71b8da5f | |||
bfc8e9200f | |||
c4311abc9f | |||
ec42869e00 | |||
45963c2ffc | |||
1aa27b5e80 | |||
1737feec91 | |||
a0e0fb930a | |||
4a32c3ca11 | |||
d307539fd0 | |||
c060a3eec2 | |||
4612ae1ff4 | |||
7af883d80c | |||
4a5374d03f | |||
3b536f6e55 | |||
6aa13a8666 | |||
24e4924dec | |||
a252f303c0 | |||
33cdbd7776 | |||
18bc54214d | |||
db7e9f9b95 | |||
a885247d36 | |||
91282c7bd8 | |||
830b8bcd5b | |||
0f5e6d0d8c | |||
6aa6615608 | |||
91d6a3c8c7 | |||
a6ac82c492 | |||
05d777c373 | |||
32cf960053 | |||
83bf639926 | |||
2717742bd2 | |||
ef70e93bbd | |||
478d3430eb | |||
9c1ade59e9 | |||
fadf746234 | |||
397dfc29f1 | |||
b0e3b8b39d | |||
df9ae796d4 | |||
dfdad5388f | |||
c38ea69bdd | |||
dca6f43858 | |||
51cbb7cc8e | |||
1f8130e685 | |||
580d59e921 | |||
e639d8ab56 | |||
9f478bb46a | |||
7a16f97908 | |||
dd8c1eeb52 | |||
005b4d8dda | |||
de2d8b2d85 | |||
7d107991a2 | |||
14dc420747 | |||
89dc4db30b | |||
cc3fccb27e | |||
add20de8de | |||
7e2a471903 | |||
9ca9e67ffa | |||
178417fe67 | |||
53f002a123 | |||
c7c387eb38 | |||
1b3760a4b7 | |||
704a502089 | |||
3b12ef80eb | |||
1101810fea | |||
1ab5289e2e | |||
ac24fc9ce3 | |||
4b24b185f2 | |||
ea0ba5ae30 | |||
44686de74e | |||
b74c08620a | |||
e25d03d8f4 | |||
f8f26d2a23 | |||
1f2e177e3e | |||
cfed41439e | |||
3ac148d01c | |||
3e696d6ac8 | |||
0114bc0d6a | |||
c60934f9b1 | |||
09bdcfaab0 | |||
624206281e | |||
4d7e64c48c | |||
3d112e7688 | |||
3c4ff65a01 | |||
d7f54ce5d5 | |||
bc55c97fa2 | |||
d9a907e39e | |||
8616647045 | |||
4d861e2830 | |||
881730f52e | |||
e78577d470 | |||
d502f4d77d | |||
3c5f7deba9 | |||
b61334c482 | |||
eb762632d0 | |||
6a882249aa | |||
94f6bbd431 | |||
3926ee9eb6 | |||
7fbf915e0a | |||
5af9e8c05d | |||
7c0c453d9f | |||
d8ae56ed19 | |||
a9a65ceca6 | |||
c11fd884b8 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.1.4-stable
|
||||
current_version = 2021.2.3-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
|
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@ -16,6 +16,14 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: npm
|
||||
directory: "/website"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/authentik:2021.1.4-stable
|
||||
-t beryju/authentik:2021.2.3-stable
|
||||
-t beryju/authentik:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik:2021.1.4-stable
|
||||
run: docker push beryju/authentik:2021.2.3-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik:latest
|
||||
build-proxy:
|
||||
@ -48,11 +48,11 @@ jobs:
|
||||
cd outpost/
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/authentik-proxy:2021.1.4-stable \
|
||||
-t beryju/authentik-proxy:2021.2.3-stable \
|
||||
-t beryju/authentik-proxy:latest \
|
||||
-f proxy.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik-proxy:2021.1.4-stable
|
||||
run: docker push beryju/authentik-proxy:2021.2.3-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik-proxy:latest
|
||||
build-static:
|
||||
@ -69,11 +69,11 @@ jobs:
|
||||
cd web/
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/authentik-static:2021.1.4-stable \
|
||||
-t beryju/authentik-static:2021.2.3-stable \
|
||||
-t beryju/authentik-static:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik-static:2021.1.4-stable
|
||||
run: docker push beryju/authentik-static:2021.2.3-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik-static:latest
|
||||
test-release:
|
||||
@ -107,5 +107,5 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 2021.1.4-stable
|
||||
tagName: 2021.2.3-stable
|
||||
environment: beryjuorg-prod
|
||||
|
257
Pipfile.lock
generated
257
Pipfile.lock
generated
@ -53,10 +53,10 @@
|
||||
},
|
||||
"autobahn": {
|
||||
"hashes": [
|
||||
"sha256:410a93e0e29882c8b5d5ab05d220b07609b886ef5f23c0b8d39153254ffd6895",
|
||||
"sha256:52ee4236ff9a1fcbbd9500439dcf3284284b37f8a6b31ecc8a36e00cf9f95049"
|
||||
"sha256:93df8fc9d1821c9dabff9fed52181a9ad6eea5e9989d53102c391607d7c1666e",
|
||||
"sha256:cceed2121b7a93024daa93c91fae33007f8346f0e522796421f36a6183abea99"
|
||||
],
|
||||
"version": "==20.12.3"
|
||||
"version": "==21.1.1"
|
||||
},
|
||||
"automat": {
|
||||
"hashes": [
|
||||
@ -74,18 +74,17 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:a280123db79e73478bd23933486f3a0ffa2397d1a6381f32573f2731ff48c59a",
|
||||
"sha256:bb91fecf982e1bbfb68bb6bd2c9a0cce3c84ac6f97dd338d1ef9e47780679091"
|
||||
"sha256:d6aafb804fca2b67c65dda78ad8b4afed901e004071208b84c804d345ad9ebba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.16.62"
|
||||
"version": "==1.17.5"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:1046c152e5865aabbe6b10b2d33e652b3dd072516f3976e96cacc6b7c4460d02",
|
||||
"sha256:29b4b9be5b40f392a033926c08c004c01bd6471384ef6f12eaa49ee3870a010c"
|
||||
"sha256:04a1df759681f5f171accb354d863bfed0774d64a4e8ee35ff49835755660a4e",
|
||||
"sha256:3c55f0db5e08920727f4fa24a87aed60060643f4b0b5665c62ec762f79e82d6b"
|
||||
],
|
||||
"version": "==1.19.62"
|
||||
"version": "==1.20.5"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -127,6 +126,7 @@
|
||||
"sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009",
|
||||
"sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03",
|
||||
"sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b",
|
||||
"sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e",
|
||||
"sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909",
|
||||
"sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53",
|
||||
"sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35",
|
||||
@ -223,22 +223,15 @@
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d",
|
||||
"sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7",
|
||||
"sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901",
|
||||
"sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c",
|
||||
"sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244",
|
||||
"sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6",
|
||||
"sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5",
|
||||
"sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e",
|
||||
"sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c",
|
||||
"sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0",
|
||||
"sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812",
|
||||
"sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a",
|
||||
"sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030",
|
||||
"sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302"
|
||||
"sha256:287032b6a7d86abc98e8e977b20138c53fea40e5b24e29090d5a675a973dcd10",
|
||||
"sha256:288c65eea20bd89b11102c47b118bc1e0749386b0a0dfebba414076c5d4c8188",
|
||||
"sha256:7eed937ad9b53280a5f53570d3a7dc93cb4412b6a3d58d4c6bb78cc26319c729",
|
||||
"sha256:dab437c2e84628703e3358f0f06555a6259bc5039209d51aa3b05af667ff4fd0",
|
||||
"sha256:ee5e19f0856b6fbbdbab15c2787ca65d203801d2d65d0b8de6218f424206c848",
|
||||
"sha256:f21be9ec6b44c223b2024bbe59d394fadc7be320d18a8d595419afadb6cd5620",
|
||||
"sha256:f6ea140d2736b7e1f0de4f988c43f76b0b3f3d365080e091715429ba218dce28"
|
||||
],
|
||||
"version": "==3.3.1"
|
||||
"version": "==3.4.4"
|
||||
},
|
||||
"dacite": {
|
||||
"hashes": [
|
||||
@ -265,11 +258,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7",
|
||||
"sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"
|
||||
"sha256:169e2e7b4839a7910b393eec127fd7cbae62e80fa55f89c6510426abf673fe5f",
|
||||
"sha256:c6c0462b8b361f8691171af1fb87eceb4442da28477e12200c40420176206ba7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.5"
|
||||
"version": "==3.1.6"
|
||||
},
|
||||
"django-cors-middleware": {
|
||||
"hashes": [
|
||||
@ -397,10 +390,10 @@
|
||||
},
|
||||
"google-auth": {
|
||||
"hashes": [
|
||||
"sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e",
|
||||
"sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8"
|
||||
"sha256:008e23ed080674f69f9d2d7d80db4c2591b9bb307d136cea7b3bc129771d211d",
|
||||
"sha256:514e39f4190ca972200ba33876da5a8857c5665f2b4ccc36c8b8ee21228aae80"
|
||||
],
|
||||
"version": "==1.24.0"
|
||||
"version": "==1.25.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@ -522,10 +515,10 @@
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
|
||||
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
|
||||
],
|
||||
"version": "==2.11.2"
|
||||
"version": "==2.11.3"
|
||||
},
|
||||
"jmespath": {
|
||||
"hashes": [
|
||||
@ -614,8 +607,12 @@
|
||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
|
||||
"sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
|
||||
"sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
|
||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||
"sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
|
||||
"sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
|
||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||
@ -624,24 +621,39 @@
|
||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
|
||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||
"sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
|
||||
"sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
|
||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||
"sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
|
||||
"sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
|
||||
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||
"sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
|
||||
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||
"sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
|
||||
"sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
|
||||
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||
"sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
|
||||
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||
"sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
|
||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||
"sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
|
||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||
"sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
|
||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
|
||||
"sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
|
||||
"sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
|
||||
"sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
|
||||
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
@ -687,11 +699,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
|
||||
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
|
||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.8"
|
||||
"version": "==20.9"
|
||||
},
|
||||
"prometheus-client": {
|
||||
"hashes": [
|
||||
@ -771,84 +783,74 @@
|
||||
},
|
||||
"pycryptodome": {
|
||||
"hashes": [
|
||||
"sha256:19cb674df6c74a14b8b408aa30ba8a89bd1c01e23505100fb45f930fbf0ed0d9",
|
||||
"sha256:1cfdb92dca388e27e732caa72a1cc624520fe93752a665c3b6cd8f1a91b34916",
|
||||
"sha256:27397aee992af69d07502126561d851ba3845aa808f0e55c71ad0efa264dd7d4",
|
||||
"sha256:28f75e58d02019a7edc7d4135203d2501dfc47256d175c72c9798f9a129a49a7",
|
||||
"sha256:2a68df525b387201a43b27b879ce8c08948a430e883a756d6c9e3acdaa7d7bd8",
|
||||
"sha256:411745c6dce4eff918906eebcde78771d44795d747e194462abb120d2e537cd9",
|
||||
"sha256:46e96aeb8a9ca8b1edf9b1fd0af4bf6afcf3f1ca7fa35529f5d60b98f3e4e959",
|
||||
"sha256:4ed27951b0a17afd287299e2206a339b5b6d12de9321e1a1575261ef9c4a851b",
|
||||
"sha256:50826b49fbca348a61529693b0031cdb782c39060fb9dca5ac5dff858159dc5a",
|
||||
"sha256:5598dc6c9dbfe882904e54584322893eff185b98960bbe2cdaaa20e8a437b6e5",
|
||||
"sha256:5c3c4865730dfb0263f822b966d6d58429d8b1e560d1ddae37685fd9e7c63161",
|
||||
"sha256:5f19e6ef750f677d924d9c7141f54bade3cd56695bbfd8a9ef15d0378557dfe4",
|
||||
"sha256:60febcf5baf70c566d9d9351c47fbd8321da9a4edf2eff45c4c31c86164ca794",
|
||||
"sha256:62c488a21c253dadc9f731a32f0ac61e4e436d81a1ea6f7d1d9146ed4d20d6bd",
|
||||
"sha256:6d3baaf82681cfb1a842f1c8f77beac791ceedd99af911e4f5fabec32bae2259",
|
||||
"sha256:6e4227849e4231a3f5b35ea5bdedf9a82b3883500e5624f00a19156e9a9ef861",
|
||||
"sha256:6e89bb3826e6f84501e8e3b205c22595d0c5492c2f271cbb9ee1c48eb1866645",
|
||||
"sha256:70d807d11d508433daf96244ec1c64e55039e8a35931fc5ea9eee94dbe3cb6b5",
|
||||
"sha256:76b1a34d74bb2c91bce460cdc74d1347592045627a955e9a252554481c17c52f",
|
||||
"sha256:7798e73225a699651888489fbb1dbc565e03a509942a8ce6194bbe6fb582a41f",
|
||||
"sha256:834b790bbb6bd18956f625af4004d9c15eed12d5186d8e57851454ae76d52215",
|
||||
"sha256:843e5f10ecdf9d307032b8b91afe9da1d6ed5bb89d0bbec5c8dcb4ba44008e11",
|
||||
"sha256:8f9f84059039b672a5a705b3c5aa21747867bacc30a72e28bf0d147cc8ef85ed",
|
||||
"sha256:9000877383e2189dafd1b2fc68c6c726eca9a3cfb6d68148fbb72ccf651959b6",
|
||||
"sha256:910e202a557e1131b1c1b3f17a63914d57aac55cf9fb9b51644962841c3995c4",
|
||||
"sha256:946399d15eccebafc8ce0257fc4caffe383c75e6b0633509bd011e357368306c",
|
||||
"sha256:a199e9ca46fc6e999e5f47fce342af4b56c7de85fae893c69ab6aa17531fb1e1",
|
||||
"sha256:a3d8a9efa213be8232c59cdc6b65600276508e375e0a119d710826248fd18d37",
|
||||
"sha256:a4599c0ca0fc027c780c1c45ed996d5bef03e571470b7b1c7171ec1e1a90914c",
|
||||
"sha256:b4e6b269a8ddaede774e5c3adbef6bf452ee144e6db8a716d23694953348cd86",
|
||||
"sha256:b68794fba45bdb367eeb71249c26d23e61167510a1d0c3d6cf0f2f14636e62ee",
|
||||
"sha256:d7ec2bd8f57c559dd24e71891c51c25266a8deb66fc5f02cc97c7fb593d1780a",
|
||||
"sha256:e15bde67ccb7d4417f627dd16ffe2f5a4c2941ce5278444e884cb26d73ecbc61",
|
||||
"sha256:eb01f9997e4d6a8ec8a1ad1f676ba5a362781ff64e8189fe2985258ba9cb9706",
|
||||
"sha256:faa682c404c218e8788c3126c9a4b8fbcc54dc245b5b6e8ea5b46f3b63bd0c84"
|
||||
"sha256:09c1555a3fa450e7eaca41ea11cd00afe7c91fef52353488e65663777d8524e0",
|
||||
"sha256:12222a5edc9ca4a29de15fbd5339099c4c26c56e13c2ceddf0b920794f26165d",
|
||||
"sha256:1723ebee5561628ce96748501cdaa7afaa67329d753933296321f0be55358dce",
|
||||
"sha256:1c5e1ca507de2ad93474be5cfe2bfa76b7cf039a1a32fc196f40935944871a06",
|
||||
"sha256:2603c98ae04aac675fefcf71a6c87dc4bb74a75e9071ae3923bbc91a59f08d35",
|
||||
"sha256:2dea65df54349cdfa43d6b2e8edb83f5f8d6861e5cf7b1fbc3e34c5694c85e27",
|
||||
"sha256:31c1df17b3dc5f39600a4057d7db53ac372f492c955b9b75dd439f5d8b460129",
|
||||
"sha256:38661348ecb71476037f1e1f553159b80d256c00f6c0b00502acac891f7116d9",
|
||||
"sha256:3e2e3a06580c5f190df843cdb90ea28d61099cf4924334d5297a995de68e4673",
|
||||
"sha256:3f840c49d38986f6e17dbc0673d37947c88bc9d2d9dba1c01b979b36f8447db1",
|
||||
"sha256:501ab36aae360e31d0ec370cf5ce8ace6cb4112060d099b993bc02b36ac83fb6",
|
||||
"sha256:60386d1d4cfaad299803b45a5bc2089696eaf6cdd56f9fc17479a6f89595cfc8",
|
||||
"sha256:6260e24d41149268122dd39d4ebd5941e9d107f49463f7e071fd397e29923b0c",
|
||||
"sha256:6bbf7fee7b7948b29d7e71fcacf48bac0c57fb41332007061a933f2d996f9713",
|
||||
"sha256:6d2df5223b12437e644ce0a3be7809471ffa71de44ccd28b02180401982594a6",
|
||||
"sha256:758949ca62690b1540dfb24ad773c6da9cd0e425189e83e39c038bbd52b8e438",
|
||||
"sha256:77997519d8eb8a4adcd9a47b9cec18f9b323e296986528186c0e9a7a15d6a07e",
|
||||
"sha256:7fd519b89585abf57bf47d90166903ec7b43af4fe23c92273ea09e6336af5c07",
|
||||
"sha256:98213ac2b18dc1969a47bc65a79a8fca02a414249d0c8635abb081c7f38c91b6",
|
||||
"sha256:99b2f3fc51d308286071d0953f92055504a6ffe829a832a9fc7a04318a7683dd",
|
||||
"sha256:9b6f711b25e01931f1c61ce0115245a23cdc8b80bf8539ac0363bdcf27d649b6",
|
||||
"sha256:a3105a0eb63eacf98c2ecb0eb4aa03f77f40fbac2bdde22020bb8a536b226bb8",
|
||||
"sha256:a8eb8b6ea09ec1c2535bf39914377bc8abcab2c7d30fa9225eb4fe412024e427",
|
||||
"sha256:a92d5c414e8ee1249e850789052608f582416e82422502dc0ac8c577808a9067",
|
||||
"sha256:d3d6958d53ad307df5e8469cc44474a75393a434addf20ecd451f38a72fe29b8",
|
||||
"sha256:e0a4d5933a88a2c98bbe19c0c722f5483dc628d7a38338ac2cb64a7dbd34064b",
|
||||
"sha256:e3bf558c6aeb49afa9f0c06cee7fb5947ee5a1ff3bd794b653d39926b49077fa",
|
||||
"sha256:e61e363d9a5d7916f3a4ce984a929514c0df3daf3b1b2eb5e6edbb131ee771cf",
|
||||
"sha256:f977cdf725b20f6b8229b0c87acb98c7717e742ef9f46b113985303ae12a99da",
|
||||
"sha256:fc7489a50323a0df02378bc2fff86eb69d94cc5639914346c736be981c6a02e7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.9.9"
|
||||
"version": "==3.10.1"
|
||||
},
|
||||
"pycryptodomex": {
|
||||
"hashes": [
|
||||
"sha256:15c03ffdac17731b126880622823d30d0a3cc7203cd219e6b9814140a44e7fab",
|
||||
"sha256:20fb7f4efc494016eab1bc2f555bc0a12dd5ca61f35c95df8061818ffb2c20a3",
|
||||
"sha256:28ee3bcb4d609aea3040cad995a8e2c9c6dc57c12183dadd69e53880c35333b9",
|
||||
"sha256:305e3c46f20d019cd57543c255e7ba49e432e275d7c0de8913b6dbe57a851bc8",
|
||||
"sha256:3547b87b16aad6afb28c9b3a9cd870e11b5e7b5ac649b74265258d96d8de1130",
|
||||
"sha256:3642252d7bfc4403a42050e18ba748bedebd5a998a8cba89665a4f42aea4c380",
|
||||
"sha256:404faa3e518f8bea516aae2aac47d4d960397199a15b4bd6f66cad97825469a0",
|
||||
"sha256:42669638e4f7937b7141044a2fbd1019caca62bd2cdd8b535f731426ab07bde1",
|
||||
"sha256:4632d55a140b28e20be3cd7a3057af52fb747298ff0fd3290d4e9f245b5004ba",
|
||||
"sha256:4a88c9383d273bdce3afc216020282c9c5c39ec0bd9462b1a206af6afa377cf0",
|
||||
"sha256:4ce1fc1e6d2fd2d6dc197607153327989a128c093e0e94dca63408f506622c3e",
|
||||
"sha256:55cf4e99b3ba0122dee570dc7661b97bf35c16aab3e2ccb5070709d282a1c7ab",
|
||||
"sha256:5e486cab2dfcfaec934dd4f5d5837f4a9428b690f4d92a3b020fd31d1497ca64",
|
||||
"sha256:65ec88c8271448d2ea109d35c1f297b09b872c57214ab7e832e413090d3469a9",
|
||||
"sha256:6c95a3361ce70068cf69526a58751f73ddac5ba27a3c2379b057efa2f5338c8c",
|
||||
"sha256:73240335f4a1baf12880ebac6df66ab4d3a9212db9f3efe809c36a27280d16f8",
|
||||
"sha256:7651211e15109ac0058a49159265d9f6e6423c8a81c65434d3c56d708417a05b",
|
||||
"sha256:7b5b7c5896f8172ea0beb283f7f9428e0ab88ec248ce0a5b8c98d73e26267d51",
|
||||
"sha256:836fe39282e75311ce4c38468be148f7fac0df3d461c5de58c5ff1ddb8966bac",
|
||||
"sha256:871852044f55295449fbf225538c2c4118525093c32f0a6c43c91bed0452d7e3",
|
||||
"sha256:892e93f3e7e10c751d6c17fa0dc422f7984cfd5eb6690011f9264dc73e2775fc",
|
||||
"sha256:934e460c5058346c6f1d62fdf3db5680fbdfbfd212722d24d8277bf47cd9ebdc",
|
||||
"sha256:9736f3f3e1761024200637a080a4f922f5298ad5d780e10dbb5634fe8c65b34c",
|
||||
"sha256:a1d38a96da57e6103423a446079ead600b450cf0f8ebf56a231895abf77e7ffc",
|
||||
"sha256:a385fceaa0cdb97f0098f1c1e9ec0b46cc09186ddf60ec23538e871b1dddb6dc",
|
||||
"sha256:a7cf1c14e47027d9fb9d26aa62e5d603994227bd635e58a8df4b1d2d1b6a8ed7",
|
||||
"sha256:a9aac1a30b00b5038d3d8e48248f3b58ea15c827b67325c0d18a447552e30fc8",
|
||||
"sha256:b696876ee583d15310be57311e90e153a84b7913ac93e6b99675c0c9867926d0",
|
||||
"sha256:bef9e9d39393dc7baec39ba4bac6c73826a4db02114cdeade2552a9d6afa16e2",
|
||||
"sha256:c885fe4d5f26ce8ca20c97d02e88f5fdd92c01e1cc771ad0951b21e1641faf6d",
|
||||
"sha256:d2d1388595cb5d27d9220d5cbaff4f37c6ec696a25882eb06d224d241e6e93fb",
|
||||
"sha256:d2e853e0f9535e693fade97768cf7293f3febabecc5feb1e9b2ffdfe1044ab96",
|
||||
"sha256:d62fbab185a6b01c5469eda9f0795f3d1a5bba24f5a5813f362e4b73a3c4dc70",
|
||||
"sha256:f20a62397e09704049ce9007bea4f6bad965ba9336a760c6f4ef1b4192e12d6d",
|
||||
"sha256:f81f7311250d9480e36dec819127897ae772e7e8de07abfabe931b8566770b8e"
|
||||
"sha256:00a584ee52bf5e27d540129ca9bf7c4a7e7447f24ff4a220faa1304ad0c09bcd",
|
||||
"sha256:04265a7a84ae002001249bd1de2823bcf46832bd4b58f6965567cb8a07cf4f00",
|
||||
"sha256:0bd35af6a18b724c689e56f2dbbdd8e409288be71952d271ba3d9614b31d188c",
|
||||
"sha256:20c45a30f3389148f94edb77f3b216c677a277942f62a2b81a1cc0b6b2dde7fc",
|
||||
"sha256:2959304d1ce31ab303d9fb5db2b294814278b35154d9b30bf7facc52d6088d0a",
|
||||
"sha256:36dab7f506948056ceba2d57c1ade74e898401960de697cefc02f3519bd26c1b",
|
||||
"sha256:37ec1b407ec032c7a0c1fdd2da12813f560bad38ae61ad9c7ce3c0573b3e5e30",
|
||||
"sha256:3b8eb85b3cc7f083d87978c264d10ff9de3b4bfc46f1c6fdc2792e7d7ebc87bb",
|
||||
"sha256:3dfce70c4e425607ae87b8eae67c9c7dbba59a33b62d70f79417aef0bc5c735b",
|
||||
"sha256:418f51c61eab52d9920f4ef468d22c89dab1be5ac796f71cf3802f6a6e667df0",
|
||||
"sha256:4195604f75cdc1db9bccdb9e44d783add3c817319c30aaff011670c9ed167690",
|
||||
"sha256:4344ab16faf6c2d9df2b6772995623698fb2d5f114dace4ab2ff335550cf71d5",
|
||||
"sha256:541cd3e3e252fb19a7b48f420b798b53483302b7fe4d9954c947605d0a263d62",
|
||||
"sha256:564063e3782474c92cbb333effd06e6eb718471783c6e67f28c63f0fc3ac7b23",
|
||||
"sha256:72f44b5be46faef2a1bf2a85902511b31f4dd7b01ce0c3978e92edb2cc812a82",
|
||||
"sha256:8a98e02cbf8f624add45deff444539bf26345b479fc04fa0937b23cd84078d91",
|
||||
"sha256:940db96449d7b2ebb2c7bf190be1514f3d67914bd37e54e8d30a182bd375a1a9",
|
||||
"sha256:961333e7ee896651f02d4692242aa36b787b8e8e0baa2256717b2b9d55ae0a3c",
|
||||
"sha256:9f713ffb4e27b5575bd917c70bbc3f7b348241a351015dbbc514c01b7061ff7e",
|
||||
"sha256:a6584ae58001d17bb4dc0faa8a426919c2c028ef4d90ceb4191802ca6edb8204",
|
||||
"sha256:c2b680987f418858e89dbb4f09c8c919ece62811780a27051ace72b2f69fb1be",
|
||||
"sha256:d8fae5ba3d34c868ae43614e0bd6fb61114b2687ac3255798791ce075d95aece",
|
||||
"sha256:dbd2c361db939a4252589baa94da4404d45e3fc70da1a31e541644cdf354336e",
|
||||
"sha256:e090a8609e2095aa86978559b140cf8968af99ee54b8791b29ff804838f29f10",
|
||||
"sha256:e4a1245e7b846e88ba63e7543483bda61b9acbaee61eadbead5a1ce479d94740",
|
||||
"sha256:ec9901d19cadb80d9235ee41cc58983f18660314a0eb3fc7b11b0522ac3b6c4a",
|
||||
"sha256:f2abeb4c4ce7584912f4d637b2c57f23720d35dd2892bfeb1b2c84b6fb7a8c88",
|
||||
"sha256:f3bb267df679f70a9f40f17d62d22fe12e8b75e490f41807e7560de4d3e6bf9f",
|
||||
"sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34",
|
||||
"sha256:fc9c55dc1ed57db76595f2d19a479fc1c3a1be2c9da8de798a93d286c5f65f38"
|
||||
],
|
||||
"version": "==3.9.9"
|
||||
"version": "==3.10.1"
|
||||
},
|
||||
"pyhamcrest": {
|
||||
"hashes": [
|
||||
@ -900,10 +902,10 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
|
||||
"sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
|
||||
"sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
|
||||
"sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
|
||||
],
|
||||
"version": "==2020.5"
|
||||
"version": "==2021.1"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
@ -1084,7 +1086,6 @@
|
||||
"sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.26.3"
|
||||
},
|
||||
"uvicorn": {
|
||||
@ -1275,10 +1276,11 @@
|
||||
},
|
||||
"autopep8": {
|
||||
"hashes": [
|
||||
"sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094"
|
||||
"sha256:9e136c472c475f4ee4978b51a88a494bfcd4e3ed17950a44a988d9e434837bea",
|
||||
"sha256:cae4bc0fb616408191af41d062d7ec7ef8679c7f27b068875ca3a9e2878d5443"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.5.4"
|
||||
"version": "==1.5.5"
|
||||
},
|
||||
"bandit": {
|
||||
"hashes": [
|
||||
@ -1382,11 +1384,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7",
|
||||
"sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"
|
||||
"sha256:169e2e7b4839a7910b393eec127fd7cbae62e80fa55f89c6510426abf673fe5f",
|
||||
"sha256:c6c0462b8b361f8691171af1fb87eceb4442da28477e12200c40420176206ba7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.5"
|
||||
"version": "==3.1.6"
|
||||
},
|
||||
"django-debug-toolbar": {
|
||||
"hashes": [
|
||||
@ -1426,10 +1428,10 @@
|
||||
},
|
||||
"gitpython": {
|
||||
"hashes": [
|
||||
"sha256:42dbefd8d9e2576c496ed0059f3103dcef7125b9ce16f9d5f9c834aed44a1dac",
|
||||
"sha256:867ec3dfb126aac0f8296b19fb63b8c4a399f32b4b6fafe84c4b10af5fa9f7b5"
|
||||
"sha256:8621a7e777e276a5ec838b59280ba5272dd144a18169c36c903d8b38b99f750a",
|
||||
"sha256:c5347c81d232d9b8e7f47b68a83e5dc92e7952127133c5f2df9133f2c75a1b29"
|
||||
],
|
||||
"version": "==3.1.12"
|
||||
"version": "==3.1.13"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
@ -1487,11 +1489,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
|
||||
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
|
||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.8"
|
||||
"version": "==20.9"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
@ -1616,10 +1618,10 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
|
||||
"sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
|
||||
"sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
|
||||
"sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
|
||||
],
|
||||
"version": "==2020.5"
|
||||
"version": "==2021.1"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
@ -1808,7 +1810,6 @@
|
||||
"sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.26.3"
|
||||
},
|
||||
"wrapt": {
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.1.4-stable"
|
||||
__version__ = "2021.2.3-stable"
|
||||
|
@ -1,19 +0,0 @@
|
||||
"""authentik core source form fields"""
|
||||
|
||||
SOURCE_FORM_FIELDS = [
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
]
|
||||
SOURCE_SERIALIZER_FIELDS = [
|
||||
"pk",
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
@ -1,5 +1,8 @@
|
||||
"""authentik admin tasks"""
|
||||
import re
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.core.validators import URLValidator
|
||||
from packaging.version import parse
|
||||
from requests import RequestException, get
|
||||
from structlog.stdlib import get_logger
|
||||
@ -11,7 +14,9 @@ from authentik.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
VERSION_CACHE_KEY = "authentik_latest_version"
|
||||
VERSION_CACHE_TIMEOUT = 2 * 60 * 60 # 2 hours
|
||||
VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours
|
||||
# Chop of the first ^ because we want to search the entire string
|
||||
URL_FINDER = URLValidator.regex.pattern[1:]
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@ -39,7 +44,10 @@ def update_latest_version(self: MonitoredTask):
|
||||
context__new_version=upstream_version,
|
||||
).exists():
|
||||
return
|
||||
Event.new(EventAction.UPDATE_AVAILABLE, new_version=upstream_version).save()
|
||||
event_dict = {"new_version": upstream_version}
|
||||
if match := re.search(URL_FINDER, data.get("body", "")):
|
||||
event_dict["message"] = f"Changelog: {match.group()}"
|
||||
Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save()
|
||||
except (RequestException, IndexError) as exc:
|
||||
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
|
@ -0,0 +1,14 @@
|
||||
{% extends base_template|default:"generic/form.html" %}
|
||||
|
||||
{% load authentik_utils %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block above_form %}
|
||||
<h1>
|
||||
{% trans 'Generate Certificate-Key Pair' %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
{% trans 'Generate Certificate-Key Pair' %}
|
||||
{% endblock %}
|
@ -26,6 +26,12 @@
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="{% url 'authentik_admin:certificatekeypair-generate' %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
{% trans 'Generate' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<button role="ak-refresh" class="pf-c-button pf-m-primary">
|
||||
{% trans 'Refresh' %}
|
||||
</button>
|
||||
|
@ -1,149 +0,0 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load authentik_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="pf-icon pf-icon-zone"></i>
|
||||
{% trans 'Outposts' %}
|
||||
</h1>
|
||||
<p>{% trans "Outposts are deployments of authentik components to support different environments and protocols, like reverse proxies." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<ak-modal-button href="{% url 'authentik_admin:outpost-create' %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
{% trans 'Create' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<button role="ak-refresh" class="pf-c-button pf-m-primary">
|
||||
{% trans 'Refresh' %}
|
||||
</button>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Providers' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Health' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Version' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for outpost in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<span>{{ outpost.name }}</span>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ outpost.providers.all.select_subclasses|join:", " }}
|
||||
</span>
|
||||
</td>
|
||||
{% with states=outpost.state %}
|
||||
{% if states|length > 0 %}
|
||||
<td role="cell">
|
||||
{% for state in states %}
|
||||
<div>
|
||||
{% if state.last_seen %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ state.last_seen|naturaltime }}
|
||||
{% else %}
|
||||
<i class="fas fa-times pf-m-danger"></i> {% trans 'Unhealthy' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td role="cell">
|
||||
{% for state in states %}
|
||||
<div>
|
||||
{% if not state.version %}
|
||||
<i class="fas fa-question-circle"></i>
|
||||
{% elif state.version_outdated %}
|
||||
<i class="fas fa-times pf-m-danger"></i> {% blocktrans with is=state.version should=state.version_should %}{{ is }}, should be {{ should }}{% endblocktrans %}
|
||||
{% else %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ state.version }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td role="cell">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<td>
|
||||
<ak-modal-button href="{% url 'authentik_admin:outpost-update' pk=outpost.pk %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
{% trans 'Edit' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="{% url 'authentik_admin:outpost-delete' pk=outpost.pk %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
{% trans 'Delete' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
{% get_htmls outpost as htmls %}
|
||||
{% for html in htmls %}
|
||||
{{ html|safe }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-map-marker pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Outposts.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any outposts." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no outposts exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<ak-modal-button href="{% url 'authentik_admin:outpost-create' %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
{% trans 'Create' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
@ -3,7 +3,6 @@
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load authentik_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
|
@ -3,7 +3,42 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block above_form %}
|
||||
<h1>{% blocktrans with policy=policy %}Test policy {{ policy }}{% endblocktrans %}</h1>
|
||||
<h1>{% blocktrans with policy=policy %}Test {{ policy }}{% endblocktrans %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block beneath_form %}
|
||||
{% if result %}
|
||||
<div class="pf-c-form__group ">
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label" for="context-1">
|
||||
<span class="pf-c-form__label-text">{% trans 'Passing' %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-label">
|
||||
<div class="c-form__horizontal-group">
|
||||
<span class="pf-c-form__label-text">{{ result.passing|yesno:"Yes,No" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__group ">
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label" for="context-1">
|
||||
<span class="pf-c-form__label-text">{% trans 'Messages' %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-label">
|
||||
<div class="c-form__horizontal-group">
|
||||
<ul>
|
||||
{% for m in result.messages %}
|
||||
<li><span class="pf-c-form__label-text">{{ m }}</span></li>
|
||||
{% empty %}
|
||||
<li><span class="pf-c-form__label-text">-</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
|
@ -1,139 +0,0 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="pf-icon pf-icon-blueprint"></i>
|
||||
{% trans 'Property Mappings' %}
|
||||
</h1>
|
||||
<p>{% trans "Control how authentik exposes and interprets information." %}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<ak-dropdown class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<ak-modal-button href="{% url 'authentik_admin:property-mapping-create' %}?type={{ type }}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</ak-dropdown>
|
||||
<button role="ak-refresh" class="pf-c-button pf-m-primary">
|
||||
{% trans 'Refresh' %}
|
||||
</button>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for property_mapping in object_list %}
|
||||
<tr role="row">
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ property_mapping.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ property_mapping|verbose_name }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<ak-modal-button href="{% url 'authentik_admin:property-mapping-update' pk=property_mapping.pk %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
{% trans 'Edit' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="{% url 'authentik_admin:property-mapping-delete' pk=property_mapping.pk %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
{% trans 'Delete' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="pf-icon pf-icon-blueprint pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Property Mappings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any property mappings." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no property mappings exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<ak-dropdown class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<ak-modal-button href="{% url 'authentik_admin:property-mapping-create' %}?type={{ type }}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</ak-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
@ -0,0 +1,28 @@
|
||||
{% extends 'generic/form.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block above_form %}
|
||||
<h1>{% blocktrans with property_mapping=property_mapping %}Test {{ property_mapping }}{% endblocktrans %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block beneath_form %}
|
||||
{% if result %}
|
||||
<div class="pf-c-form__group ">
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label" for="context-1">
|
||||
<span class="pf-c-form__label-text">{% trans 'Result' %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="c-form__horizontal-group">
|
||||
<ak-codemirror mode="javascript"><textarea class="pf-c-form-control">{{ result }}</textarea></ak-codemirror>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
{% trans 'Test' %}
|
||||
{% endblock %}
|
@ -1,181 +0,0 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="pf-icon pf-icon-integration"></i>
|
||||
{% trans 'Providers' %}
|
||||
</h1>
|
||||
<p>{% trans "Provide support for protocols like SAML and OAuth to assigned applications." %}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<ak-dropdown class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<ak-modal-button href="{% url 'authentik_admin:provider-create' %}?type={{ type }}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
<ak-modal-button href="{% url 'authentik_admin:provider-saml-from-metadata' %}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">
|
||||
{% trans 'SAML Provider from Metadata' %}<br>
|
||||
<small>
|
||||
{% trans "Create a SAML Provider by importing its Metadata." %}
|
||||
</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>
|
||||
</ul>
|
||||
</ak-dropdown>
|
||||
<button role="ak-refresh" class="pf-c-button pf-m-primary">
|
||||
{% trans 'Refresh' %}
|
||||
</button>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for provider in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ provider.name }}</div>
|
||||
{% if not provider.application %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i>
|
||||
<small>{% trans 'Warning: Provider not assigned to any application.' %}</small>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i>
|
||||
<small>
|
||||
{% blocktrans with app=provider.application %}
|
||||
Assigned to application {{ app }}.
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ provider|verbose_name }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<ak-modal-button href="{% url 'authentik_admin:provider-update' pk=provider.pk %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
{% trans 'Edit' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="{% url 'authentik_admin:provider-delete' pk=provider.pk %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
{% trans 'Delete' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
{% get_links provider as links %}
|
||||
{% for name, href in links.items %}
|
||||
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
|
||||
{% endfor %}
|
||||
{% get_htmls provider as htmls %}
|
||||
{% for html in htmls %}
|
||||
{{ html|safe }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="pf-icon-integration pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Providers.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any providers." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no providers exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<ak-dropdown class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<ak-modal-button href="{% url 'authentik_admin:provider-create' %}?type={{ type }}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
<ak-modal-button href="{% url 'authentik_admin:provider-saml-from-metadata' %}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">
|
||||
{% trans 'SAML Provider from Metadata' %}<br>
|
||||
<small>
|
||||
{% trans "Create a SAML Provider by importing its Metadata." %}
|
||||
</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>
|
||||
</ul>
|
||||
</ak-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
@ -2,7 +2,6 @@
|
||||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
@ -63,7 +62,7 @@
|
||||
{% for source in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<a href="/sources/{{ source.slug }}/">
|
||||
<a href="/sources/{{ source.slug }}">
|
||||
<div>{{ source.name }}</div>
|
||||
{% if not source.enabled %}
|
||||
<small>{% trans 'Disabled' %}</small>
|
||||
@ -93,10 +92,6 @@
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
{% get_links source as links %}
|
||||
{% for name, href in links %}
|
||||
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
@ -88,10 +87,6 @@
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
{% get_links stage as links %}
|
||||
{% for name, href in links.items %}
|
||||
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
@ -90,10 +89,6 @@
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
{% get_links prompt as links %}
|
||||
{% for name, href in links.items %}
|
||||
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -1,62 +0,0 @@
|
||||
"""authentik admin templatetags"""
|
||||
from django import template
|
||||
from django.db.models import Model
|
||||
from django.utils.html import mark_safe
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
register = template.Library()
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def get_links(model_instance):
|
||||
"""Find all link_ methods on an object instance, run them and return as dict"""
|
||||
prefix = "link_"
|
||||
links = {}
|
||||
|
||||
if not isinstance(model_instance, Model):
|
||||
LOGGER.warning("Model is not instance of Model", model_instance=model_instance)
|
||||
return links
|
||||
|
||||
try:
|
||||
for name in dir(model_instance):
|
||||
if not name.startswith(prefix):
|
||||
continue
|
||||
value = getattr(model_instance, name)
|
||||
if not callable(value):
|
||||
continue
|
||||
human_name = name.replace(prefix, "").replace("_", " ").capitalize()
|
||||
link = value()
|
||||
if link:
|
||||
links[human_name] = link
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
return links
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def get_htmls(context, model_instance):
|
||||
"""Find all html_ methods on an object instance, run them and return as dict"""
|
||||
prefix = "html_"
|
||||
htmls = []
|
||||
|
||||
if not isinstance(model_instance, Model):
|
||||
LOGGER.warning("Model is not instance of Model", model_instance=model_instance)
|
||||
return htmls
|
||||
|
||||
try:
|
||||
for name in dir(model_instance):
|
||||
if not name.startswith(prefix):
|
||||
continue
|
||||
value = getattr(model_instance, name)
|
||||
if not callable(value):
|
||||
continue
|
||||
if name.startswith(prefix):
|
||||
html = value(context.get("request"))
|
||||
if html:
|
||||
htmls.append(mark_safe(html))
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
return htmls
|
@ -32,7 +32,8 @@ REQUEST_MOCK_VALID = Mock(
|
||||
return_value=MockResponse(
|
||||
200,
|
||||
"""{
|
||||
"tag_name": "version/99999999.9999999"
|
||||
"tag_name": "version/99999999.9999999",
|
||||
"body": "https://goauthentik.io/test"
|
||||
}""",
|
||||
)
|
||||
)
|
||||
@ -52,6 +53,7 @@ class TestAdminTasks(TestCase):
|
||||
Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE,
|
||||
context__new_version="99999999.9999999",
|
||||
context__message="Changelog: https://goauthentik.io/test",
|
||||
).exists()
|
||||
)
|
||||
# test that a consecutive check doesn't create a duplicate event
|
||||
@ -61,6 +63,7 @@ class TestAdminTasks(TestCase):
|
||||
Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE,
|
||||
context__new_version="99999999.9999999",
|
||||
context__message="Changelog: https://goauthentik.io/test",
|
||||
)
|
||||
),
|
||||
1,
|
||||
|
@ -24,7 +24,7 @@ from authentik.admin.views import (
|
||||
tokens,
|
||||
users,
|
||||
)
|
||||
from authentik.providers.saml.views import MetadataImportView
|
||||
from authentik.providers.saml.views.metadata import MetadataImportView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
@ -61,7 +61,6 @@ urlpatterns = [
|
||||
name="token-delete",
|
||||
),
|
||||
# Sources
|
||||
path("sources/", sources.SourceListView.as_view(), name="sources"),
|
||||
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
|
||||
path(
|
||||
"sources/<uuid:pk>/update/",
|
||||
@ -113,7 +112,6 @@ urlpatterns = [
|
||||
name="policy-binding-delete",
|
||||
),
|
||||
# Providers
|
||||
path("providers/", providers.ProviderListView.as_view(), name="providers"),
|
||||
path(
|
||||
"providers/create/",
|
||||
providers.ProviderCreateView.as_view(),
|
||||
@ -170,22 +168,22 @@ urlpatterns = [
|
||||
),
|
||||
# Stage Prompts
|
||||
path(
|
||||
"stages/prompts/",
|
||||
"stages_prompts/",
|
||||
stages_prompts.PromptListView.as_view(),
|
||||
name="stage-prompts",
|
||||
),
|
||||
path(
|
||||
"stages/prompts/create/",
|
||||
"stages_prompts/create/",
|
||||
stages_prompts.PromptCreateView.as_view(),
|
||||
name="stage-prompt-create",
|
||||
),
|
||||
path(
|
||||
"stages/prompts/<uuid:pk>/update/",
|
||||
"stages_prompts/<uuid:pk>/update/",
|
||||
stages_prompts.PromptUpdateView.as_view(),
|
||||
name="stage-prompt-update",
|
||||
),
|
||||
path(
|
||||
"stages/prompts/<uuid:pk>/delete/",
|
||||
"stages_prompts/<uuid:pk>/delete/",
|
||||
stages_prompts.PromptDeleteView.as_view(),
|
||||
name="stage-prompt-delete",
|
||||
),
|
||||
@ -238,11 +236,6 @@ urlpatterns = [
|
||||
name="flow-delete",
|
||||
),
|
||||
# Property Mappings
|
||||
path(
|
||||
"property-mappings/",
|
||||
property_mappings.PropertyMappingListView.as_view(),
|
||||
name="property-mappings",
|
||||
),
|
||||
path(
|
||||
"property-mappings/create/",
|
||||
property_mappings.PropertyMappingCreateView.as_view(),
|
||||
@ -258,6 +251,11 @@ urlpatterns = [
|
||||
property_mappings.PropertyMappingDeleteView.as_view(),
|
||||
name="property-mapping-delete",
|
||||
),
|
||||
path(
|
||||
"property-mappings/<uuid:pk>/test/",
|
||||
property_mappings.PropertyMappingTestView.as_view(),
|
||||
name="property-mapping-test",
|
||||
),
|
||||
# Users
|
||||
path("users/", users.UserListView.as_view(), name="users"),
|
||||
path("users/create/", users.UserCreateView.as_view(), name="user-create"),
|
||||
@ -296,6 +294,11 @@ urlpatterns = [
|
||||
certificate_key_pair.CertificateKeyPairCreateView.as_view(),
|
||||
name="certificatekeypair-create",
|
||||
),
|
||||
path(
|
||||
"crypto/certificates/generate/",
|
||||
certificate_key_pair.CertificateKeyPairGenerateView.as_view(),
|
||||
name="certificatekeypair-generate",
|
||||
),
|
||||
path(
|
||||
"crypto/certificates/<uuid:pk>/update/",
|
||||
certificate_key_pair.CertificateKeyPairUpdateView.as_view(),
|
||||
@ -307,11 +310,6 @@ urlpatterns = [
|
||||
name="certificatekeypair-delete",
|
||||
),
|
||||
# Outposts
|
||||
path(
|
||||
"outposts/",
|
||||
outposts.OutpostListView.as_view(),
|
||||
name="outposts",
|
||||
),
|
||||
path(
|
||||
"outposts/create/",
|
||||
outposts.OutpostCreateView.as_view(),
|
||||
@ -329,22 +327,22 @@ urlpatterns = [
|
||||
),
|
||||
# Outpost Service Connections
|
||||
path(
|
||||
"outposts/service_connections/",
|
||||
"outpost_service_connections/",
|
||||
outposts_service_connections.OutpostServiceConnectionListView.as_view(),
|
||||
name="outpost-service-connections",
|
||||
),
|
||||
path(
|
||||
"outposts/service_connections/create/",
|
||||
"outpost_service_connections/create/",
|
||||
outposts_service_connections.OutpostServiceConnectionCreateView.as_view(),
|
||||
name="outpost-service-connection-create",
|
||||
),
|
||||
path(
|
||||
"outposts/service_connections/<uuid:pk>/update/",
|
||||
"outpost_service_connections/<uuid:pk>/update/",
|
||||
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
|
||||
name="outpost-service-connection-update",
|
||||
),
|
||||
path(
|
||||
"outposts/service_connections/<uuid:pk>/delete/",
|
||||
"outpost_service_connections/<uuid:pk>/delete/",
|
||||
outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(),
|
||||
name="outpost-service-connection-delete",
|
||||
),
|
||||
|
@ -4,7 +4,6 @@ from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import UpdateView
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
@ -29,7 +28,6 @@ class ApplicationCreateView(
|
||||
permission_required = "authentik_core.add_application"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully created Application")
|
||||
|
||||
|
||||
@ -47,7 +45,6 @@ class ApplicationUpdateView(
|
||||
permission_required = "authentik_core.change_application"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully updated Application")
|
||||
|
||||
|
||||
@ -60,5 +57,4 @@ class ApplicationDeleteView(
|
||||
permission_required = "authentik_core.delete_application"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully deleted Application")
|
||||
|
@ -4,9 +4,11 @@ from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http.response import HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from django.views.generic.edit import FormView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from authentik.admin.views.utils import (
|
||||
@ -15,7 +17,11 @@ from authentik.admin.views.utils import (
|
||||
SearchListMixin,
|
||||
UserPaginateListMixin,
|
||||
)
|
||||
from authentik.crypto.forms import CertificateKeyPairForm
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.forms import (
|
||||
CertificateKeyPairForm,
|
||||
CertificateKeyPairGenerateForm,
|
||||
)
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.views import CreateAssignPermView
|
||||
|
||||
@ -52,7 +58,35 @@ class CertificateKeyPairCreateView(
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("authentik_admin:certificate_key_pair")
|
||||
success_message = _("Successfully created CertificateKeyPair")
|
||||
success_message = _("Successfully created Certificate-Key Pair")
|
||||
|
||||
|
||||
class CertificateKeyPairGenerateView(
|
||||
SuccessMessageMixin,
|
||||
BackSuccessUrlMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
FormView,
|
||||
):
|
||||
"""Generate new CertificateKeyPair"""
|
||||
|
||||
model = CertificateKeyPair
|
||||
form_class = CertificateKeyPairGenerateForm
|
||||
permission_required = "authentik_crypto.add_certificatekeypair"
|
||||
|
||||
template_name = "administration/certificatekeypair/generate.html"
|
||||
success_url = reverse_lazy("authentik_admin:certificate_key_pair")
|
||||
success_message = _("Successfully generated Certificate-Key Pair")
|
||||
|
||||
def form_valid(self, form: CertificateKeyPairGenerateForm) -> HttpResponse:
|
||||
builder = CertificateBuilder()
|
||||
builder.common_name = form.data["common_name"]
|
||||
builder.build(
|
||||
subject_alt_names=form.data.get("subject_alt_name", "").split(","),
|
||||
validity_days=int(form.data["validity_days"]),
|
||||
)
|
||||
builder.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class CertificateKeyPairUpdateView(
|
||||
|
@ -4,7 +4,6 @@ from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import UpdateView
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
@ -29,7 +28,6 @@ class NotificationRuleCreateView(
|
||||
permission_required = "authentik_events.add_NotificationRule"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully created Notification Rule")
|
||||
|
||||
|
||||
@ -47,7 +45,6 @@ class NotificationRuleUpdateView(
|
||||
permission_required = "authentik_events.change_NotificationRule"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully updated Notification Rule")
|
||||
|
||||
|
||||
@ -60,5 +57,4 @@ class NotificationRuleDeleteView(
|
||||
permission_required = "authentik_events.delete_NotificationRule"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully deleted Notification Rule")
|
||||
|
@ -4,7 +4,6 @@ from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import UpdateView
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
@ -29,7 +28,6 @@ class NotificationTransportCreateView(
|
||||
permission_required = "authentik_events.add_notificationtransport"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully created Notification Transport")
|
||||
|
||||
|
||||
@ -47,7 +45,6 @@ class NotificationTransportUpdateView(
|
||||
permission_required = "authentik_events.change_notificationtransport"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully updated Notification Transport")
|
||||
|
||||
|
||||
@ -60,5 +57,4 @@ class NotificationTransportDeleteView(
|
||||
permission_required = "authentik_events.delete_notificationtransport"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully deleted Notification Transport")
|
||||
|
@ -7,38 +7,16 @@ from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from django.views.generic import UpdateView
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
|
||||
from authentik.admin.views.utils import (
|
||||
BackSuccessUrlMixin,
|
||||
DeleteMessageView,
|
||||
SearchListMixin,
|
||||
UserPaginateListMixin,
|
||||
)
|
||||
from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
|
||||
from authentik.lib.views import CreateAssignPermView
|
||||
from authentik.outposts.forms import OutpostForm
|
||||
from authentik.outposts.models import Outpost, OutpostConfig
|
||||
|
||||
|
||||
class OutpostListView(
|
||||
LoginRequiredMixin,
|
||||
PermissionListMixin,
|
||||
UserPaginateListMixin,
|
||||
SearchListMixin,
|
||||
ListView,
|
||||
):
|
||||
"""Show list of all outposts"""
|
||||
|
||||
model = Outpost
|
||||
permission_required = "authentik_outposts.view_outpost"
|
||||
ordering = "name"
|
||||
template_name = "administration/outpost/list.html"
|
||||
search_fields = ["name", "_config"]
|
||||
|
||||
|
||||
class OutpostCreateView(
|
||||
SuccessMessageMixin,
|
||||
BackSuccessUrlMixin,
|
||||
@ -53,7 +31,6 @@ class OutpostCreateView(
|
||||
permission_required = "authentik_outposts.add_outpost"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("authentik_admin:outposts")
|
||||
success_message = _("Successfully created Outpost")
|
||||
|
||||
def get_initial(self) -> Dict[str, Any]:
|
||||
@ -78,7 +55,6 @@ class OutpostUpdateView(
|
||||
permission_required = "authentik_outposts.change_outpost"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("authentik_admin:outposts")
|
||||
success_message = _("Successfully updated Outpost")
|
||||
|
||||
|
||||
@ -89,5 +65,4 @@ class OutpostDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessa
|
||||
permission_required = "authentik_outposts.delete_outpost"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("authentik_admin:outposts")
|
||||
success_message = _("Successfully deleted Outpost")
|
||||
|
@ -20,7 +20,6 @@ class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
||||
form_class = PolicyCacheClearForm
|
||||
|
||||
template_name = "generic/form_non_model.html"
|
||||
success_url = "/"
|
||||
success_message = _("Successfully cleared Policy cache")
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
@ -28,7 +27,7 @@ class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
||||
cache.delete_many(keys)
|
||||
LOGGER.debug("Cleared Policy cache", keys=len(keys))
|
||||
# Also delete user application cache
|
||||
keys = user_app_cache_key("*")
|
||||
keys = cache.keys(user_app_cache_key("*"))
|
||||
cache.delete_many(keys)
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
@ -39,7 +38,6 @@ class FlowCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
||||
form_class = FlowCacheClearForm
|
||||
|
||||
template_name = "generic/form_non_model.html"
|
||||
success_url = "/"
|
||||
success_message = _("Successfully cleared Flow cache")
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
|
@ -1,13 +1,11 @@
|
||||
"""authentik Policy administration"""
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db.models import QuerySet
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
@ -99,7 +97,7 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
|
||||
template_name = "administration/policy/test.html"
|
||||
object = None
|
||||
|
||||
def get_object(self, queryset=None) -> QuerySet:
|
||||
def get_object(self, queryset=None) -> Policy:
|
||||
return (
|
||||
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
@ -117,13 +115,12 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
|
||||
user = form.cleaned_data.get("user")
|
||||
|
||||
p_request = PolicyRequest(user)
|
||||
p_request.debug = True
|
||||
p_request.http_request = self.request
|
||||
p_request.context = form.cleaned_data
|
||||
p_request.context = form.cleaned_data.get("context", {})
|
||||
|
||||
proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None)
|
||||
result = proc.execute()
|
||||
if result.passing:
|
||||
messages.success(self.request, _("User successfully passed policy."))
|
||||
else:
|
||||
messages.error(self.request, _("User didn't pass policy."))
|
||||
return self.render_to_response(self.get_context_data(form=form, result=result))
|
||||
context = self.get_context_data(form=form)
|
||||
context["result"] = result
|
||||
return self.render_to_response(context)
|
||||
|
@ -1,41 +1,28 @@
|
||||
"""authentik PropertyMapping administration"""
|
||||
from json import dumps
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from django.views.generic import FormView
|
||||
from django.views.generic.detail import DetailView
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
|
||||
from authentik.admin.forms.policies import PolicyTestForm
|
||||
from authentik.admin.views.utils import (
|
||||
BackSuccessUrlMixin,
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
SearchListMixin,
|
||||
UserPaginateListMixin,
|
||||
)
|
||||
from authentik.core.models import PropertyMapping
|
||||
|
||||
|
||||
class PropertyMappingListView(
|
||||
LoginRequiredMixin,
|
||||
PermissionListMixin,
|
||||
UserPaginateListMixin,
|
||||
SearchListMixin,
|
||||
InheritanceListView,
|
||||
):
|
||||
"""Show list of all property_mappings"""
|
||||
|
||||
model = PropertyMapping
|
||||
permission_required = "authentik_core.view_propertymapping"
|
||||
template_name = "administration/property_mapping/list.html"
|
||||
ordering = "name"
|
||||
search_fields = ["name", "expression"]
|
||||
|
||||
|
||||
class PropertyMappingCreateView(
|
||||
SuccessMessageMixin,
|
||||
BackSuccessUrlMixin,
|
||||
@ -49,7 +36,6 @@ class PropertyMappingCreateView(
|
||||
permission_required = "authentik_core.add_propertymapping"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("authentik_admin:property-mappings")
|
||||
success_message = _("Successfully created Property Mapping")
|
||||
|
||||
|
||||
@ -66,7 +52,6 @@ class PropertyMappingUpdateView(
|
||||
permission_required = "authentik_core.change_propertymapping"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("authentik_admin:property-mappings")
|
||||
success_message = _("Successfully updated Property Mapping")
|
||||
|
||||
|
||||
@ -79,5 +64,45 @@ class PropertyMappingDeleteView(
|
||||
permission_required = "authentik_core.delete_propertymapping"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("authentik_admin:property-mappings")
|
||||
success_message = _("Successfully deleted Property Mapping")
|
||||
|
||||
|
||||
class PropertyMappingTestView(
|
||||
LoginRequiredMixin, DetailView, PermissionRequiredMixin, FormView
|
||||
):
|
||||
"""View to test property mappings"""
|
||||
|
||||
model = PropertyMapping
|
||||
form_class = PolicyTestForm
|
||||
permission_required = "authentik_core.view_propertymapping"
|
||||
template_name = "administration/property_mapping/test.html"
|
||||
object = None
|
||||
|
||||
def get_object(self, queryset=None) -> PropertyMapping:
|
||||
return (
|
||||
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
kwargs["property_mapping"] = self.get_object()
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def post(self, *args, **kwargs) -> HttpResponse:
|
||||
self.object = self.get_object()
|
||||
return super().post(*args, **kwargs)
|
||||
|
||||
def form_valid(self, form: PolicyTestForm) -> HttpResponse:
|
||||
mapping = self.get_object()
|
||||
user = form.cleaned_data.get("user")
|
||||
|
||||
context = self.get_context_data(form=form)
|
||||
try:
|
||||
result = mapping.evaluate(
|
||||
user, self.request, **form.cleaned_data.get("context", {})
|
||||
)
|
||||
context["result"] = dumps(result, indent=4)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
context["result"] = str(exc)
|
||||
return self.render_to_response(context)
|
||||
|
@ -4,38 +4,18 @@ from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
|
||||
from authentik.admin.views.utils import (
|
||||
BackSuccessUrlMixin,
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
SearchListMixin,
|
||||
UserPaginateListMixin,
|
||||
)
|
||||
from authentik.core.models import Provider
|
||||
|
||||
|
||||
class ProviderListView(
|
||||
LoginRequiredMixin,
|
||||
PermissionListMixin,
|
||||
UserPaginateListMixin,
|
||||
SearchListMixin,
|
||||
InheritanceListView,
|
||||
):
|
||||
"""Show list of all providers"""
|
||||
|
||||
model = Provider
|
||||
permission_required = "authentik_core.add_provider"
|
||||
template_name = "administration/provider/list.html"
|
||||
ordering = "pk"
|
||||
search_fields = ["pk", "name"]
|
||||
|
||||
|
||||
class ProviderCreateView(
|
||||
SuccessMessageMixin,
|
||||
BackSuccessUrlMixin,
|
||||
@ -49,7 +29,6 @@ class ProviderCreateView(
|
||||
permission_required = "authentik_core.add_provider"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("authentik_admin:providers")
|
||||
success_message = _("Successfully created Provider")
|
||||
|
||||
|
||||
@ -66,7 +45,6 @@ class ProviderUpdateView(
|
||||
permission_required = "authentik_core.change_provider"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("authentik_admin:providers")
|
||||
success_message = _("Successfully updated Provider")
|
||||
|
||||
|
||||
@ -79,5 +57,4 @@ class ProviderDeleteView(
|
||||
permission_required = "authentik_core.delete_provider"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("authentik_admin:providers")
|
||||
success_message = _("Successfully deleted Provider")
|
||||
|
@ -4,38 +4,18 @@ from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
|
||||
from authentik.admin.views.utils import (
|
||||
BackSuccessUrlMixin,
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
SearchListMixin,
|
||||
UserPaginateListMixin,
|
||||
)
|
||||
from authentik.core.models import Source
|
||||
|
||||
|
||||
class SourceListView(
|
||||
LoginRequiredMixin,
|
||||
PermissionListMixin,
|
||||
UserPaginateListMixin,
|
||||
SearchListMixin,
|
||||
InheritanceListView,
|
||||
):
|
||||
"""Show list of all sources"""
|
||||
|
||||
model = Source
|
||||
permission_required = "authentik_core.view_source"
|
||||
ordering = "name"
|
||||
template_name = "administration/source/list.html"
|
||||
search_fields = ["name", "slug"]
|
||||
|
||||
|
||||
class SourceCreateView(
|
||||
SuccessMessageMixin,
|
||||
BackSuccessUrlMixin,
|
||||
@ -49,7 +29,6 @@ class SourceCreateView(
|
||||
permission_required = "authentik_core.add_source"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("authentik_admin:sources")
|
||||
success_message = _("Successfully created Source")
|
||||
|
||||
|
||||
@ -66,7 +45,6 @@ class SourceUpdateView(
|
||||
permission_required = "authentik_core.change_source"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("authentik_admin:sources")
|
||||
success_message = _("Successfully updated Source")
|
||||
|
||||
|
||||
@ -77,5 +55,4 @@ class SourceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessag
|
||||
permission_required = "authentik_core.delete_source"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("authentik_admin:sources")
|
||||
success_message = _("Successfully deleted Source")
|
||||
|
@ -1,7 +1,31 @@
|
||||
{% extends "rest_framework/base.html" %}
|
||||
|
||||
{% block title %}{% if name %}{{ name }} – {% endif %}authentik{% endblock %}
|
||||
|
||||
{% block branding %}
|
||||
<span class='navbar-brand'>
|
||||
authentik
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
body {
|
||||
background-color: #18191a;
|
||||
color: #fafafa;
|
||||
}
|
||||
.prettyprint {
|
||||
background-color: #1c1e21;
|
||||
color: #fafafa;
|
||||
border: 1px solid #2b2e33;
|
||||
}
|
||||
.pln {
|
||||
color: #fafafa;
|
||||
}
|
||||
.well {
|
||||
background-color: #1c1e21;
|
||||
border: 1px solid #2b2e33;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
@ -29,11 +29,12 @@ from authentik.flows.api import (
|
||||
FlowViewSet,
|
||||
StageViewSet,
|
||||
)
|
||||
from authentik.outposts.api import (
|
||||
from authentik.outposts.api.outpost_service_connections import (
|
||||
DockerServiceConnectionViewSet,
|
||||
KubernetesServiceConnectionViewSet,
|
||||
OutpostViewSet,
|
||||
ServiceConnectionViewSet,
|
||||
)
|
||||
from authentik.outposts.api.outposts import OutpostViewSet
|
||||
from authentik.policies.api import (
|
||||
PolicyBindingViewSet,
|
||||
PolicyCacheViewSet,
|
||||
@ -88,6 +89,7 @@ router.register("core/users", UserViewSet)
|
||||
router.register("core/tokens", TokenViewSet)
|
||||
|
||||
router.register("outposts/outposts", OutpostViewSet)
|
||||
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
|
||||
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
|
||||
router.register(
|
||||
"outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet
|
||||
|
@ -4,9 +4,6 @@ from django.apps import AppConfig, apps
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.sites import AlreadyRegistered
|
||||
from guardian.admin import GuardedModelAdmin
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def admin_autoregister(app: AppConfig):
|
||||
@ -20,5 +17,4 @@ def admin_autoregister(app: AppConfig):
|
||||
|
||||
for _app in apps.get_app_configs():
|
||||
if _app.label.startswith("authentik_"):
|
||||
LOGGER.debug("Registering application for dj-admin", application=_app.label)
|
||||
admin_autoregister(_app)
|
||||
|
@ -91,7 +91,7 @@ class ApplicationViewSet(ModelViewSet):
|
||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||
self.paginate_queryset(queryset)
|
||||
|
||||
should_cache = "search" not in request.GET
|
||||
should_cache = request.GET.get("search", "") == ""
|
||||
|
||||
allowed_applications = []
|
||||
if not should_cache:
|
||||
|
@ -2,22 +2,36 @@
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.models import PropertyMapping
|
||||
|
||||
|
||||
class PropertyMappingSerializer(ModelSerializer):
|
||||
class PropertyMappingSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""PropertyMapping Serializer"""
|
||||
|
||||
__type__ = SerializerMethodField(method_name="get_type")
|
||||
object_type = SerializerMethodField(method_name="get_type")
|
||||
|
||||
def get_type(self, obj):
|
||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
||||
return obj._meta.object_name.lower().replace("propertymapping", "")
|
||||
|
||||
def to_representation(self, instance: PropertyMapping):
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
if instance.__class__ == PropertyMapping:
|
||||
return super().to_representation(instance)
|
||||
return instance.serializer(instance=instance).data
|
||||
|
||||
class Meta:
|
||||
|
||||
model = PropertyMapping
|
||||
fields = ["pk", "name", "expression", "__type__"]
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"expression",
|
||||
"object_type",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
class PropertyMappingViewSet(ReadOnlyModelViewSet):
|
||||
@ -25,6 +39,11 @@ class PropertyMappingViewSet(ReadOnlyModelViewSet):
|
||||
|
||||
queryset = PropertyMapping.objects.none()
|
||||
serializer_class = PropertyMappingSerializer
|
||||
search_fields = [
|
||||
"name",
|
||||
]
|
||||
filterset_fields = {"managed": ["isnull"]}
|
||||
ordering = ["name"]
|
||||
|
||||
def get_queryset(self):
|
||||
return PropertyMapping.objects.select_subclasses()
|
||||
|
@ -1,26 +1,32 @@
|
||||
"""Provider API Views"""
|
||||
from django.shortcuts import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.models import Provider
|
||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Provider Serializer"""
|
||||
|
||||
assigned_application_slug = ReadOnlyField(source="application.slug")
|
||||
assigned_application_name = ReadOnlyField(source="application.name")
|
||||
|
||||
object_type = SerializerMethodField()
|
||||
|
||||
def get_object_type(self, obj):
|
||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
||||
return obj._meta.object_name.lower().replace("provider", "")
|
||||
|
||||
def to_representation(self, instance: Provider):
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
if instance.__class__ == Provider:
|
||||
return super().to_representation(instance)
|
||||
return instance.serializer(instance=instance).data
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Provider
|
||||
@ -31,6 +37,8 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"authorization_flow",
|
||||
"property_mappings",
|
||||
"object_type",
|
||||
"assigned_application_slug",
|
||||
"assigned_application_name",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
@ -44,6 +52,33 @@ class ProviderViewSet(ModelViewSet):
|
||||
filterset_fields = {
|
||||
"application": ["isnull"],
|
||||
}
|
||||
search_fields = [
|
||||
"name",
|
||||
"application__name",
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return Provider.objects.select_subclasses()
|
||||
|
||||
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False)
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable provider types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
data.append(
|
||||
{
|
||||
"name": verbose_name(subclass),
|
||||
"description": subclass.__doc__,
|
||||
"link": reverse("authentik_admin:provider-create")
|
||||
+ f"?type={subclass.__name__}",
|
||||
}
|
||||
)
|
||||
data.append(
|
||||
{
|
||||
"name": _("SAML Provider from Metadata"),
|
||||
"description": _("Create a SAML Provider by importing its Metadata."),
|
||||
"link": reverse("authentik_admin:provider-saml-from-metadata"),
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
@ -1,31 +1,41 @@
|
||||
"""Source API Views"""
|
||||
from django.shortcuts import reverse
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.models import Source
|
||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Source Serializer"""
|
||||
|
||||
__type__ = SerializerMethodField(method_name="get_type")
|
||||
object_type = SerializerMethodField()
|
||||
|
||||
def get_type(self, obj):
|
||||
def get_object_type(self, obj):
|
||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
||||
return obj._meta.object_name.lower().replace("source", "")
|
||||
|
||||
def to_representation(self, instance: Source):
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
if instance.__class__ == Source:
|
||||
return super().to_representation(instance)
|
||||
return instance.serializer(instance=instance).data
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Source
|
||||
fields = SOURCE_SERIALIZER_FIELDS + ["__type__"]
|
||||
fields = SOURCE_SERIALIZER_FIELDS = [
|
||||
"pk",
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
"object_type",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
class SourceViewSet(ReadOnlyModelViewSet):
|
||||
@ -37,3 +47,19 @@ class SourceViewSet(ReadOnlyModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
return Source.objects.select_subclasses()
|
||||
|
||||
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False)
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable source types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
data.append(
|
||||
{
|
||||
"name": verbose_name(subclass),
|
||||
"description": subclass.__doc__,
|
||||
"link": reverse("authentik_admin:source-create")
|
||||
+ f"?type={subclass.__name__}",
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
@ -1,9 +1,12 @@
|
||||
"""Tokens API Viewset"""
|
||||
from django.db.models.base import Model
|
||||
from django.http.response import Http404
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.serializers import ModelSerializer, Serializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.models import Token
|
||||
@ -19,6 +22,18 @@ class TokenSerializer(ModelSerializer):
|
||||
fields = ["pk", "identifier", "intent", "user", "description"]
|
||||
|
||||
|
||||
class TokenViewSerializer(Serializer):
|
||||
"""Show token's current key"""
|
||||
|
||||
key = CharField(read_only=True)
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TokenViewSet(ModelViewSet):
|
||||
"""Token Viewset"""
|
||||
|
||||
@ -26,12 +41,15 @@ class TokenViewSet(ModelViewSet):
|
||||
queryset = Token.filter_not_expired()
|
||||
serializer_class = TokenSerializer
|
||||
|
||||
@swagger_auto_schema(responses={200: TokenViewSerializer(many=False)})
|
||||
@action(detail=True)
|
||||
# pylint: disable=unused-argument
|
||||
def view_key(self, request: Request, identifier: str) -> Response:
|
||||
"""Return token key and log access"""
|
||||
tokens = Token.filter_not_expired(identifier=identifier)
|
||||
if not tokens.exists():
|
||||
token: Token = self.get_object()
|
||||
if token.is_expired:
|
||||
raise Http404
|
||||
token = tokens.first()
|
||||
Event.new(EventAction.TOKEN_VIEW, token=token).from_http(request)
|
||||
return Response({"key": token.key})
|
||||
Event.new(EventAction.SECRET_VIEW, secret=token).from_http( # noqa # nosec
|
||||
request
|
||||
)
|
||||
return Response(TokenViewSerializer({"key": token.key}).data)
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""API Utilities"""
|
||||
from django.db.models import Model
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.serializers import Serializer, SerializerMethodField
|
||||
|
||||
|
||||
@ -22,3 +23,17 @@ class MetaNameSerializer(Serializer):
|
||||
def get_verbose_name_plural(self, obj: Model) -> str:
|
||||
"""Return object's plural verbose_name"""
|
||||
return obj._meta.verbose_name_plural
|
||||
|
||||
|
||||
class TypeCreateSerializer(Serializer):
|
||||
"""Types of an object that can be created"""
|
||||
|
||||
name = CharField(read_only=True)
|
||||
description = CharField(read_only=True)
|
||||
link = CharField(read_only=True)
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""authentik core app config"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
@ -9,3 +11,6 @@ class AuthentikCoreConfig(AppConfig):
|
||||
label = "authentik_core"
|
||||
verbose_name = "authentik Core"
|
||||
mountpoint = ""
|
||||
|
||||
def ready(self):
|
||||
import_module("authentik.core.signals")
|
||||
|
35
authentik/core/migrations/0017_managed.py
Normal file
35
authentik/core/migrations/0017_managed.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-30 18:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0016_auto_20201202_2234"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="propertymapping",
|
||||
name="managed",
|
||||
field=models.TextField(
|
||||
default=None,
|
||||
help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
|
||||
null=True,
|
||||
verbose_name="Managed by authentik",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="token",
|
||||
name="managed",
|
||||
field=models.TextField(
|
||||
default=None,
|
||||
help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
|
||||
null=True,
|
||||
verbose_name="Managed by authentik",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
]
|
@ -22,6 +22,7 @@ from authentik.core.signals import password_changed
|
||||
from authentik.core.types import UILoginButton
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
||||
from authentik.managed.models import ManagedModel
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -313,7 +314,7 @@ class TokenIntents(models.TextChoices):
|
||||
INTENT_RECOVERY = "recovery"
|
||||
|
||||
|
||||
class Token(ExpiringModel):
|
||||
class Token(ManagedModel, ExpiringModel):
|
||||
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
|
||||
|
||||
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
@ -341,7 +342,7 @@ class Token(ExpiringModel):
|
||||
]
|
||||
|
||||
|
||||
class PropertyMapping(models.Model):
|
||||
class PropertyMapping(SerializerModel, ManagedModel):
|
||||
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
|
||||
|
||||
pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
@ -355,6 +356,11 @@ class PropertyMapping(models.Model):
|
||||
"""Return Form class used to edit this object"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def serializer(self) -> Type[Serializer]:
|
||||
"""Get serializer for this model"""
|
||||
raise NotImplementedError
|
||||
|
||||
def evaluate(
|
||||
self, user: Optional[User], request: Optional[HttpRequest], **kwargs
|
||||
) -> Any:
|
||||
|
@ -1,5 +1,24 @@
|
||||
"""authentik core signals"""
|
||||
from django.core.cache import cache
|
||||
from django.core.signals import Signal
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
# Arguments: user: User, password: str
|
||||
password_changed = Signal()
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_application(sender, instance, created: bool, **_):
|
||||
"""Clear user's application cache upon application creation"""
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.api.applications import user_app_cache_key
|
||||
|
||||
if sender != Application:
|
||||
return
|
||||
if not created:
|
||||
return
|
||||
# Also delete user application cache
|
||||
keys = cache.keys(user_app_cache_key("*"))
|
||||
cache.delete_many(keys)
|
||||
|
@ -2,15 +2,29 @@
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from rest_framework.serializers import ModelSerializer, ValidationError
|
||||
from django.db.models import Model
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, DateTimeField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, Serializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
|
||||
class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"""CertificateKeyPair Serializer"""
|
||||
|
||||
cert_expiry = DateTimeField(source="certificate.not_valid_after", read_only=True)
|
||||
cert_subject = SerializerMethodField()
|
||||
|
||||
def get_cert_subject(self, instance: CertificateKeyPair) -> str:
|
||||
"""Get certificate subject as full rfc4514"""
|
||||
return instance.certificate.subject.rfc4514_string()
|
||||
|
||||
def validate_certificate_data(self, value):
|
||||
"""Verify that input is a valid PEM x509 Certificate"""
|
||||
try:
|
||||
@ -36,7 +50,31 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
model = CertificateKeyPair
|
||||
fields = ["pk", "name", "certificate_data", "key_data"]
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"fingerprint",
|
||||
"certificate_data",
|
||||
"key_data",
|
||||
"cert_expiry",
|
||||
"cert_subject",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"key_data": {"write_only": True},
|
||||
"certificate_data": {"write_only": True},
|
||||
}
|
||||
|
||||
|
||||
class CertificateDataSerializer(Serializer):
|
||||
"""Get CertificateKeyPair's data"""
|
||||
|
||||
data = CharField(read_only=True)
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CertificateKeyPairViewSet(ModelViewSet):
|
||||
@ -44,3 +82,31 @@ class CertificateKeyPairViewSet(ModelViewSet):
|
||||
|
||||
queryset = CertificateKeyPair.objects.all()
|
||||
serializer_class = CertificateKeyPairSerializer
|
||||
|
||||
@swagger_auto_schema(responses={200: CertificateDataSerializer(many=False)})
|
||||
@action(detail=True)
|
||||
# pylint: disable=invalid-name, unused-argument
|
||||
def view_certificate(self, request: Request, pk: str) -> Response:
|
||||
"""Return certificate-key pairs certificate and log access"""
|
||||
certificate: CertificateKeyPair = self.get_object()
|
||||
Event.new( # noqa # nosec
|
||||
EventAction.SECRET_VIEW,
|
||||
secret=certificate,
|
||||
type="certificate",
|
||||
).from_http(request)
|
||||
return Response(
|
||||
CertificateDataSerializer({"data": certificate.certificate_data}).data
|
||||
)
|
||||
|
||||
@swagger_auto_schema(responses={200: CertificateDataSerializer(many=False)})
|
||||
@action(detail=True)
|
||||
# pylint: disable=invalid-name, unused-argument
|
||||
def view_private_key(self, request: Request, pk: str) -> Response:
|
||||
"""Return certificate-key pairs private key and log access"""
|
||||
certificate: CertificateKeyPair = self.get_object()
|
||||
Event.new( # noqa # nosec
|
||||
EventAction.SECRET_VIEW,
|
||||
secret=certificate,
|
||||
type="private_key",
|
||||
).from_http(request)
|
||||
return Response(CertificateDataSerializer({"data": certificate.key_data}).data)
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Create self-signed certificates"""
|
||||
import datetime
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
@ -8,6 +9,9 @@ from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.x509.oid import NameOID
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
|
||||
class CertificateBuilder:
|
||||
"""Build self-signed certificates"""
|
||||
@ -17,19 +21,39 @@ class CertificateBuilder:
|
||||
__builder = None
|
||||
__certificate = None
|
||||
|
||||
common_name: str
|
||||
|
||||
def __init__(self):
|
||||
self.__public_key = None
|
||||
self.__private_key = None
|
||||
self.__builder = None
|
||||
self.__certificate = None
|
||||
self.common_name = "authentik Self-signed Certificate"
|
||||
|
||||
def build(self):
|
||||
def save(self) -> Optional[CertificateKeyPair]:
|
||||
"""Save generated certificate as model"""
|
||||
if not self.__certificate:
|
||||
return None
|
||||
return CertificateKeyPair.objects.create(
|
||||
name=self.common_name,
|
||||
certificate_data=self.certificate,
|
||||
key_data=self.private_key,
|
||||
)
|
||||
|
||||
def build(
|
||||
self,
|
||||
validity_days: int = 365,
|
||||
subject_alt_names: Optional[list[str]] = None,
|
||||
):
|
||||
"""Build self-signed certificate"""
|
||||
one_day = datetime.timedelta(1, 0, 0)
|
||||
self.__private_key = rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=2048, backend=default_backend()
|
||||
)
|
||||
self.__public_key = self.__private_key.public_key()
|
||||
alt_names: list[x509.GeneralName] = [
|
||||
x509.DNSName(x) for x in subject_alt_names or []
|
||||
]
|
||||
self.__builder = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(
|
||||
@ -37,7 +61,7 @@ class CertificateBuilder:
|
||||
[
|
||||
x509.NameAttribute(
|
||||
NameOID.COMMON_NAME,
|
||||
"authentik Self-signed Certificate",
|
||||
self.common_name,
|
||||
),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "authentik"),
|
||||
x509.NameAttribute(
|
||||
@ -51,13 +75,16 @@ class CertificateBuilder:
|
||||
[
|
||||
x509.NameAttribute(
|
||||
NameOID.COMMON_NAME,
|
||||
"authentik Self-signed Certificate",
|
||||
f"authentik {__version__}",
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
.add_extension(x509.SubjectAlternativeName(alt_names), critical=True)
|
||||
.not_valid_before(datetime.datetime.today() - one_day)
|
||||
.not_valid_after(datetime.datetime.today() + datetime.timedelta(days=365))
|
||||
.not_valid_after(
|
||||
datetime.datetime.today() + datetime.timedelta(days=validity_days)
|
||||
)
|
||||
.serial_number(int(uuid.uuid4()))
|
||||
.public_key(self.__public_key)
|
||||
)
|
||||
|
@ -8,6 +8,14 @@ from django.utils.translation import gettext_lazy as _
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
|
||||
class CertificateKeyPairGenerateForm(forms.Form):
|
||||
"""CertificateKeyPair generation form"""
|
||||
|
||||
common_name = forms.CharField()
|
||||
subject_alt_name = forms.CharField(required=False, label=_("Subject-alt name"))
|
||||
validity_days = forms.IntegerField(initial=365)
|
||||
|
||||
|
||||
class CertificateKeyPairForm(forms.ModelForm):
|
||||
"""CertificateKeyPair Form"""
|
||||
|
||||
|
@ -50,6 +50,7 @@ class EventViewSet(ReadOnlyModelViewSet):
|
||||
serializer_class = EventSerializer
|
||||
ordering = ["-created"]
|
||||
search_fields = [
|
||||
"event_uuid",
|
||||
"user",
|
||||
"action",
|
||||
"app",
|
||||
|
@ -15,6 +15,7 @@ class NotificationTransportForm(forms.ModelForm):
|
||||
"name",
|
||||
"mode",
|
||||
"webhook_url",
|
||||
"send_once",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
|
52
authentik/events/migrations/0012_auto_20210202_1821.py
Normal file
52
authentik/events/migrations/0012_auto_20210202_1821.py
Normal file
@ -0,0 +1,52 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-02 18:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0011_notification_rules_default_v1"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="notificationtransport",
|
||||
name="send_once",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Only send notification once, for example when sending a webhook into a chat channel.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("user_write", "User Write"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("token_view", "Token View"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("policy_execution", "Policy Execution"),
|
||||
("policy_exception", "Policy Exception"),
|
||||
("property_mapping_exception", "Property Mapping Exception"),
|
||||
("system_task_execution", "System Task Execution"),
|
||||
("system_task_exception", "System Task Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("update_available", "Update Available"),
|
||||
("custom_", "Custom Prefix"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
61
authentik/events/migrations/0013_auto_20210209_1657.py
Normal file
61
authentik/events/migrations/0013_auto_20210209_1657.py
Normal file
@ -0,0 +1,61 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-09 16:57
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def token_view_to_secret_view(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from authentik.events.models import EventAction
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
Event = apps.get_model("authentik_events", "Event")
|
||||
|
||||
events = Event.objects.using(db_alias).filter(action="token_view")
|
||||
|
||||
for event in events:
|
||||
event.context["secret"] = event.context.pop("token")
|
||||
event.action = EventAction.SECRET_VIEW
|
||||
|
||||
Event.objects.using(db_alias).bulk_update(events, ["context", "action"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0012_auto_20210202_1821"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("user_write", "User Write"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("secret_view", "Secret View"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("policy_execution", "Policy Execution"),
|
||||
("policy_exception", "Policy Exception"),
|
||||
("property_mapping_exception", "Property Mapping Exception"),
|
||||
("system_task_execution", "System Task Execution"),
|
||||
("system_task_exception", "System Task Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("update_available", "Update Available"),
|
||||
("custom_", "Custom Prefix"),
|
||||
]
|
||||
),
|
||||
),
|
||||
migrations.RunPython(token_view_to_secret_view),
|
||||
]
|
@ -42,7 +42,7 @@ class EventAction(models.TextChoices):
|
||||
SUSPICIOUS_REQUEST = "suspicious_request"
|
||||
PASSWORD_SET = "password_set" # noqa # nosec
|
||||
|
||||
TOKEN_VIEW = "token_view" # nosec
|
||||
SECRET_VIEW = "secret_view" # noqa # nosec
|
||||
|
||||
INVITE_USED = "invitation_used"
|
||||
|
||||
@ -184,6 +184,12 @@ class NotificationTransport(models.Model):
|
||||
mode = models.TextField(choices=TransportMode.choices)
|
||||
|
||||
webhook_url = models.TextField(blank=True)
|
||||
send_once = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"Only send notification once, for example when sending a webhook into a chat channel."
|
||||
),
|
||||
)
|
||||
|
||||
def send(self, notification: "Notification") -> list[str]:
|
||||
"""Send notification to user, called from async task"""
|
||||
@ -254,7 +260,6 @@ class NotificationTransport(models.Model):
|
||||
}
|
||||
if notification.event:
|
||||
body["attachments"][0]["title"] = notification.event.action
|
||||
body["attachments"][0]["text"] = notification.event.action
|
||||
try:
|
||||
response = post(self.webhook_url, json=body)
|
||||
response.raise_for_status()
|
||||
@ -267,17 +272,24 @@ class NotificationTransport(models.Model):
|
||||
|
||||
def send_email(self, notification: "Notification") -> list[str]:
|
||||
"""Send notification via global email configuration"""
|
||||
body_trunc = (
|
||||
(notification.body[:75] + "..")
|
||||
if len(notification.body) > 75
|
||||
else notification.body
|
||||
)
|
||||
subject = "authentik Notification: "
|
||||
key_value = {}
|
||||
if notification.event:
|
||||
subject += notification.event.action
|
||||
for key, value in notification.event.context.items():
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
key_value[key] = value
|
||||
else:
|
||||
subject += notification.body[:75]
|
||||
mail = TemplateEmailMessage(
|
||||
subject=f"authentik Notification: {body_trunc}",
|
||||
subject=subject,
|
||||
template_name="email/generic.html",
|
||||
to=[notification.user.email],
|
||||
template_context={
|
||||
"title": subject,
|
||||
"body": notification.body,
|
||||
"key_value": key_value,
|
||||
},
|
||||
)
|
||||
# Email is sent directly here, as the call to send() should have been from a task.
|
||||
|
@ -124,13 +124,6 @@ class MonitoredTask(Task):
|
||||
task_call_args=args,
|
||||
task_call_kwargs=kwargs,
|
||||
).save(self.result_timeout_hours)
|
||||
Event.new(
|
||||
EventAction.SYSTEM_TASK_EXECUTION,
|
||||
message=(
|
||||
f"Task {self.__name__} finished successfully: "
|
||||
"\n".join(self._result.messages)
|
||||
),
|
||||
).save()
|
||||
return super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
|
@ -65,15 +65,17 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
|
||||
|
||||
LOGGER.debug("e(trigger): event trigger matched", trigger=trigger)
|
||||
# Create the notification objects
|
||||
for user in trigger.group.users.all():
|
||||
notification = Notification.objects.create(
|
||||
severity=trigger.severity, body=event.summary, event=event, user=user
|
||||
)
|
||||
|
||||
for transport in trigger.transports.all():
|
||||
for transport in trigger.transports.all():
|
||||
for user in trigger.group.users.all():
|
||||
LOGGER.debug("created notification")
|
||||
notification = Notification.objects.create(
|
||||
severity=trigger.severity, body=event.summary, event=event, user=user
|
||||
)
|
||||
notification_transport.apply_async(
|
||||
args=[notification.pk, transport.pk], queue="authentik_events"
|
||||
)
|
||||
if transport.send_once:
|
||||
break
|
||||
|
||||
|
||||
@CELERY_APP.task(
|
||||
|
@ -8,6 +8,7 @@ from authentik.core.models import Group, User
|
||||
from authentik.events.models import (
|
||||
Event,
|
||||
EventAction,
|
||||
Notification,
|
||||
NotificationRule,
|
||||
NotificationTransport,
|
||||
)
|
||||
@ -21,7 +22,7 @@ class TestEventsNotifications(TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.group = Group.objects.create(name="test-group")
|
||||
self.user = User.objects.create(name="test-user")
|
||||
self.user = User.objects.create(name="test-user", username="test")
|
||||
self.group.users.add(self.user)
|
||||
self.group.save()
|
||||
|
||||
@ -88,3 +89,26 @@ class TestEventsNotifications(TestCase):
|
||||
):
|
||||
Event.new(EventAction.CUSTOM_PREFIX).save()
|
||||
self.assertEqual(passes.call_count, 1)
|
||||
|
||||
def test_transport_once(self):
|
||||
"""Test transport's send_once"""
|
||||
user2 = User.objects.create(name="test2-user", username="test2")
|
||||
self.group.users.add(user2)
|
||||
self.group.save()
|
||||
|
||||
transport = NotificationTransport.objects.create(
|
||||
name="transport", send_once=True
|
||||
)
|
||||
NotificationRule.objects.filter(name__startswith="default").delete()
|
||||
trigger = NotificationRule.objects.create(name="trigger", group=self.group)
|
||||
trigger.transports.add(transport)
|
||||
trigger.save()
|
||||
matcher = EventMatcherPolicy.objects.create(
|
||||
name="matcher", action=EventAction.CUSTOM_PREFIX
|
||||
)
|
||||
PolicyBinding.objects.create(target=trigger, policy=matcher, order=0)
|
||||
|
||||
execute_mock = MagicMock()
|
||||
with patch("authentik.events.models.NotificationTransport.send", execute_mock):
|
||||
Event.new(EventAction.CUSTOM_PREFIX).save()
|
||||
self.assertEqual(Notification.objects.count(), 1)
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""authentik benchmark command"""
|
||||
from csv import DictWriter
|
||||
from multiprocessing import Manager, Process, cpu_count
|
||||
from multiprocessing import Manager, cpu_count, get_context
|
||||
from sys import stdout
|
||||
from time import time
|
||||
|
||||
@ -15,9 +15,11 @@ from authentik.flows.models import Flow
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
|
||||
LOGGER = get_logger()
|
||||
FORK_CTX = get_context("fork")
|
||||
PROCESS_CLASS = FORK_CTX.Process
|
||||
|
||||
|
||||
class FlowPlanProcess(Process): # pragma: no cover
|
||||
class FlowPlanProcess(PROCESS_CLASS): # pragma: no cover
|
||||
"""Test process which executes flow planner"""
|
||||
|
||||
def __init__(self, index, return_dict, flow, user) -> None:
|
||||
|
@ -6,7 +6,7 @@ from django.core.cache import cache
|
||||
from django.http import HttpRequest
|
||||
from sentry_sdk.hub import Hub
|
||||
from sentry_sdk.tracing import Span
|
||||
from structlog.stdlib import get_logger
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import cleanse_dict
|
||||
@ -16,7 +16,6 @@ from authentik.flows.models import Flow, FlowStageBinding, Stage
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
PLAN_CONTEXT_PENDING_USER = "pending_user"
|
||||
PLAN_CONTEXT_SSO = "is_sso"
|
||||
PLAN_CONTEXT_REDIRECT = "redirect"
|
||||
@ -88,10 +87,13 @@ class FlowPlanner:
|
||||
|
||||
flow: Flow
|
||||
|
||||
_logger: BoundLogger
|
||||
|
||||
def __init__(self, flow: Flow):
|
||||
self.use_cache = True
|
||||
self.allow_empty_flows = False
|
||||
self.flow = flow
|
||||
self._logger = get_logger().bind(flow=flow)
|
||||
|
||||
def plan(
|
||||
self, request: HttpRequest, default_context: Optional[Dict[str, Any]] = None
|
||||
@ -103,7 +105,9 @@ class FlowPlanner:
|
||||
span.set_data("flow", self.flow)
|
||||
span.set_data("request", request)
|
||||
|
||||
LOGGER.debug("f(plan): Starting planning process", flow=self.flow)
|
||||
self._logger.debug(
|
||||
"f(plan): starting planning process",
|
||||
)
|
||||
# Bit of a workaround here, if there is a pending user set in the default context
|
||||
# we use that user for our cache key
|
||||
# to make sure they don't get the generic response
|
||||
@ -125,15 +129,16 @@ class FlowPlanner:
|
||||
cached_plan_key = cache_key(self.flow, user)
|
||||
cached_plan = cache.get(cached_plan_key, None)
|
||||
if cached_plan and self.use_cache:
|
||||
LOGGER.debug(
|
||||
"f(plan): Taking plan from cache",
|
||||
flow=self.flow,
|
||||
self._logger.debug(
|
||||
"f(plan): taking plan from cache",
|
||||
key=cached_plan_key,
|
||||
)
|
||||
# Reset the context as this isn't factored into caching
|
||||
cached_plan.context = default_context or {}
|
||||
return cached_plan
|
||||
LOGGER.debug("f(plan): building plan", flow=self.flow)
|
||||
self._logger.debug(
|
||||
"f(plan): building plan",
|
||||
)
|
||||
plan = self._build_plan(user, request, default_context)
|
||||
cache.set(cache_key(self.flow, user), plan)
|
||||
if not plan.stages and not self.allow_empty_flows:
|
||||
@ -165,39 +170,34 @@ class FlowPlanner:
|
||||
stage = binding.stage
|
||||
marker = StageMarker()
|
||||
if binding.evaluate_on_plan:
|
||||
LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"f(plan): evaluating on plan",
|
||||
stage=binding.stage,
|
||||
flow=self.flow,
|
||||
)
|
||||
engine = PolicyEngine(binding, user, request)
|
||||
engine.request.context = plan.context
|
||||
engine.build()
|
||||
if engine.passing:
|
||||
LOGGER.debug(
|
||||
"f(plan): Stage passing",
|
||||
self._logger.debug(
|
||||
"f(plan): stage passing",
|
||||
stage=binding.stage,
|
||||
flow=self.flow,
|
||||
)
|
||||
else:
|
||||
stage = None
|
||||
else:
|
||||
LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"f(plan): not evaluating on plan",
|
||||
stage=binding.stage,
|
||||
flow=self.flow,
|
||||
)
|
||||
if binding.re_evaluate_policies and stage:
|
||||
LOGGER.debug(
|
||||
"f(plan): Stage has re-evaluate marker",
|
||||
self._logger.debug(
|
||||
"f(plan): stage has re-evaluate marker",
|
||||
stage=binding.stage,
|
||||
flow=self.flow,
|
||||
)
|
||||
marker = ReevaluateMarker(binding=binding, user=user)
|
||||
if stage:
|
||||
plan.append(stage, marker)
|
||||
LOGGER.debug(
|
||||
"f(plan): Finished building",
|
||||
flow=self.flow,
|
||||
self._logger.debug(
|
||||
"f(plan): finished building",
|
||||
)
|
||||
return plan
|
||||
|
@ -15,7 +15,7 @@ from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from django.views.generic import TemplateView, View
|
||||
from structlog.stdlib import get_logger
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
||||
from authentik.events.models import cleanse_dict
|
||||
@ -49,45 +49,48 @@ class FlowExecutorView(View):
|
||||
current_stage: Stage
|
||||
current_stage_view: View
|
||||
|
||||
_logger: BoundLogger
|
||||
|
||||
def setup(self, request: HttpRequest, flow_slug: str):
|
||||
super().setup(request, flow_slug=flow_slug)
|
||||
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
|
||||
self._logger = get_logger().bind(flow_slug=flow_slug)
|
||||
|
||||
def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
|
||||
"""When a flow is non-applicable check if user is on the correct domain"""
|
||||
if NEXT_ARG_NAME in self.request.GET:
|
||||
if not is_url_absolute(self.request.GET.get(NEXT_ARG_NAME)):
|
||||
LOGGER.debug("f(exec): Redirecting to next on fail")
|
||||
self._logger.debug("f(exec): Redirecting to next on fail")
|
||||
return redirect(self.request.GET.get(NEXT_ARG_NAME))
|
||||
message = exc.__doc__ if exc.__doc__ else str(exc)
|
||||
return self.stage_invalid(error_message=message)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
||||
# Early check if theres an active Plan for the current session
|
||||
if SESSION_KEY_PLAN in self.request.session:
|
||||
self.plan = self.request.session[SESSION_KEY_PLAN]
|
||||
if self.plan.flow_pk != self.flow.pk.hex:
|
||||
LOGGER.warning(
|
||||
self._logger.warning(
|
||||
"f(exec): Found existing plan for other flow, deleteing plan",
|
||||
flow_slug=flow_slug,
|
||||
)
|
||||
# Existing plan is deleted from session and instance
|
||||
self.plan = None
|
||||
self.cancel()
|
||||
LOGGER.debug("f(exec): Continuing existing plan", flow_slug=flow_slug)
|
||||
self._logger.debug("f(exec): Continuing existing plan")
|
||||
|
||||
# Don't check session again as we've either already loaded the plan or we need to plan
|
||||
if not self.plan:
|
||||
LOGGER.debug(
|
||||
"f(exec): No active Plan found, initiating planner", flow_slug=flow_slug
|
||||
)
|
||||
self._logger.debug("f(exec): No active Plan found, initiating planner")
|
||||
try:
|
||||
self.plan = self._initiate_plan()
|
||||
except FlowNonApplicableException as exc:
|
||||
LOGGER.warning("f(exec): Flow not applicable to current user", exc=exc)
|
||||
self._logger.warning(
|
||||
"f(exec): Flow not applicable to current user", exc=exc
|
||||
)
|
||||
return to_stage_response(self.request, self.handle_invalid_flow(exc))
|
||||
except EmptyFlowException as exc:
|
||||
LOGGER.warning("f(exec): Flow is empty", exc=exc)
|
||||
self._logger.warning("f(exec): Flow is empty", exc=exc)
|
||||
# To match behaviour with loading an empty flow plan from cache,
|
||||
# we don't show an error message here, but rather call _flow_done()
|
||||
return self._flow_done()
|
||||
@ -95,10 +98,10 @@ class FlowExecutorView(View):
|
||||
# as it hasn't been successfully passed yet
|
||||
next_stage = self.plan.next(self.request)
|
||||
if not next_stage:
|
||||
LOGGER.debug("f(exec): no more stages, flow is done.")
|
||||
self._logger.debug("f(exec): no more stages, flow is done.")
|
||||
return self._flow_done()
|
||||
self.current_stage = next_stage
|
||||
LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"f(exec): Current stage",
|
||||
current_stage=self.current_stage,
|
||||
flow_slug=self.flow.slug,
|
||||
@ -112,32 +115,30 @@ class FlowExecutorView(View):
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""pass get request to current stage"""
|
||||
LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"f(exec): Passing GET",
|
||||
view_class=class_to_path(self.current_stage_view.__class__),
|
||||
stage=self.current_stage,
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
try:
|
||||
stage_response = self.current_stage_view.get(request, *args, **kwargs)
|
||||
return to_stage_response(request, stage_response)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
LOGGER.exception(exc)
|
||||
self._logger.exception(exc)
|
||||
return to_stage_response(request, FlowErrorResponse(request, exc))
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""pass post request to current stage"""
|
||||
LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"f(exec): Passing POST",
|
||||
view_class=class_to_path(self.current_stage_view.__class__),
|
||||
stage=self.current_stage,
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
try:
|
||||
stage_response = self.current_stage_view.post(request, *args, **kwargs)
|
||||
return to_stage_response(request, stage_response)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
LOGGER.exception(exc)
|
||||
self._logger.exception(exc)
|
||||
return to_stage_response(request, FlowErrorResponse(request, exc))
|
||||
|
||||
def _initiate_plan(self) -> FlowPlan:
|
||||
@ -163,26 +164,23 @@ class FlowExecutorView(View):
|
||||
def stage_ok(self) -> HttpResponse:
|
||||
"""Callback called by stages upon successful completion.
|
||||
Persists updated plan and context to session."""
|
||||
LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"f(exec): Stage ok",
|
||||
stage_class=class_to_path(self.current_stage_view.__class__),
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
self.plan.pop()
|
||||
self.request.session[SESSION_KEY_PLAN] = self.plan
|
||||
if self.plan.stages:
|
||||
LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"f(exec): Continuing with next stage",
|
||||
reamining=len(self.plan.stages),
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
return redirect_with_qs(
|
||||
"authentik_flows:flow-executor", self.request.GET, **self.kwargs
|
||||
)
|
||||
# User passed all stages
|
||||
LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"f(exec): User passed all stages",
|
||||
flow_slug=self.flow.slug,
|
||||
context=cleanse_dict(self.plan.context),
|
||||
)
|
||||
return self._flow_done()
|
||||
@ -193,7 +191,7 @@ class FlowExecutorView(View):
|
||||
|
||||
Optionally, an exception can be passed, which will be shown if the current user
|
||||
is a superuser."""
|
||||
LOGGER.debug("f(exec): Stage invalid", flow_slug=self.flow.slug)
|
||||
self._logger.debug("f(exec): Stage invalid")
|
||||
self.cancel()
|
||||
response = AccessDeniedResponse(
|
||||
self.request, template="flows/denied_shell.html"
|
||||
|
@ -98,6 +98,10 @@ class BaseEvaluator:
|
||||
exec(ast_obj, self._globals, _locals) # nosec # noqa
|
||||
result = _locals["result"]
|
||||
except Exception as exc:
|
||||
# So, this is a bit questionable. Essentially, we are edit the stacktrace
|
||||
# so the user only sees information relevant to them
|
||||
# and none of our surrounding error handling
|
||||
exc.__traceback__ = exc.__traceback__.tb_next
|
||||
self.handle_error(exc, expression_source)
|
||||
raise exc
|
||||
return result
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""logging helpers"""
|
||||
from logging import Logger
|
||||
from os import getpid
|
||||
from typing import Callable
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@ -9,15 +8,3 @@ def add_process_id(logger: Logger, method_name: str, event_dict):
|
||||
"""Add the current process ID"""
|
||||
event_dict["pid"] = getpid()
|
||||
return event_dict
|
||||
|
||||
|
||||
def add_common_fields(environment: str) -> Callable:
|
||||
"""Add a common field to easily search for authentik logs"""
|
||||
|
||||
def add_common_field(logger: Logger, method_name: str, event_dict):
|
||||
"""Add a common field to easily search for authentik logs"""
|
||||
event_dict["app"] = "authentik"
|
||||
event_dict["app_environment"] = environment
|
||||
return event_dict
|
||||
|
||||
return add_common_field
|
||||
|
@ -59,6 +59,5 @@ def before_send(event, hint):
|
||||
if "exc_info" in hint:
|
||||
_, exc_value, _ = hint["exc_info"]
|
||||
if isinstance(exc_value, ignored_classes):
|
||||
LOGGER.info("Supressing error %r", exc_value)
|
||||
return None
|
||||
return event
|
||||
|
16
authentik/managed/apps.py
Normal file
16
authentik/managed/apps.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""authentik Managed app"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikManagedConfig(AppConfig):
|
||||
"""authentik Managed app"""
|
||||
|
||||
name = "authentik.managed"
|
||||
label = "authentik_Managed"
|
||||
verbose_name = "authentik Managed"
|
||||
|
||||
def ready(self) -> None:
|
||||
from authentik.managed.tasks import managed_reconcile
|
||||
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
managed_reconcile() # pylint: disable=no-value-for-parameter
|
56
authentik/managed/manager.py
Normal file
56
authentik/managed/manager.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""Managed objects manager"""
|
||||
from typing import Type
|
||||
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.managed.models import ManagedModel
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class EnsureOp:
|
||||
"""Ensure operation, executed as part of an ObjectManager run"""
|
||||
|
||||
_obj: Type[ManagedModel]
|
||||
_managed_uid: str
|
||||
_kwargs: dict
|
||||
|
||||
def __init__(self, obj: Type[ManagedModel], managed_uid: str, **kwargs) -> None:
|
||||
self._obj = obj
|
||||
self._managed_uid = managed_uid
|
||||
self._kwargs = kwargs
|
||||
|
||||
def run(self):
|
||||
"""Do the actual ensure action"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class EnsureExists(EnsureOp):
|
||||
"""Ensure object exists, with kwargs as given values"""
|
||||
|
||||
def run(self):
|
||||
self._kwargs.setdefault("managed", self._managed_uid)
|
||||
self._obj.objects.update_or_create(
|
||||
**{
|
||||
"managed": self._managed_uid,
|
||||
"defaults": self._kwargs,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ObjectManager:
|
||||
"""Base class for Apps Object manager"""
|
||||
|
||||
def run(self):
|
||||
"""Main entrypoint for tasks, iterate through all implementation of this
|
||||
and execute all operations"""
|
||||
for sub in ObjectManager.__subclasses__():
|
||||
sub_inst = sub()
|
||||
ops = sub_inst.reconcile()
|
||||
LOGGER.debug("Reconciling managed objects", manager=sub.__name__)
|
||||
for operation in ops:
|
||||
operation.run()
|
||||
|
||||
def reconcile(self) -> list[EnsureOp]:
|
||||
"""Method which is implemented in subclass that returns a list of Operations"""
|
||||
raise NotImplementedError
|
31
authentik/managed/models.py
Normal file
31
authentik/managed/models.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""Managed Object models"""
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ManagedModel(models.Model):
|
||||
"""Model which can be managed by authentik exclusively"""
|
||||
|
||||
managed = models.TextField(
|
||||
default=None,
|
||||
null=True,
|
||||
verbose_name=_("Managed by authentik"),
|
||||
help_text=_(
|
||||
(
|
||||
"Objects which are managed by authentik. These objects are created and updated "
|
||||
"automatically. This is flag only indicates that an object can be overwritten by "
|
||||
"migrations. You can still modify the objects via the API, but expect changes "
|
||||
"to be overwritten in a later update."
|
||||
)
|
||||
),
|
||||
unique=True,
|
||||
)
|
||||
|
||||
def managed_objects(self) -> QuerySet:
|
||||
"""Get all objects which are managed"""
|
||||
return self.objects.exclude(managed__isnull=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
10
authentik/managed/settings.py
Normal file
10
authentik/managed/settings.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""managed Settings"""
|
||||
from celery.schedules import crontab
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"managed_reconcile": {
|
||||
"task": "authentik.managed.tasks.managed_reconcile",
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
}
|
20
authentik/managed/tasks.py
Normal file
20
authentik/managed/tasks.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""managed tasks"""
|
||||
from django.db import DatabaseError
|
||||
|
||||
from authentik.core.tasks import CELERY_APP
|
||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.managed.manager import ObjectManager
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def managed_reconcile(self: MonitoredTask):
|
||||
"""Run ObjectManager to ensure objects are up-to-date"""
|
||||
try:
|
||||
ObjectManager().run()
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL, ["Successfully updated managed models."]
|
||||
)
|
||||
)
|
||||
except DatabaseError as exc:
|
||||
self.set_status(TaskResult(TaskResultStatus.WARNING, [str(exc)]))
|
0
authentik/outposts/api/__init__.py
Normal file
0
authentik/outposts/api/__init__.py
Normal file
@ -1,30 +1,28 @@
|
||||
"""Outpost API Views"""
|
||||
from rest_framework.serializers import JSONField, ModelSerializer
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
Outpost,
|
||||
OutpostServiceConnection,
|
||||
)
|
||||
|
||||
|
||||
class OutpostSerializer(ModelSerializer):
|
||||
"""Outpost Serializer"""
|
||||
|
||||
_config = JSONField()
|
||||
class ServiceConnectionSerializer(ModelSerializer):
|
||||
"""ServiceConnection Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Outpost
|
||||
fields = ["pk", "name", "providers", "service_connection", "_config"]
|
||||
model = OutpostServiceConnection
|
||||
fields = ["pk", "name"]
|
||||
|
||||
|
||||
class OutpostViewSet(ModelViewSet):
|
||||
"""Outpost Viewset"""
|
||||
class ServiceConnectionViewSet(ModelViewSet):
|
||||
"""ServiceConnection Viewset"""
|
||||
|
||||
queryset = Outpost.objects.all()
|
||||
serializer_class = OutpostSerializer
|
||||
queryset = OutpostServiceConnection.objects.all()
|
||||
serializer_class = ServiceConnectionSerializer
|
||||
|
||||
|
||||
class DockerServiceConnectionSerializer(ModelSerializer):
|
79
authentik/outposts/api/outposts.py
Normal file
79
authentik/outposts/api/outposts.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""Outpost API Views"""
|
||||
from django.db.models import Model
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, CharField, DateTimeField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import JSONField, ModelSerializer, Serializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.outposts.models import Outpost
|
||||
|
||||
|
||||
class OutpostSerializer(ModelSerializer):
|
||||
"""Outpost Serializer"""
|
||||
|
||||
_config = JSONField()
|
||||
providers_obj = ProviderSerializer(source="providers", many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Outpost
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"providers",
|
||||
"providers_obj",
|
||||
"service_connection",
|
||||
"token_identifier",
|
||||
"_config",
|
||||
]
|
||||
|
||||
|
||||
class OutpostHealthSerializer(Serializer):
|
||||
"""Outpost health status"""
|
||||
|
||||
last_seen = DateTimeField(read_only=True)
|
||||
version = CharField(read_only=True)
|
||||
version_should = CharField(read_only=True)
|
||||
version_outdated = BooleanField(read_only=True)
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class OutpostViewSet(ModelViewSet):
|
||||
"""Outpost Viewset"""
|
||||
|
||||
queryset = Outpost.objects.all()
|
||||
serializer_class = OutpostSerializer
|
||||
filterset_fields = {
|
||||
"providers": ["isnull"],
|
||||
}
|
||||
search_fields = [
|
||||
"name",
|
||||
"providers__name",
|
||||
]
|
||||
|
||||
@swagger_auto_schema(responses={200: OutpostHealthSerializer(many=True)})
|
||||
@action(methods=["GET"], detail=True)
|
||||
# pylint: disable=invalid-name, unused-argument
|
||||
def health(self, request: Request, pk: int) -> Response:
|
||||
"""Get outposts current health"""
|
||||
outpost: Outpost = self.get_object()
|
||||
states = []
|
||||
for state in outpost.state:
|
||||
states.append(
|
||||
{
|
||||
"last_seen": state.last_seen,
|
||||
"version": state.version,
|
||||
"version_should": state.version_should,
|
||||
"version_outdated": state.version_outdated,
|
||||
}
|
||||
)
|
||||
return Response(OutpostHealthSerializer(states, many=True).data)
|
@ -9,7 +9,6 @@ from django.core.cache import cache
|
||||
from django.db import models, transaction
|
||||
from django.db.models.base import Model
|
||||
from django.forms.models import ModelForm
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from docker.client import DockerClient
|
||||
from docker.errors import DockerException
|
||||
@ -33,7 +32,6 @@ from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.models import InheritanceForeignKey
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.template import render_to_string
|
||||
from authentik.outposts.docker_tls import DockerInlineTLS
|
||||
|
||||
OUR_VERSION = parse(__version__)
|
||||
@ -363,6 +361,7 @@ class Outpost(models.Model):
|
||||
intent=TokenIntents.INTENT_API,
|
||||
description=f"Autogenerated by authentik for Outpost {self.name}",
|
||||
expiring=False,
|
||||
managed="goauthentik.io/outpost",
|
||||
)
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model]:
|
||||
@ -377,13 +376,6 @@ class Outpost(models.Model):
|
||||
objects.append(provider)
|
||||
return objects
|
||||
|
||||
def html_deployment_view(self, request: HttpRequest) -> Optional[str]:
|
||||
"""return template and context modal to view token and other config info"""
|
||||
return render_to_string(
|
||||
"outposts/deployment_modal.html",
|
||||
{"outpost": self, "full_url": request.build_absolute_uri("/")},
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Outpost {self.name}"
|
||||
|
||||
|
@ -20,6 +20,7 @@ UPDATE_TRIGGERING_MODELS = (
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_update(sender, instance: Model, **_):
|
||||
"""If an Outpost is saved, Ensure that token is created/updated
|
||||
|
||||
@ -29,7 +30,7 @@ def post_save_update(sender, instance: Model, **_):
|
||||
return
|
||||
if instance.__module__ == "__fake__":
|
||||
return
|
||||
if sender not in UPDATE_TRIGGERING_MODELS:
|
||||
if not isinstance(instance, UPDATE_TRIGGERING_MODELS):
|
||||
return
|
||||
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
|
||||
|
||||
|
@ -1,43 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
<ak-modal-button>
|
||||
<button slot="trigger" class="pf-c-button pf-m-tertiary">
|
||||
{% trans 'View Deployment Info' %}
|
||||
</button>
|
||||
<div slot="modal">
|
||||
<div class="pf-c-modal-box__header">
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Outpost Deployment Info' %}</h1>
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body" id="modal-description">
|
||||
<p><a href="https://goauthentik.io/docs/outposts/outposts/#deploy">{% trans 'View deployment documentation' %}</a></p>
|
||||
<form class="pf-c-form">
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||
<span class="pf-c-form__label-text">AUTHENTIK_HOST</span>
|
||||
</label>
|
||||
<input class="pf-c-form-control" readonly type="text" value="{{ full_url }}" />
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||
<span class="pf-c-form__label-text">AUTHENTIK_TOKEN</span>
|
||||
</label>
|
||||
<div>
|
||||
<ak-token-copy-button identifier="{{ outpost.token_identifier }}">
|
||||
{% trans 'Click to copy token' %}
|
||||
</ak-token-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
<h3>{% trans 'If your authentik Instance is using a self-signed certificate, set this value.' %}</h3>
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||
<span class="pf-c-form__label-text">AUTHENTIK_INSECURE</span>
|
||||
</label>
|
||||
<input class="pf-c-form-control" readonly type="text" value="true" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-modal-box__footer pf-m-align-left">
|
||||
<a class="pf-c-button pf-m-primary">{% trans 'Close' %}</a>
|
||||
</footer>
|
||||
</div>
|
||||
</ak-modal-button>
|
@ -8,14 +8,13 @@ from django.core.cache import cache
|
||||
from django.http import HttpRequest
|
||||
from sentry_sdk.hub import Hub
|
||||
from sentry_sdk.tracing import Span
|
||||
from structlog.stdlib import get_logger
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
||||
from authentik.policies.process import PolicyProcess, cache_key
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
CURRENT_PROCESS = current_process()
|
||||
|
||||
|
||||
@ -49,6 +48,7 @@ class PolicyEngine:
|
||||
use_cache: bool
|
||||
request: PolicyRequest
|
||||
|
||||
logger: BoundLogger
|
||||
mode: PolicyEngineMode
|
||||
# Allow objects with no policies attached to pass
|
||||
empty_result: bool
|
||||
@ -62,6 +62,7 @@ class PolicyEngine:
|
||||
def __init__(
|
||||
self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
|
||||
):
|
||||
self.logger = get_logger().bind()
|
||||
self.mode = PolicyEngineMode.MODE_AND
|
||||
# For backwards compatibility, set empty_result to true
|
||||
# objects with no policies attached will pass.
|
||||
@ -105,18 +106,18 @@ class PolicyEngine:
|
||||
key = cache_key(binding, self.request)
|
||||
cached_policy = cache.get(key, None)
|
||||
if cached_policy and self.use_cache:
|
||||
LOGGER.debug(
|
||||
self.logger.debug(
|
||||
"P_ENG: Taking result from cache",
|
||||
policy=binding.policy,
|
||||
cache_key=key,
|
||||
)
|
||||
self.__cached_policies.append(cached_policy)
|
||||
continue
|
||||
LOGGER.debug("P_ENG: Evaluating policy", policy=binding.policy)
|
||||
self.logger.debug("P_ENG: Evaluating policy", policy=binding.policy)
|
||||
our_end, task_end = Pipe(False)
|
||||
task = PolicyProcess(binding, self.request, task_end)
|
||||
task.daemon = False
|
||||
LOGGER.debug("P_ENG: Starting Process", policy=binding.policy)
|
||||
self.logger.debug("P_ENG: Starting Process", policy=binding.policy)
|
||||
if not CURRENT_PROCESS._config.get("daemon"):
|
||||
task.run()
|
||||
else:
|
||||
|
@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-02 18:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_event_matcher", "0004_auto_20210112_2158"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventmatcherpolicy",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("user_write", "User Write"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("token_view", "Token View"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("policy_execution", "Policy Execution"),
|
||||
("policy_exception", "Policy Exception"),
|
||||
("property_mapping_exception", "Property Mapping Exception"),
|
||||
("system_task_execution", "System Task Execution"),
|
||||
("system_task_exception", "System Task Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("update_available", "Update Available"),
|
||||
("custom_", "Custom Prefix"),
|
||||
],
|
||||
help_text="Match created events with this action type. When left empty, all action types will be matched.",
|
||||
),
|
||||
),
|
||||
]
|
@ -0,0 +1,73 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-03 11:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_event_matcher", "0005_auto_20210202_1821"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventmatcherpolicy",
|
||||
name="app",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("authentik.admin", "authentik Admin"),
|
||||
("authentik.api", "authentik API"),
|
||||
("authentik.events", "authentik Events"),
|
||||
("authentik.crypto", "authentik Crypto"),
|
||||
("authentik.flows", "authentik Flows"),
|
||||
("authentik.outposts", "authentik Outpost"),
|
||||
("authentik.lib", "authentik lib"),
|
||||
("authentik.policies", "authentik Policies"),
|
||||
("authentik.policies.dummy", "authentik Policies.Dummy"),
|
||||
(
|
||||
"authentik.policies.event_matcher",
|
||||
"authentik Policies.Event Matcher",
|
||||
),
|
||||
("authentik.policies.expiry", "authentik Policies.Expiry"),
|
||||
("authentik.policies.expression", "authentik Policies.Expression"),
|
||||
(
|
||||
"authentik.policies.group_membership",
|
||||
"authentik Policies.Group Membership",
|
||||
),
|
||||
("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
|
||||
("authentik.policies.password", "authentik Policies.Password"),
|
||||
("authentik.policies.reputation", "authentik Policies.Reputation"),
|
||||
("authentik.providers.proxy", "authentik Providers.Proxy"),
|
||||
("authentik.providers.oauth2", "authentik Providers.OAuth2"),
|
||||
("authentik.providers.saml", "authentik Providers.SAML"),
|
||||
("authentik.recovery", "authentik Recovery"),
|
||||
("authentik.sources.ldap", "authentik Sources.LDAP"),
|
||||
("authentik.sources.oauth", "authentik Sources.OAuth"),
|
||||
("authentik.sources.saml", "authentik Sources.SAML"),
|
||||
("authentik.stages.captcha", "authentik Stages.Captcha"),
|
||||
("authentik.stages.consent", "authentik Stages.Consent"),
|
||||
("authentik.stages.dummy", "authentik Stages.Dummy"),
|
||||
("authentik.stages.email", "authentik Stages.Email"),
|
||||
("authentik.stages.prompt", "authentik Stages.Prompt"),
|
||||
(
|
||||
"authentik.stages.identification",
|
||||
"authentik Stages.Identification",
|
||||
),
|
||||
("authentik.stages.invitation", "authentik Stages.User Invitation"),
|
||||
("authentik.stages.user_delete", "authentik Stages.User Delete"),
|
||||
("authentik.stages.user_login", "authentik Stages.User Login"),
|
||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
|
||||
("authentik.stages.otp_time", "authentik Stages.OTP.Time"),
|
||||
("authentik.stages.otp_validate", "authentik Stages.OTP.Validate"),
|
||||
("authentik.stages.password", "authentik Stages.Password"),
|
||||
("authentik.managed", "authentik Managed"),
|
||||
("authentik.core", "authentik Core"),
|
||||
],
|
||||
default="",
|
||||
help_text="Match events created by selected application. When left empty, all applications are matched.",
|
||||
),
|
||||
),
|
||||
]
|
@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-09 16:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_event_matcher", "0006_auto_20210203_1134"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventmatcherpolicy",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("user_write", "User Write"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("secret_view", "Secret View"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("policy_execution", "Policy Execution"),
|
||||
("policy_exception", "Policy Exception"),
|
||||
("property_mapping_exception", "Property Mapping Exception"),
|
||||
("system_task_execution", "System Task Execution"),
|
||||
("system_task_exception", "System Task Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("update_available", "Update Available"),
|
||||
("custom_", "Custom Prefix"),
|
||||
],
|
||||
help_text="Match created events with this action type. When left empty, all action types will be matched.",
|
||||
),
|
||||
),
|
||||
]
|
@ -55,10 +55,6 @@ class PolicyEvaluator(BaseEvaluator):
|
||||
|
||||
def handle_error(self, exc: Exception, expression_source: str):
|
||||
"""Exception Handler"""
|
||||
# So, this is a bit questionable. Essentially, we are edit the stacktrace
|
||||
# so the user only sees information relevant to them
|
||||
# and none of our surrounding error handling
|
||||
exc.__traceback__ = exc.__traceback__.tb_next
|
||||
raise PolicyException(exc)
|
||||
|
||||
def evaluate(self, expression_source: str) -> PolicyResult:
|
||||
|
@ -17,6 +17,9 @@ from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
LOGGER = get_logger()
|
||||
TRACEBACK_HEADER = "Traceback (most recent call last):\n"
|
||||
|
||||
FORK_CTX = get_context("fork")
|
||||
PROCESS_CLASS = FORK_CTX.Process
|
||||
|
||||
|
||||
def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str:
|
||||
"""Generate Cache key for policy"""
|
||||
@ -28,10 +31,6 @@ def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str:
|
||||
return prefix
|
||||
|
||||
|
||||
FORK_CTX = get_context("fork")
|
||||
PROCESS_CLASS = FORK_CTX.Process
|
||||
|
||||
|
||||
class PolicyProcess(PROCESS_CLASS):
|
||||
"""Evaluate a single policy within a seprate process"""
|
||||
|
||||
@ -81,7 +80,7 @@ class PolicyProcess(PROCESS_CLASS):
|
||||
)
|
||||
try:
|
||||
policy_result = self.binding.policy.passes(self.request)
|
||||
if self.binding.policy.execution_logging:
|
||||
if self.binding.policy.execution_logging and not self.request.debug:
|
||||
self.create_event(
|
||||
EventAction.POLICY_EXECUTION,
|
||||
message="Policy Execution",
|
||||
@ -95,25 +94,26 @@ class PolicyProcess(PROCESS_CLASS):
|
||||
+ "".join(format_tb(src_exc.__traceback__))
|
||||
+ str(src_exc)
|
||||
)
|
||||
# Create policy exception event
|
||||
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
||||
# Create policy exception event, only when we're not debugging
|
||||
if not self.request.debug:
|
||||
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
||||
LOGGER.debug("P_ENG(proc): error", exc=src_exc)
|
||||
policy_result = PolicyResult(False, str(src_exc))
|
||||
policy_result.source_policy = self.binding.policy
|
||||
# Invert result if policy.negate is set
|
||||
if self.binding.negate:
|
||||
policy_result.passing = not policy_result.passing
|
||||
if not self.request.debug:
|
||||
key = cache_key(self.binding, self.request)
|
||||
cache.set(key, policy_result)
|
||||
LOGGER.debug(
|
||||
"P_ENG(proc): Finished",
|
||||
"P_ENG(proc): finished and cached ",
|
||||
policy=self.binding.policy,
|
||||
result=policy_result,
|
||||
process="PolicyProcess",
|
||||
passing=policy_result.passing,
|
||||
user=self.request.user,
|
||||
)
|
||||
key = cache_key(self.binding, self.request)
|
||||
cache.set(key, policy_result)
|
||||
LOGGER.debug("P_ENG(proc): Cached policy evaluation", key=key)
|
||||
return policy_result
|
||||
|
||||
def run(self): # pragma: no cover
|
||||
|
@ -26,5 +26,5 @@ def invalidate_policy_cache(sender, instance, **_):
|
||||
cache.delete_many(keys)
|
||||
LOGGER.debug("Invalidating policy cache", policy=instance, keys=total)
|
||||
# Also delete user application cache
|
||||
keys = user_app_cache_key("*")
|
||||
keys = cache.keys(user_app_cache_key("*"))
|
||||
cache.delete_many(keys)
|
||||
|
@ -20,6 +20,7 @@ class PolicyRequest:
|
||||
http_request: Optional[HttpRequest]
|
||||
obj: Optional[Model]
|
||||
context: dict[str, Any]
|
||||
debug: bool = False
|
||||
|
||||
def __init__(self, user: User):
|
||||
super().__init__()
|
||||
|
@ -1,20 +1,27 @@
|
||||
"""OAuth2Provider API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from django.shortcuts import reverse
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, Serializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.models import Provider
|
||||
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
||||
|
||||
|
||||
class OAuth2ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
class OAuth2ProviderSerializer(ProviderSerializer):
|
||||
"""OAuth2Provider Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OAuth2Provider
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
fields = ProviderSerializer.Meta.fields + [
|
||||
"authorization_flow",
|
||||
"client_type",
|
||||
"client_id",
|
||||
@ -27,25 +34,83 @@ class OAuth2ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"sub_mode",
|
||||
"property_mappings",
|
||||
"issuer_mode",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
class OAuth2ProviderSetupURLs(Serializer):
|
||||
"""OAuth2 Provider Metadata serializer"""
|
||||
|
||||
issuer = ReadOnlyField()
|
||||
authorize = ReadOnlyField()
|
||||
token = ReadOnlyField()
|
||||
user_info = ReadOnlyField()
|
||||
provider_info = ReadOnlyField()
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class OAuth2ProviderViewSet(ModelViewSet):
|
||||
"""OAuth2Provider Viewset"""
|
||||
|
||||
queryset = OAuth2Provider.objects.all()
|
||||
serializer_class = OAuth2ProviderSerializer
|
||||
|
||||
@swagger_auto_schema(responses={200: OAuth2ProviderSetupURLs(many=False)})
|
||||
@action(methods=["GET"], detail=True)
|
||||
# pylint: disable=invalid-name
|
||||
def setup_urls(self, request: Request, pk: int) -> str:
|
||||
"""Get Providers setup URLs"""
|
||||
provider = get_object_or_404(OAuth2Provider, pk=pk)
|
||||
data = {
|
||||
"issuer": provider.get_issuer(request),
|
||||
"authorize": request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_oauth2:authorize",
|
||||
)
|
||||
),
|
||||
"token": request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_oauth2:token",
|
||||
)
|
||||
),
|
||||
"user_info": request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_oauth2:userinfo",
|
||||
)
|
||||
),
|
||||
"provider_info": None,
|
||||
}
|
||||
try:
|
||||
data["provider_info"] = request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_oauth2:provider-info",
|
||||
kwargs={"application_slug": provider.application.slug},
|
||||
)
|
||||
)
|
||||
except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member
|
||||
pass
|
||||
return Response(data)
|
||||
|
||||
class ScopeMappingSerializer(ModelSerializer):
|
||||
|
||||
class ScopeMappingSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""ScopeMapping Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ScopeMapping
|
||||
fields = ["pk", "name", "scope_name", "description", "expression"]
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"scope_name",
|
||||
"description",
|
||||
"expression",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
class ScopeMappingViewSet(ModelViewSet):
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""authentik auth oauth provider app config"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
@ -12,3 +14,6 @@ class AuthentikProviderOAuth2Config(AppConfig):
|
||||
"authentik.providers.oauth2.urls": "application/o/",
|
||||
"authentik.providers.oauth2.urls_github": "",
|
||||
}
|
||||
|
||||
def ready(self) -> None:
|
||||
import_module("authentik.providers.oauth2.managed")
|
||||
|
@ -23,11 +23,12 @@ class OAuth2Error(SentryIgnoredException):
|
||||
def __repr__(self) -> str:
|
||||
return self.error
|
||||
|
||||
def to_event(self, message: Optional[str] = None) -> Event:
|
||||
def to_event(self, message: Optional[str] = None, **kwargs) -> Event:
|
||||
"""Create configuration_error Event and save it."""
|
||||
return Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=message or self.description,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@ -49,10 +50,11 @@ class RedirectUriError(OAuth2Error):
|
||||
self.provided_uri = provided_uri
|
||||
self.allowed_uris = allowed_uris
|
||||
|
||||
def to_event(self) -> Event:
|
||||
def to_event(self, **kwargs) -> Event:
|
||||
return super().to_event(
|
||||
f"Invalid redirect URI was used. Client used '{self.provided_uri}'. "
|
||||
f"Allowed redirect URIs are {','.join(self.allowed_uris)}"
|
||||
f"Allowed redirect URIs are {','.join(self.allowed_uris)}",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@ -68,8 +70,10 @@ class ClientIdError(OAuth2Error):
|
||||
super().__init__()
|
||||
self.client_id = client_id
|
||||
|
||||
def to_event(self) -> Event:
|
||||
return super().to_event(f"Invalid client identifier: {self.client_id}.")
|
||||
def to_event(self, **kwargs) -> Event:
|
||||
return super().to_event(
|
||||
f"Invalid client identifier: {self.client_id}.", **kwargs
|
||||
)
|
||||
|
||||
|
||||
class UserAuthError(OAuth2Error):
|
||||
|
60
authentik/providers/oauth2/managed.py
Normal file
60
authentik/providers/oauth2/managed.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""OAuth2 Provider managed objects"""
|
||||
from authentik.managed.manager import EnsureExists, ObjectManager
|
||||
from authentik.providers.oauth2.models import ScopeMapping
|
||||
|
||||
SCOPE_OPENID_EXPRESSION = """
|
||||
# This scope is required by the OpenID-spec, and must as such exist in authentik.
|
||||
# The scope by itself does not grant any information
|
||||
return {}
|
||||
"""
|
||||
SCOPE_EMAIL_EXPRESSION = """
|
||||
return {
|
||||
"email": user.email,
|
||||
"email_verified": True
|
||||
}
|
||||
"""
|
||||
SCOPE_PROFILE_EXPRESSION = """
|
||||
return {
|
||||
# Because authentik only saves the user's full name, and has no concept of first and last names,
|
||||
# the full name is used as given name.
|
||||
# You can override this behaviour in custom mappings, i.e. `user.name.split(" ")`
|
||||
"name": user.name,
|
||||
"given_name": user.name,
|
||||
"family_name": "",
|
||||
"preferred_username": user.username,
|
||||
"nickname": user.username,
|
||||
# groups is not part of the official userinfo schema, but is a quasi-standard
|
||||
"groups": [group.name for group in user.ak_groups.all()],
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class ScopeMappingManager(ObjectManager):
|
||||
"""OAuth2 Provider managed objects"""
|
||||
|
||||
def reconcile(self):
|
||||
return [
|
||||
EnsureExists(
|
||||
ScopeMapping,
|
||||
"goauthentik.io/providers/oauth2/scope-openid",
|
||||
name="authentik default OAuth Mapping: OpenID 'openid'",
|
||||
scope_name="openid",
|
||||
expression=SCOPE_OPENID_EXPRESSION,
|
||||
),
|
||||
EnsureExists(
|
||||
ScopeMapping,
|
||||
"goauthentik.io/providers/oauth2/scope-email",
|
||||
name="authentik default OAuth Mapping: OpenID 'email'",
|
||||
scope_name="email",
|
||||
description="Email address",
|
||||
expression=SCOPE_EMAIL_EXPRESSION,
|
||||
),
|
||||
EnsureExists(
|
||||
ScopeMapping,
|
||||
"goauthentik.io/providers/oauth2/scope-profile",
|
||||
name="authentik default OAuth Mapping: OpenID 'profile'",
|
||||
scope_name="profile",
|
||||
description="General Profile Information",
|
||||
expression=SCOPE_PROFILE_EXPRESSION,
|
||||
),
|
||||
]
|
@ -10,54 +10,6 @@ import authentik.core.models
|
||||
import authentik.lib.utils.time
|
||||
import authentik.providers.oauth2.generators
|
||||
|
||||
SCOPE_OPENID_EXPRESSION = """# This is only required for OpenID Applications, but does not grant any information by itself.
|
||||
return {}
|
||||
"""
|
||||
SCOPE_EMAIL_EXPRESSION = """return {
|
||||
"email": user.email,
|
||||
"email_verified": True
|
||||
}
|
||||
"""
|
||||
SCOPE_PROFILE_EXPRESSION = """return {
|
||||
"name": user.name,
|
||||
"given_name": user.name,
|
||||
"family_name": "",
|
||||
"preferred_username": user.username,
|
||||
"nickname": user.username,
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def create_default_scopes(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping")
|
||||
ScopeMapping.objects.update_or_create(
|
||||
scope_name="openid",
|
||||
defaults={
|
||||
"name": "Autogenerated OAuth2 Mapping: OpenID 'openid'",
|
||||
"scope_name": "openid",
|
||||
"description": "",
|
||||
"expression": SCOPE_OPENID_EXPRESSION,
|
||||
},
|
||||
)
|
||||
ScopeMapping.objects.update_or_create(
|
||||
scope_name="email",
|
||||
defaults={
|
||||
"name": "Autogenerated OAuth2 Mapping: OpenID 'email'",
|
||||
"scope_name": "email",
|
||||
"description": "Email address",
|
||||
"expression": SCOPE_EMAIL_EXPRESSION,
|
||||
},
|
||||
)
|
||||
ScopeMapping.objects.update_or_create(
|
||||
scope_name="profile",
|
||||
defaults={
|
||||
"name": "Autogenerated OAuth2 Mapping: OpenID 'profile'",
|
||||
"scope_name": "profile",
|
||||
"description": "General Profile Information",
|
||||
"expression": SCOPE_PROFILE_EXPRESSION,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@ -235,7 +187,6 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
bases=("authentik_core.propertymapping",),
|
||||
),
|
||||
migrations.RunPython(create_default_scopes),
|
||||
migrations.CreateModel(
|
||||
name="RefreshToken",
|
||||
fields=[
|
||||
|
33
authentik/providers/oauth2/migrations/0011_managed.py
Normal file
33
authentik/providers/oauth2/migrations/0011_managed.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-03 09:24
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
|
||||
scope_uid_map = {
|
||||
"openid": "goauthentik.io/providers/oauth2/scope-openid",
|
||||
"email": "goauthentik.io/providers/oauth2/scope-email",
|
||||
"profile": "goauthentik.io/providers/oauth2/scope-profile",
|
||||
"ak_proxy": "goauthentik.io/providers/proxy/scope-proxy",
|
||||
}
|
||||
|
||||
|
||||
def set_managed_flag(apps: Apps, schema_editor):
|
||||
ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping")
|
||||
db_alias = schema_editor.connection.alias
|
||||
for mapping in ScopeMapping.objects.using(db_alias).filter(
|
||||
name__startswith="Autogenerated "
|
||||
):
|
||||
mapping.managed = scope_uid_map[mapping.scope_name]
|
||||
mapping.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0017_managed"),
|
||||
("authentik_providers_oauth2", "0010_auto_20201227_1804"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_managed_flag),
|
||||
]
|
@ -14,7 +14,6 @@ from django.conf import settings
|
||||
from django.db import models
|
||||
from django.forms import ModelForm
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import reverse
|
||||
from django.utils import dateformat, timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key
|
||||
@ -25,7 +24,6 @@ from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import get_user
|
||||
from authentik.lib.utils.template import render_to_string
|
||||
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
||||
from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config
|
||||
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
||||
@ -118,6 +116,12 @@ class ScopeMapping(PropertyMapping):
|
||||
|
||||
return ScopeMappingForm
|
||||
|
||||
@property
|
||||
def serializer(self) -> Type[Serializer]:
|
||||
from authentik.providers.oauth2.api import ScopeMappingSerializer
|
||||
|
||||
return ScopeMappingSerializer
|
||||
|
||||
def __str__(self):
|
||||
return f"Scope Mapping {self.name} ({self.scope_name})"
|
||||
|
||||
@ -303,41 +307,6 @@ class OAuth2Provider(Provider):
|
||||
jws = JWS(payload, alg=self.jwt_alg)
|
||||
return jws.sign_compact(keys)
|
||||
|
||||
def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
|
||||
"""return template and context modal with URLs for authorize, token, openid-config, etc"""
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
return render_to_string(
|
||||
"providers/oauth2/setup_url_modal.html",
|
||||
{
|
||||
"provider": self,
|
||||
"issuer": self.get_issuer(request),
|
||||
"authorize": request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_oauth2:authorize",
|
||||
)
|
||||
),
|
||||
"token": request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_oauth2:token",
|
||||
)
|
||||
),
|
||||
"userinfo": request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_oauth2:userinfo",
|
||||
)
|
||||
),
|
||||
"provider_info": request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_oauth2:provider-info",
|
||||
kwargs={"application_slug": self.application.slug},
|
||||
)
|
||||
),
|
||||
},
|
||||
)
|
||||
except Provider.application.RelatedObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("OAuth2/OpenID Provider")
|
||||
|
@ -1,50 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
<ak-modal-button>
|
||||
<button slot="trigger" class="pf-c-button pf-m-tertiary">
|
||||
{% trans 'View Setup URLs' %}
|
||||
</button>
|
||||
<div slot="modal">
|
||||
<div class="pf-c-modal-box__header">
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Setup URLs' %}</h1>
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body" id="modal-description">
|
||||
<form class="pf-c-form">
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||
<span class="pf-c-form__label-text">{% trans 'OpenID Configuration URL' %}</span>
|
||||
</label>
|
||||
<input class="pf-c-form-control" readonly type="text" value="{{ provider_info }}" />
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||
<span class="pf-c-form__label-text">{% trans 'OpenID Configuration Issuer' %}</span>
|
||||
</label>
|
||||
<input class="pf-c-form-control" readonly type="text" value="{{ issuer }}" />
|
||||
</div>
|
||||
<hr>
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||
<span class="pf-c-form__label-text">{% trans 'Authorize URL' %}</span>
|
||||
</label>
|
||||
<input class="pf-c-form-control" readonly type="text" value="{{ authorize }}" />
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||
<span class="pf-c-form__label-text">{% trans 'Token URL' %}</span>
|
||||
</label>
|
||||
<input class="pf-c-form-control" readonly type="text" value="{{ token }}" />
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||
<span class="pf-c-form__label-text">{% trans 'Userinfo Endpoint' %}</span>
|
||||
</label>
|
||||
<input class="pf-c-form-control" readonly type="text" value="{{ userinfo }}" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-modal-box__footer pf-m-align-left">
|
||||
<a class="pf-c-button pf-m-primary">{% trans 'Close' %}</a>
|
||||
</footer>
|
||||
</div>
|
||||
</ak-modal-button>
|
@ -253,15 +253,16 @@ class OAuthFulfillmentStage(StageView):
|
||||
EventAction.AUTHORIZE_APPLICATION,
|
||||
authorized_application=application,
|
||||
flow=self.executor.plan.flow_pk,
|
||||
scopes=", ".join(self.params.scope),
|
||||
).from_http(self.request)
|
||||
return redirect(self.create_response_uri())
|
||||
except (ClientIdError, RedirectUriError) as error:
|
||||
error.to_event().from_http(request)
|
||||
error.to_event(application=application).from_http(request)
|
||||
self.executor.stage_invalid()
|
||||
# pylint: disable=no-member
|
||||
return bad_request_message(request, error.description, title=error.error)
|
||||
except AuthorizeError as error:
|
||||
error.to_event().from_http(request)
|
||||
error.to_event(application=application).from_http(request)
|
||||
self.executor.stage_invalid()
|
||||
return redirect(error.create_uri())
|
||||
|
||||
@ -379,7 +380,7 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||
try:
|
||||
self.params = OAuthAuthorizationParams.from_request(self.request)
|
||||
except AuthorizeError as error:
|
||||
error.to_event().from_http(self.request)
|
||||
error.to_event(redirect_uri=error.redirect_uri).from_http(self.request)
|
||||
raise RequestValidationError(redirect(error.create_uri()))
|
||||
except OAuth2Error as error:
|
||||
error.to_event().from_http(self.request)
|
||||
@ -396,7 +397,7 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||
self.params.grant_type,
|
||||
self.params.state,
|
||||
)
|
||||
error.to_event().from_http(self.request)
|
||||
error.to_event(redirect_uri=error.redirect_uri).from_http(self.request)
|
||||
raise RequestValidationError(redirect(error.create_uri()))
|
||||
|
||||
def resolve_provider_application(self):
|
||||
|
@ -6,7 +6,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, Serializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
|
||||
@ -34,7 +34,7 @@ class OpenIDConnectConfigurationSerializer(Serializer):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ProxyProviderSerializer(MetaNameSerializer, ModelSerializer):
|
||||
class ProxyProviderSerializer(ProviderSerializer):
|
||||
"""ProxyProvider Serializer"""
|
||||
|
||||
def create(self, validated_data):
|
||||
@ -50,9 +50,7 @@ class ProxyProviderSerializer(MetaNameSerializer, ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
model = ProxyProvider
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
fields = ProviderSerializer.Meta.fields + [
|
||||
"internal_host",
|
||||
"external_host",
|
||||
"internal_host_ssl_validation",
|
||||
@ -61,8 +59,6 @@ class ProxyProviderSerializer(MetaNameSerializer, ModelSerializer):
|
||||
"basic_auth_enabled",
|
||||
"basic_auth_password_attribute",
|
||||
"basic_auth_user_attribute",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""authentik Proxy app"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
@ -8,3 +10,6 @@ class AuthentikProviderProxyConfig(AppConfig):
|
||||
name = "authentik.providers.proxy"
|
||||
label = "authentik_providers_proxy"
|
||||
verbose_name = "authentik Providers.Proxy"
|
||||
|
||||
def ready(self) -> None:
|
||||
import_module("authentik.providers.proxy.managed")
|
||||
|
@ -18,7 +18,7 @@ class ProxyProviderForm(forms.ModelForm):
|
||||
)
|
||||
self.fields["certificate"].queryset = CertificateKeyPair.objects.filter(
|
||||
key_data__isnull=False
|
||||
)
|
||||
).exclude(key_data="")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
actual_save = super().save(*args, **kwargs)
|
||||
|
28
authentik/providers/proxy/managed.py
Normal file
28
authentik/providers/proxy/managed.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""OAuth2 Provider managed objects"""
|
||||
from authentik.managed.manager import EnsureExists, ObjectManager
|
||||
from authentik.providers.oauth2.models import ScopeMapping
|
||||
from authentik.providers.proxy.models import SCOPE_AK_PROXY
|
||||
|
||||
SCOPE_AK_PROXY_EXPRESSION = """
|
||||
# This mapping is used by the authentik proxy. It passes extra user attributes,
|
||||
# which are used for example for the HTTP-Basic Authentication mapping.
|
||||
return {
|
||||
"ak_proxy": {
|
||||
"user_attributes": user.group_attributes()
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
class ProxyScopeMappingManager(ObjectManager):
|
||||
"""OAuth2 Provider managed objects"""
|
||||
|
||||
def reconcile(self):
|
||||
return [
|
||||
EnsureExists(
|
||||
ScopeMapping,
|
||||
"goauthentik.io/providers/proxy/scope-proxy",
|
||||
name="authentik default OAuth Mapping: proxy outpost",
|
||||
scope_name=SCOPE_AK_PROXY,
|
||||
expression=SCOPE_AK_PROXY_EXPRESSION,
|
||||
),
|
||||
]
|
@ -1,35 +1,5 @@
|
||||
# Generated by Django 3.1.4 on 2020-12-14 09:42
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
SCOPE_AK_PROXY_EXPRESSION = """return {
|
||||
"ak_proxy": {
|
||||
"user_attributes": user.group_attributes()
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
def create_proxy_scope(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from authentik.providers.proxy.models import SCOPE_AK_PROXY, ProxyProvider
|
||||
|
||||
ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping")
|
||||
|
||||
ScopeMapping.objects.filter(scope_name="pb_proxy").delete()
|
||||
|
||||
ScopeMapping.objects.update_or_create(
|
||||
scope_name=SCOPE_AK_PROXY,
|
||||
defaults={
|
||||
"name": "Autogenerated OAuth2 Mapping: authentik Proxy",
|
||||
"scope_name": SCOPE_AK_PROXY,
|
||||
"description": "",
|
||||
"expression": SCOPE_AK_PROXY_EXPRESSION,
|
||||
},
|
||||
)
|
||||
|
||||
for provider in ProxyProvider.objects.all():
|
||||
provider.set_oauth_defaults()
|
||||
provider.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -38,4 +8,4 @@ class Migration(migrations.Migration):
|
||||
("authentik_providers_proxy", "0009_auto_20201007_1721"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(create_proxy_scope)]
|
||||
operations = []
|
||||
|
@ -6,7 +6,6 @@ from urllib.parse import urljoin
|
||||
|
||||
from django.db import models
|
||||
from django.forms import ModelForm
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
@ -119,10 +118,6 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||
"""Use external_host as launch URL"""
|
||||
return self.external_host
|
||||
|
||||
def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
|
||||
"""Overwrite Setup URLs as they are not needed for proxy"""
|
||||
return None
|
||||
|
||||
def set_oauth_defaults(self):
|
||||
"""Ensure all OAuth2-related settings are correct"""
|
||||
self.client_type = ClientTypes.CONFIDENTIAL
|
||||
|
@ -1,20 +1,26 @@
|
||||
"""SAMLProvider API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, Serializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.views.metadata import DescriptorDownloadView
|
||||
|
||||
|
||||
class SAMLProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
class SAMLProviderSerializer(ProviderSerializer):
|
||||
"""SAMLProvider Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = SAMLProvider
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
fields = ProviderSerializer.Meta.fields + [
|
||||
"acs_url",
|
||||
"audience",
|
||||
"issuer",
|
||||
@ -27,25 +33,52 @@ class SAMLProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"signature_algorithm",
|
||||
"signing_kp",
|
||||
"verification_kp",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
class SAMLMetadataSerializer(Serializer):
|
||||
"""SAML Provider Metadata serializer"""
|
||||
|
||||
metadata = ReadOnlyField()
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SAMLProviderViewSet(ModelViewSet):
|
||||
"""SAMLProvider Viewset"""
|
||||
|
||||
queryset = SAMLProvider.objects.all()
|
||||
serializer_class = SAMLProviderSerializer
|
||||
|
||||
@swagger_auto_schema(responses={200: SAMLMetadataSerializer(many=False)})
|
||||
@action(methods=["GET"], detail=True)
|
||||
# pylint: disable=invalid-name
|
||||
def metadata(self, request: Request, pk: int) -> Response:
|
||||
"""Return metadata as XML string"""
|
||||
provider = get_object_or_404(SAMLProvider, pk=pk)
|
||||
metadata = DescriptorDownloadView.get_metadata(request, provider)
|
||||
return Response({"metadata": metadata})
|
||||
|
||||
class SAMLPropertyMappingSerializer(ModelSerializer):
|
||||
|
||||
class SAMLPropertyMappingSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""SAMLPropertyMapping Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = SAMLPropertyMapping
|
||||
fields = ["pk", "name", "saml_name", "friendly_name", "expression"]
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"saml_name",
|
||||
"friendly_name",
|
||||
"expression",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
class SAMLPropertyMappingViewSet(ModelViewSet):
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""authentik SAML IdP app config"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
@ -10,3 +11,6 @@ class AuthentikProviderSAMLConfig(AppConfig):
|
||||
label = "authentik_providers_saml"
|
||||
verbose_name = "authentik Providers.SAML"
|
||||
mountpoint = "application/saml/"
|
||||
|
||||
def ready(self) -> None:
|
||||
import_module("authentik.providers.saml.managed")
|
||||
|
74
authentik/providers/saml/managed.py
Normal file
74
authentik/providers/saml/managed.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""SAML Provider managed objects"""
|
||||
from authentik.managed.manager import EnsureExists, ObjectManager
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping
|
||||
|
||||
GROUP_EXPRESSION = """
|
||||
for group in user.ak_groups.all():
|
||||
yield group.name
|
||||
"""
|
||||
|
||||
|
||||
class SAMLProviderManager(ObjectManager):
|
||||
"""SAML Provider managed objects"""
|
||||
|
||||
def reconcile(self):
|
||||
return [
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
"goauthentik.io/providers/saml/upn",
|
||||
name="authentik default SAML Mapping: UPN",
|
||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
|
||||
expression="return user.attributes.get('upn', user.email)",
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
"goauthentik.io/providers/saml/name",
|
||||
name="authentik default SAML Mapping: Name",
|
||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
|
||||
expression="return user.name",
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
"goauthentik.io/providers/saml/email",
|
||||
name="authentik default SAML Mapping: Email",
|
||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
expression="return user.email",
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
"goauthentik.io/providers/saml/username",
|
||||
name="authentik default SAML Mapping: Username",
|
||||
saml_name="http://schemas.goauthentik.io/2021/02/saml/username",
|
||||
expression="return user.username",
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
"goauthentik.io/providers/saml/uid",
|
||||
name="authentik default SAML Mapping: User ID",
|
||||
saml_name="http://schemas.goauthentik.io/2021/02/saml/uid",
|
||||
expression="return user.pk",
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
"goauthentik.io/providers/saml/groups",
|
||||
name="authentik default SAML Mapping: Groups",
|
||||
saml_name="http://schemas.xmlsoap.org/claims/Group",
|
||||
expression=GROUP_EXPRESSION,
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
"goauthentik.io/providers/saml/ms-windowsaccountname",
|
||||
name="authentik default SAML Mapping: WindowsAccountname (Username)",
|
||||
saml_name=(
|
||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||
),
|
||||
expression="return user.username",
|
||||
friendly_name="",
|
||||
),
|
||||
]
|
@ -3,61 +3,10 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def create_default_property_mappings(apps, schema_editor):
|
||||
"""Create default SAML Property Mappings"""
|
||||
SAMLPropertyMapping = apps.get_model(
|
||||
"authentik_providers_saml", "SAMLPropertyMapping"
|
||||
)
|
||||
db_alias = schema_editor.connection.alias
|
||||
defaults = [
|
||||
{
|
||||
"FriendlyName": "eduPersonPrincipalName",
|
||||
"Name": "urn:oid:1.3.6.1.4.1.5923.1.1.1.6",
|
||||
"Expression": "return user.email",
|
||||
},
|
||||
{
|
||||
"FriendlyName": "cn",
|
||||
"Name": "http://schemas.xmlsoap.org/claims/CommonName",
|
||||
"Expression": "return user.name",
|
||||
},
|
||||
{
|
||||
"FriendlyName": "mail",
|
||||
"Name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
"Expression": "return user.email",
|
||||
},
|
||||
{
|
||||
"FriendlyName": "displayName",
|
||||
"Name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
|
||||
"Expression": "return user.username",
|
||||
},
|
||||
{
|
||||
"FriendlyName": "uid",
|
||||
"Name": "urn:oid:0.9.2342.19200300.100.1.1",
|
||||
"Expression": "return user.pk",
|
||||
},
|
||||
{
|
||||
"FriendlyName": "member-of",
|
||||
"Name": "http://schemas.xmlsoap.org/claims/Group",
|
||||
"Expression": "for group in user.ak_groups.all():\n yield group.name",
|
||||
},
|
||||
]
|
||||
for default in defaults:
|
||||
SAMLPropertyMapping.objects.using(db_alias).get_or_create(
|
||||
saml_name=default["Name"],
|
||||
friendly_name=default["FriendlyName"],
|
||||
expression=default["Expression"],
|
||||
defaults={
|
||||
"name": f"Autogenerated SAML Mapping: {default['FriendlyName']} -> {default['Expression']}"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_saml", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_default_property_mappings),
|
||||
]
|
||||
operations = []
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user