Compare commits

..

4 Commits

Author SHA1 Message Date
ad029d3e0a release: 0.14.0-rc2 2020-12-28 16:03:21 +01:00
b0bd68232d Merge branch 'master' into version-0.14 2020-12-28 16:02:46 +01:00
65355372ce Merge branch 'master' into version-0.14 2020-12-28 14:33:28 +01:00
53d9092022 release: 0.14.0-rc1 2020-12-27 22:39:05 +01:00
381 changed files with 2652 additions and 9567 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2021.2.1-rc1 current_version = 0.14.0-rc2
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -31,6 +31,6 @@ values =
[bumpversion:file:authentik/__init__.py] [bumpversion:file:authentik/__init__.py]
[bumpversion:file:outpost/pkg/version.go] [bumpversion:file:proxy/pkg/version.go]
[bumpversion:file:web/src/constants.ts] [bumpversion:file:web/src/constants.ts]

View File

@ -18,11 +18,11 @@ jobs:
- name: Building Docker Image - name: Building Docker Image
run: docker build run: docker build
--no-cache --no-cache
-t beryju/authentik:2021.2.1-rc1 -t beryju/authentik:0.14.0-rc2
-t beryju/authentik:latest -t beryju/authentik:latest
-f Dockerfile . -f Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik:2021.2.1-rc1 run: docker push beryju/authentik:0.14.0-rc2
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik:latest run: docker push beryju/authentik:latest
build-proxy: build-proxy:
@ -34,7 +34,7 @@ jobs:
go-version: "^1.15" go-version: "^1.15"
- name: prepare go api client - name: prepare go api client
run: | run: |
cd outpost cd proxy
go get -u github.com/go-swagger/go-swagger/cmd/swagger go get -u github.com/go-swagger/go-swagger/cmd/swagger
swagger generate client -f ../swagger.yaml -A authentik -t pkg/ swagger generate client -f ../swagger.yaml -A authentik -t pkg/
go build -v . go build -v .
@ -45,14 +45,14 @@ jobs:
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image - name: Building Docker Image
run: | run: |
cd outpost/ cd proxy/
docker build \ docker build \
--no-cache \ --no-cache \
-t beryju/authentik-proxy:2021.2.1-rc1 \ -t beryju/authentik-proxy:0.14.0-rc2 \
-t beryju/authentik-proxy:latest \ -t beryju/authentik-proxy:latest \
-f proxy.Dockerfile . -f Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik-proxy:2021.2.1-rc1 run: docker push beryju/authentik-proxy:0.14.0-rc2
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik-proxy:latest run: docker push beryju/authentik-proxy:latest
build-static: build-static:
@ -69,11 +69,11 @@ jobs:
cd web/ cd web/
docker build \ docker build \
--no-cache \ --no-cache \
-t beryju/authentik-static:2021.2.1-rc1 \ -t beryju/authentik-static:0.14.0-rc2 \
-t beryju/authentik-static:latest \ -t beryju/authentik-static:latest \
-f Dockerfile . -f Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik-static:2021.2.1-rc1 run: docker push beryju/authentik-static:0.14.0-rc2
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik-static:latest run: docker push beryju/authentik-static:latest
test-release: test-release:
@ -107,5 +107,5 @@ jobs:
SENTRY_PROJECT: authentik SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org SENTRY_URL: https://sentry.beryju.org
with: with:
tagName: 2021.2.1-rc1 tagName: 0.14.0-rc2
environment: beryjuorg-prod environment: beryjuorg-prod

427
Pipfile.lock generated
View File

@ -25,10 +25,10 @@
}, },
"amqp": { "amqp": {
"hashes": [ "hashes": [
"sha256:1e759a7f202d910939de6eca45c23a107f6b71111f41d1282c648e9ac3d21901", "sha256:5b9062d5c0812335c75434bf17ce33d7a20ecfedaa0733faec7379868eb4068a",
"sha256:affdd263d8b8eb3c98170b78bf83867cdb6a14901d586e00ddb65bfe2f0c4e60" "sha256:fcd5b3baeeb7fc19b3486ff6d10543099d40ae1f5c9196eae695d1cde1b2f784"
], ],
"version": "==5.0.5" "version": "==5.0.2"
}, },
"asgiref": { "asgiref": {
"hashes": [ "hashes": [
@ -74,24 +74,25 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:1a282c1cd7d5028cbb3a75d747df32162295253f55d263ac85840e264830963b" "sha256:0bb2c3159b9f5e0df50430bf06a155bd7f27f480825b6374dde807d42360a668",
"sha256:a49b3ab4bfa2f6394ba60165cfc468410797dd410f32eed47e22f61451ee986e"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.2" "version": "==1.16.43"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:7442fdbbdc841bfac7f94f92ecb807de070e32ed205743eb72d4ea27c5e8e778", "sha256:7398c900dbd4e3d61647269215396ea3e8082f494f3e7b65d9b6aca049c1d463",
"sha256:bf587b044983a91a0124cc133ff167b8528c19fbbc8f0b956d9a1ac256cad7d7" "sha256:795a67338cadb0c3a45014a6c81659da6af623a4e973812f87a6f9d9fb7712e9"
], ],
"version": "==1.20.2" "version": "==1.19.43"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [
"sha256:1d9d5f567be80f7c07d765e21b814326d78c61eb0c3a637dffc0e5d1796cb2e2", "sha256:3796e1de094f0eaca982441c92ce96c68c89cced4cd97721ab297ea4b16db90e",
"sha256:f469e29e7aa4cff64d8de4aad95ce76de8ea1125a16c68e0d93f65c3c3dc92e9" "sha256:c6b07a6ded8c78bf36730b3dc452dfff7d95f2a12a2fed856b1a0cb13ca78c61"
], ],
"version": "==4.2.1" "version": "==4.2.0"
}, },
"celery": { "celery": {
"hashes": [ "hashes": [
@ -126,7 +127,6 @@
"sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009",
"sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03",
"sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b",
"sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e",
"sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909",
"sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53",
"sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35",
@ -152,11 +152,11 @@
}, },
"channels": { "channels": {
"hashes": [ "hashes": [
"sha256:056b72e51080a517a0f33a0a30003e03833b551d75394d6636c885d4edb8188f", "sha256:74db79c9eca616be69d38013b22083ab5d3f9ccda1ab5e69096b1bb7da2d9b18",
"sha256:3f15bdd2138bb4796e76ea588a0a344b12a7964ea9b2e456f992fddb988a4317" "sha256:f50a6e79757a64c1e45e95e144a2ac5f1e99ee44a0718ab182c501f5e5abd268"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.0.3" "version": "==3.0.2"
}, },
"channels-redis": { "channels-redis": {
"hashes": [ "hashes": [
@ -265,11 +265,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:169e2e7b4839a7910b393eec127fd7cbae62e80fa55f89c6510426abf673fe5f", "sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2",
"sha256:c6c0462b8b361f8691171af1fb87eceb4442da28477e12200c40420176206ba7" "sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.1.6" "version": "==3.1.4"
}, },
"django-cors-middleware": { "django-cors-middleware": {
"hashes": [ "hashes": [
@ -351,8 +351,7 @@
}, },
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
"sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7", "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7"
"sha256:0898182b4737a7b584a2c73735d89816343369f259fea932d90dc78e35d8ac33"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.12.2" "version": "==3.12.2"
@ -397,10 +396,10 @@
}, },
"google-auth": { "google-auth": {
"hashes": [ "hashes": [
"sha256:008e23ed080674f69f9d2d7d80db4c2591b9bb307d136cea7b3bc129771d211d", "sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e",
"sha256:514e39f4190ca972200ba33876da5a8857c5665f2b4ccc36c8b8ee21228aae80" "sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8"
], ],
"version": "==1.25.0" "version": "==1.24.0"
}, },
"gunicorn": { "gunicorn": {
"hashes": [ "hashes": [
@ -412,10 +411,10 @@
}, },
"h11": { "h11": {
"hashes": [ "hashes": [
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", "sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab",
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" "sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87"
], ],
"version": "==0.12.0" "version": "==0.11.0"
}, },
"hiredis": { "hiredis": {
"hashes": [ "hashes": [
@ -487,10 +486,10 @@
}, },
"hyperlink": { "hyperlink": {
"hashes": [ "hashes": [
"sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", "sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af",
"sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4" "sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63"
], ],
"version": "==21.0.0" "version": "==20.0.1"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
@ -522,10 +521,10 @@
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
], ],
"version": "==2.11.3" "version": "==2.11.2"
}, },
"jmespath": { "jmespath": {
"hashes": [ "hashes": [
@ -558,11 +557,11 @@
}, },
"ldap3": { "ldap3": {
"hashes": [ "hashes": [
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91", "sha256:37d633e20fa360c302b1263c96fe932d40622d0119f1bddcb829b03462eeeeb7",
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57" "sha256:7c3738570766f5e5e74a56fade15470f339d5c436d821cf476ef27da0a4de8b0"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.9" "version": "==2.8.1"
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
@ -614,12 +613,8 @@
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
"sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
"sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
@ -628,39 +623,24 @@
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
"sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
"sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
"sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
"sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
"sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
], ],
"version": "==1.1.1" "version": "==1.1.1"
}, },
@ -706,11 +686,11 @@
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
], ],
"index": "pypi", "index": "pypi",
"version": "==20.9" "version": "==20.8"
}, },
"prometheus-client": { "prometheus-client": {
"hashes": [ "hashes": [
@ -721,10 +701,10 @@
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:7e966747c18ececaec785699626b771c1ba8344c8d31759a1915d6b12fad6525", "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c",
"sha256:c96b30925025a7635471dc083ffb6af0cc67482a00611bd81aeaeeeb7e5a5e12" "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"
], ],
"version": "==3.0.14" "version": "==3.0.8"
}, },
"psycopg2-binary": { "psycopg2-binary": {
"hashes": [ "hashes": [
@ -919,37 +899,29 @@
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
"sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
], ],
"version": "==2021.1" "version": "==2020.4"
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
"sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
"sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
"sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e",
"sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
"sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
"sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
"sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
"sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
"sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a",
"sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
"sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
"sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
"sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
"sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
"sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
"sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
"sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
"sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
"sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
"sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.4.1" "version": "==5.3.1"
}, },
"qrcode": { "qrcode": {
"hashes": [ "hashes": [
@ -983,11 +955,11 @@
}, },
"rsa": { "rsa": {
"hashes": [ "hashes": [
"sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4", "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa",
"sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b" "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==4.7" "version": "==4.6"
}, },
"ruamel.yaml": { "ruamel.yaml": {
"hashes": [ "hashes": [
@ -998,10 +970,10 @@
}, },
"s3transfer": { "s3transfer": {
"hashes": [ "hashes": [
"sha256:1e28620e5b444652ed752cf87c7e0cb15b0e578972568c6609f0f18212f259ed", "sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13",
"sha256:7fdddb4f22275cf1d32129e21f056337fd2a80b6ccef1664528145b72c49e6d2" "sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db"
], ],
"version": "==0.3.4" "version": "==0.3.3"
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
@ -1035,11 +1007,11 @@
}, },
"structlog": { "structlog": {
"hashes": [ "hashes": [
"sha256:33dd6bd5f49355e52c1c61bb6a4f20d0b48ce0328cc4a45fe872d38b97a05ccd", "sha256:7a48375db6274ed1d0ae6123c486472aa1d0890b08d314d2b016f3aa7f35990b",
"sha256:af79dfa547d104af8d60f86eac12fb54825f54a46bc998e4504ef66177103174" "sha256:8a672be150547a93d90a7d74229a29e765be05bd156a35cdcc527ebf68e9af92"
], ],
"index": "pypi", "index": "pypi",
"version": "==20.2.0" "version": "==20.1.0"
}, },
"swagger-spec-validator": { "swagger-spec-validator": {
"hashes": [ "hashes": [
@ -1099,22 +1071,23 @@
"secure" "secure"
], ],
"hashes": [ "hashes": [
"sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
"sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.26.3" "markers": null,
"version": "==1.26.2"
}, },
"uvicorn": { "uvicorn": {
"extras": [ "extras": [
"standard" "standard"
], ],
"hashes": [ "hashes": [
"sha256:1079c50a06f6338095b4f203e7861dbff318dde5f22f3a324fc6e94c7654164c", "sha256:6707fa7f4dbd86fd6982a2d4ecdaad2704e4514d23a1e4278104311288b04691",
"sha256:ef1e0bb5f7941c6fe324e06443ddac0331e1632a776175f87891c7bd02694355" "sha256:d19ca083bebd212843e01f689900e5c637a292c63bb336c7f0735a99300a5f38"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.13.3" "version": "==0.13.2"
}, },
"uvloop": { "uvloop": {
"hashes": [ "hashes": [
@ -1293,11 +1266,10 @@
}, },
"autopep8": { "autopep8": {
"hashes": [ "hashes": [
"sha256:9e136c472c475f4ee4978b51a88a494bfcd4e3ed17950a44a988d9e434837bea", "sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094"
"sha256:cae4bc0fb616408191af41d062d7ec7ef8679c7f27b068875ca3a9e2878d5443"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.5.5" "version": "==1.5.4"
}, },
"bandit": { "bandit": {
"hashes": [ "hashes": [
@ -1346,66 +1318,66 @@
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7", "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297",
"sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5", "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1",
"sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f", "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497",
"sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde", "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606",
"sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f", "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528",
"sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f", "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b",
"sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c", "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4",
"sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66", "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830",
"sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90", "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1",
"sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337", "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f",
"sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d", "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d",
"sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4", "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3",
"sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409", "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8",
"sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37", "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500",
"sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1", "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7",
"sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247", "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb",
"sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39", "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b",
"sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c", "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059",
"sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994", "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b",
"sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c", "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72",
"sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb", "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36",
"sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc", "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277",
"sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f", "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c",
"sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca", "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631",
"sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135", "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff",
"sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3", "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8",
"sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339", "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec",
"sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9", "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b",
"sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9", "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7",
"sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af", "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105",
"sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370", "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b",
"sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19", "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c",
"sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3", "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b",
"sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44", "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98",
"sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3", "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4",
"sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a", "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879",
"sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c", "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f",
"sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b", "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4",
"sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9", "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044",
"sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8", "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e",
"sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22", "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899",
"sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f", "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f",
"sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345", "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448",
"sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880", "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714",
"sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0", "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2",
"sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b", "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d",
"sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec", "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd",
"sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3", "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7",
"sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786" "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.4" "version": "==5.3.1"
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:169e2e7b4839a7910b393eec127fd7cbae62e80fa55f89c6510426abf673fe5f", "sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2",
"sha256:c6c0462b8b361f8691171af1fb87eceb4442da28477e12200c40420176206ba7" "sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.1.6" "version": "==3.1.4"
}, },
"django-debug-toolbar": { "django-debug-toolbar": {
"hashes": [ "hashes": [
@ -1445,10 +1417,10 @@
}, },
"gitpython": { "gitpython": {
"hashes": [ "hashes": [
"sha256:42dbefd8d9e2576c496ed0059f3103dcef7125b9ce16f9d5f9c834aed44a1dac", "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
"sha256:867ec3dfb126aac0f8296b19fb63b8c4a399f32b4b6fafe84c4b10af5fa9f7b5" "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
], ],
"version": "==3.1.12" "version": "==3.1.11"
}, },
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [
@ -1506,11 +1478,11 @@
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
], ],
"index": "pypi", "index": "pypi",
"version": "==20.9" "version": "==20.8"
}, },
"pathspec": { "pathspec": {
"hashes": [ "hashes": [
@ -1619,11 +1591,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8",
"sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.2.2" "version": "==6.2.1"
}, },
"pytest-django": { "pytest-django": {
"hashes": [ "hashes": [
@ -1635,37 +1607,29 @@
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
"sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
], ],
"version": "==2021.1" "version": "==2020.4"
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
"sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
"sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
"sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e",
"sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
"sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
"sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
"sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
"sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
"sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a",
"sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
"sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
"sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
"sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
"sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
"sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
"sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
"sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
"sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
"sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
"sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.4.1" "version": "==5.3.1"
}, },
"regex": { "regex": {
"hashes": [ "hashes": [
@ -1742,17 +1706,17 @@
}, },
"smmap": { "smmap": {
"hashes": [ "hashes": [
"sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714", "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4",
"sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50" "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"
], ],
"version": "==3.0.5" "version": "==3.0.4"
}, },
"snowballstemmer": { "snowballstemmer": {
"hashes": [ "hashes": [
"sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
"sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
], ],
"version": "==2.1.0" "version": "==2.0.0"
}, },
"sqlparse": { "sqlparse": {
"hashes": [ "hashes": [
@ -1777,38 +1741,38 @@
}, },
"typed-ast": { "typed-ast": {
"hashes": [ "hashes": [
"sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
"sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
"sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d",
"sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
"sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
"sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
"sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c",
"sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
"sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
"sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
"sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
"sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
"sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
"sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d",
"sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
"sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
"sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c",
"sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
"sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395",
"sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
"sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
"sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
"sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
"sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
"sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072",
"sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298",
"sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91",
"sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
"sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f",
"sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
], ],
"version": "==1.4.2" "version": "==1.4.1"
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
@ -1823,11 +1787,12 @@
"secure" "secure"
], ],
"hashes": [ "hashes": [
"sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
"sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.26.3" "markers": null,
"version": "==1.26.2"
}, },
"wrapt": { "wrapt": {
"hashes": [ "hashes": [

View File

@ -2,11 +2,13 @@
## Supported Versions ## Supported Versions
| Version | Supported | As authentik is currently in a pre-stable, only the latest "stable" version is supported. After authentik 1.0, this will change.
| ---------- | ------------------ |
| 0.13.x | :white_check_mark: | | Version | Supported |
| 0.14.x | :white_check_mark: | | -------- | ------------------ |
| 2021.1.x | :white_check_mark: | | 0.11.x | :white_check_mark: |
| 0.12.x | :white_check_mark: |
| 0.13.x | :white_check_mark: |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -1,2 +1,2 @@
"""authentik""" """authentik"""
__version__ = "2021.2.1-rc1" __version__ = "0.14.0-rc2"

View File

@ -14,7 +14,7 @@ from rest_framework.response import Response
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
from authentik.events.monitored_tasks import TaskInfo from authentik.lib.tasks import TaskInfo
class TaskSerializer(Serializer): class TaskSerializer(Serializer):

View File

@ -1,22 +1,17 @@
"""authentik admin tasks""" """authentik admin tasks"""
import re
from django.core.cache import cache from django.core.cache import cache
from django.core.validators import URLValidator
from packaging.version import parse from packaging.version import parse
from requests import RequestException, get from requests import RequestException, get
from structlog.stdlib import get_logger from structlog import get_logger
from authentik import __version__ from authentik import __version__
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
LOGGER = get_logger() LOGGER = get_logger()
VERSION_CACHE_KEY = "authentik_latest_version" VERSION_CACHE_KEY = "authentik_latest_version"
VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours VERSION_CACHE_TIMEOUT = 2 * 60 * 60 # 2 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) @CELERY_APP.task(bind=True, base=MonitoredTask)
@ -44,10 +39,7 @@ def update_latest_version(self: MonitoredTask):
context__new_version=upstream_version, context__new_version=upstream_version,
).exists(): ).exists():
return return
event_dict = {"new_version": upstream_version} Event.new(EventAction.UPDATE_AVAILABLE, new_version=upstream_version).save()
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: except (RequestException, IndexError) as exc:
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT) cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))

View File

@ -3,42 +3,7 @@
{% load i18n %} {% load i18n %}
{% block above_form %} {% block above_form %}
<h1>{% blocktrans with policy=policy %}Test {{ policy }}{% endblocktrans %}</h1> <h1>{% blocktrans with policy=policy %}Test policy {{ 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-control">
<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-control">
<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 %} {% endblock %}
{% block action %} {% block action %}

View File

@ -1,28 +0,0 @@
{% 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 %}

View File

@ -160,17 +160,6 @@
</ak-modal-button> </ak-modal-button>
</li> </li>
{% endfor %} {% 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> </ul>
</ak-dropdown> </ak-dropdown>
</div> </div>

View File

@ -38,7 +38,7 @@
{% for task in object_list %} {% for task in object_list %}
<tr role="row"> <tr role="row">
<th role="columnheader"> <th role="columnheader">
<span>{{ task.html_name|join:"_&shy;" }}</span> <pre>{{ task.task_name }}</pre>
</th> </th>
<td role="cell"> <td role="cell">
<span> <span>

View File

@ -2,7 +2,7 @@
from django import template from django import template
from django.db.models import Model from django.db.models import Model
from django.utils.html import mark_safe from django.utils.html import mark_safe
from structlog.stdlib import get_logger from structlog import get_logger
register = template.Library() register = template.Library()
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -32,8 +32,7 @@ REQUEST_MOCK_VALID = Mock(
return_value=MockResponse( return_value=MockResponse(
200, 200,
"""{ """{
"tag_name": "version/99999999.9999999", "tag_name": "version/1.2.3"
"body": "https://goauthentik.io/test"
}""", }""",
) )
) )
@ -48,12 +47,10 @@ class TestAdminTasks(TestCase):
def test_version_valid_response(self): def test_version_valid_response(self):
"""Test Update checker with valid response""" """Test Update checker with valid response"""
update_latest_version.delay().get() update_latest_version.delay().get()
self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999") self.assertEqual(cache.get(VERSION_CACHE_KEY), "1.2.3")
self.assertTrue( self.assertTrue(
Event.objects.filter( Event.objects.filter(
action=EventAction.UPDATE_AVAILABLE, action=EventAction.UPDATE_AVAILABLE, context__new_version="1.2.3"
context__new_version="99999999.9999999",
context__message="Changelog: https://goauthentik.io/test",
).exists() ).exists()
) )
# test that a consecutive check doesn't create a duplicate event # test that a consecutive check doesn't create a duplicate event
@ -61,9 +58,7 @@ class TestAdminTasks(TestCase):
self.assertEqual( self.assertEqual(
len( len(
Event.objects.filter( Event.objects.filter(
action=EventAction.UPDATE_AVAILABLE, action=EventAction.UPDATE_AVAILABLE, context__new_version="1.2.3"
context__new_version="99999999.9999999",
context__message="Changelog: https://goauthentik.io/test",
) )
), ),
1, 1,

View File

@ -4,8 +4,6 @@ from django.urls import path
from authentik.admin.views import ( from authentik.admin.views import (
applications, applications,
certificate_key_pair, certificate_key_pair,
events_notifications_rules,
events_notifications_transports,
flows, flows,
groups, groups,
outposts, outposts,
@ -238,6 +236,11 @@ urlpatterns = [
name="flow-delete", name="flow-delete",
), ),
# Property Mappings # Property Mappings
path(
"property-mappings/",
property_mappings.PropertyMappingListView.as_view(),
name="property-mappings",
),
path( path(
"property-mappings/create/", "property-mappings/create/",
property_mappings.PropertyMappingCreateView.as_view(), property_mappings.PropertyMappingCreateView.as_view(),
@ -253,11 +256,6 @@ urlpatterns = [
property_mappings.PropertyMappingDeleteView.as_view(), property_mappings.PropertyMappingDeleteView.as_view(),
name="property-mapping-delete", name="property-mapping-delete",
), ),
path(
"property-mappings/<uuid:pk>/test/",
property_mappings.PropertyMappingTestView.as_view(),
name="property-mapping-test",
),
# Users # Users
path("users/", users.UserListView.as_view(), name="users"), path("users/", users.UserListView.as_view(), name="users"),
path("users/create/", users.UserCreateView.as_view(), name="user-create"), path("users/create/", users.UserCreateView.as_view(), name="user-create"),
@ -354,36 +352,4 @@ urlpatterns = [
tasks.TaskListView.as_view(), tasks.TaskListView.as_view(),
name="tasks", name="tasks",
), ),
# Event Notification Transpots
path(
"events/transports/create/",
events_notifications_transports.NotificationTransportCreateView.as_view(),
name="notification-transport-create",
),
path(
"events/transports/<uuid:pk>/update/",
events_notifications_transports.NotificationTransportUpdateView.as_view(),
name="notification-transport-update",
),
path(
"events/transports/<uuid:pk>/delete/",
events_notifications_transports.NotificationTransportDeleteView.as_view(),
name="notification-transport-delete",
),
# Event Notification Rules
path(
"events/rules/create/",
events_notifications_rules.NotificationRuleCreateView.as_view(),
name="notification-rule-create",
),
path(
"events/rules/<uuid:pk>/update/",
events_notifications_rules.NotificationRuleUpdateView.as_view(),
name="notification-rule-update",
),
path(
"events/rules/<uuid:pk>/delete/",
events_notifications_rules.NotificationRuleDeleteView.as_view(),
name="notification-rule-delete",
),
] ]

View File

@ -29,7 +29,7 @@ class ApplicationCreateView(
permission_required = "authentik_core.add_application" permission_required = "authentik_core.add_application"
template_name = "generic/create.html" template_name = "generic/create.html"
success_url = reverse_lazy("authentik_core:shell") success_url = reverse_lazy("authentik_admin:applications")
success_message = _("Successfully created Application") success_message = _("Successfully created Application")
@ -47,7 +47,7 @@ class ApplicationUpdateView(
permission_required = "authentik_core.change_application" permission_required = "authentik_core.change_application"
template_name = "generic/update.html" template_name = "generic/update.html"
success_url = reverse_lazy("authentik_core:shell") success_url = reverse_lazy("authentik_admin:applications")
success_message = _("Successfully updated Application") success_message = _("Successfully updated Application")
@ -60,5 +60,5 @@ class ApplicationDeleteView(
permission_required = "authentik_core.delete_application" permission_required = "authentik_core.delete_application"
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_core:shell") success_url = reverse_lazy("authentik_admin:applications")
success_message = _("Successfully deleted Application") success_message = _("Successfully deleted Application")

View File

@ -1,64 +0,0 @@
"""authentik NotificationRule administration"""
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.utils.translation import gettext as _
from django.views.generic import UpdateView
from guardian.mixins import PermissionRequiredMixin
from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
from authentik.events.forms import NotificationRuleForm
from authentik.events.models import NotificationRule
from authentik.lib.views import CreateAssignPermView
class NotificationRuleCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new NotificationRule"""
model = NotificationRule
form_class = NotificationRuleForm
permission_required = "authentik_events.add_NotificationRule"
template_name = "generic/create.html"
success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully created Notification Rule")
class NotificationRuleUpdateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update application"""
model = NotificationRule
form_class = NotificationRuleForm
permission_required = "authentik_events.change_NotificationRule"
template_name = "generic/update.html"
success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully updated Notification Rule")
class NotificationRuleDeleteView(
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete application"""
model = NotificationRule
permission_required = "authentik_events.delete_NotificationRule"
template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully deleted Notification Rule")

View File

@ -1,64 +0,0 @@
"""authentik NotificationTransport administration"""
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.utils.translation import gettext as _
from django.views.generic import UpdateView
from guardian.mixins import PermissionRequiredMixin
from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
from authentik.events.forms import NotificationTransportForm
from authentik.events.models import NotificationTransport
from authentik.lib.views import CreateAssignPermView
class NotificationTransportCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new NotificationTransport"""
model = NotificationTransport
form_class = NotificationTransportForm
permission_required = "authentik_events.add_notificationtransport"
template_name = "generic/create.html"
success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully created Notification Transport")
class NotificationTransportUpdateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update application"""
model = NotificationTransport
form_class = NotificationTransportForm
permission_required = "authentik_events.change_notificationtransport"
template_name = "generic/update.html"
success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully updated Notification Transport")
class NotificationTransportDeleteView(
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete application"""
model = NotificationTransport
permission_required = "authentik_events.delete_notificationtransport"
template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully deleted Notification Transport")

View File

@ -17,7 +17,6 @@ from authentik.admin.views.utils import (
SearchListMixin, SearchListMixin,
UserPaginateListMixin, UserPaginateListMixin,
) )
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.forms import FlowForm, FlowImportForm from authentik.flows.forms import FlowForm, FlowImportForm
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
@ -26,7 +25,7 @@ from authentik.flows.transfer.exporter import FlowExporter
from authentik.flows.transfer.importer import FlowImporter from authentik.flows.transfer.importer import FlowImporter
from authentik.flows.views import SESSION_KEY_PLAN, FlowPlanner from authentik.flows.views import SESSION_KEY_PLAN, FlowPlanner
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import CreateAssignPermView, bad_request_message from authentik.lib.views import CreateAssignPermView
class FlowListView( class FlowListView(
@ -104,17 +103,8 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
flow: Flow = self.get_object() flow: Flow = self.get_object()
planner = FlowPlanner(flow) planner = FlowPlanner(flow)
planner.use_cache = False planner.use_cache = False
try: plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user})
plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user}) self.request.session[SESSION_KEY_PLAN] = plan
self.request.session[SESSION_KEY_PLAN] = plan
except FlowNonApplicableException as exc:
return bad_request_message(
request,
_(
"Flow not applicable to current user/request: %(messages)s"
% {"messages": str(exc)}
),
)
return redirect_with_qs( return redirect_with_qs(
"authentik_flows:flow-executor-shell", "authentik_flows:flow-executor-shell",
self.request.GET, self.request.GET,

View File

@ -5,11 +5,10 @@ from django.http.request import HttpRequest
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView from django.views.generic import FormView
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm
from authentik.admin.mixins import AdminRequiredMixin from authentik.admin.mixins import AdminRequiredMixin
from authentik.core.api.applications import user_app_cache_key
LOGGER = get_logger() LOGGER = get_logger()
@ -27,9 +26,6 @@ class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
keys = cache.keys("policy_*") keys = cache.keys("policy_*")
cache.delete_many(keys) cache.delete_many(keys)
LOGGER.debug("Cleared Policy cache", keys=len(keys)) LOGGER.debug("Cleared Policy cache", keys=len(keys))
# Also delete user application cache
keys = cache.keys(user_app_cache_key("*"))
cache.delete_many(keys)
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)

View File

@ -1,11 +1,13 @@
"""authentik Policy administration""" """authentik Policy administration"""
from typing import Any, Dict from typing import Any, Dict
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import ( from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin, PermissionRequiredMixin as DjangoPermissionRequiredMixin,
) )
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import QuerySet
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -97,7 +99,7 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
template_name = "administration/policy/test.html" template_name = "administration/policy/test.html"
object = None object = None
def get_object(self, queryset=None) -> Policy: def get_object(self, queryset=None) -> QuerySet:
return ( return (
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
) )
@ -116,10 +118,12 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
p_request = PolicyRequest(user) p_request = PolicyRequest(user)
p_request.http_request = self.request p_request.http_request = self.request
p_request.context = form.cleaned_data.get("context", {}) p_request.context = form.cleaned_data
proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None) proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None)
result = proc.execute() result = proc.execute()
context = self.get_context_data(form=form) if result.passing:
context["result"] = result messages.success(self.request, _("User successfully passed policy."))
return self.render_to_response(context) else:
messages.error(self.request, _("User didn't pass policy."))
return self.render_to_response(self.get_context_data(form=form, result=result))

View File

@ -1,29 +1,41 @@
"""authentik PropertyMapping administration""" """authentik PropertyMapping administration"""
from json import dumps
from typing import Any
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import ( from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin, PermissionRequiredMixin as DjangoPermissionRequiredMixin,
) )
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
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 ( from authentik.admin.views.utils import (
BackSuccessUrlMixin, BackSuccessUrlMixin,
DeleteMessageView, DeleteMessageView,
InheritanceCreateView, InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView, InheritanceUpdateView,
SearchListMixin,
UserPaginateListMixin,
) )
from authentik.core.models import PropertyMapping 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( class PropertyMappingCreateView(
SuccessMessageMixin, SuccessMessageMixin,
BackSuccessUrlMixin, BackSuccessUrlMixin,
@ -69,44 +81,3 @@ class PropertyMappingDeleteView(
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_admin:property-mappings") success_url = reverse_lazy("authentik_admin:property-mappings")
success_message = _("Successfully deleted Property Mapping") 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)

View File

@ -4,7 +4,7 @@ from typing import Any, Dict
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from authentik.admin.mixins import AdminRequiredMixin from authentik.admin.mixins import AdminRequiredMixin
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus from authentik.lib.tasks import TaskInfo, TaskResultStatus
class TaskListView(AdminRequiredMixin, TemplateView): class TaskListView(AdminRequiredMixin, TemplateView):

View File

@ -5,7 +5,7 @@ from typing import Any, Optional, Tuple, Union
from rest_framework.authentication import BaseAuthentication, get_authorization_header from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.request import Request from rest_framework.request import Request
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.core.models import Token, TokenIntents, User from authentik.core.models import Token, TokenIntents, User

View File

@ -1,31 +1,7 @@
{% extends "rest_framework/base.html" %} {% extends "rest_framework/base.html" %}
{% block title %}{% if name %}{{ name }} {% endif %}authentik{% endblock %}
{% block branding %} {% block branding %}
<span class='navbar-brand'> <span class='navbar-brand'>
authentik authentik
</span> </span>
{% endblock %} {% 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 %}

View File

@ -19,10 +19,7 @@ from authentik.core.api.sources import SourceViewSet
from authentik.core.api.tokens import TokenViewSet from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.users import UserViewSet from authentik.core.api.users import UserViewSet
from authentik.crypto.api import CertificateKeyPairViewSet from authentik.crypto.api import CertificateKeyPairViewSet
from authentik.events.api.event import EventViewSet from authentik.events.api import EventViewSet
from authentik.events.api.notification import NotificationViewSet
from authentik.events.api.notification_rule import NotificationRuleViewSet
from authentik.events.api.notification_transport import NotificationTransportViewSet
from authentik.flows.api import ( from authentik.flows.api import (
FlowCacheViewSet, FlowCacheViewSet,
FlowStageBindingViewSet, FlowStageBindingViewSet,
@ -40,7 +37,6 @@ from authentik.policies.api import (
PolicyViewSet, PolicyViewSet,
) )
from authentik.policies.dummy.api import DummyPolicyViewSet from authentik.policies.dummy.api import DummyPolicyViewSet
from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
from authentik.policies.expression.api import ExpressionPolicyViewSet from authentik.policies.expression.api import ExpressionPolicyViewSet
from authentik.policies.group_membership.api import GroupMembershipPolicyViewSet from authentik.policies.group_membership.api import GroupMembershipPolicyViewSet
@ -101,9 +97,6 @@ router.register("flows/bindings", FlowStageBindingViewSet)
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet) router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
router.register("events/events", EventViewSet) router.register("events/events", EventViewSet)
router.register("events/notifications", NotificationViewSet)
router.register("events/transports", NotificationTransportViewSet)
router.register("events/rules", NotificationRuleViewSet)
router.register("sources/all", SourceViewSet) router.register("sources/all", SourceViewSet)
router.register("sources/ldap", LDAPSourceViewSet) router.register("sources/ldap", LDAPSourceViewSet)
@ -114,7 +107,6 @@ router.register("policies/all", PolicyViewSet)
router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache") router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache")
router.register("policies/bindings", PolicyBindingViewSet) router.register("policies/bindings", PolicyBindingViewSet)
router.register("policies/expression", ExpressionPolicyViewSet) router.register("policies/expression", ExpressionPolicyViewSet)
router.register("policies/event_matcher", EventMatcherPolicyViewSet)
router.register("policies/group_membership", GroupMembershipPolicyViewSet) router.register("policies/group_membership", GroupMembershipPolicyViewSet)
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet) router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
router.register("policies/password_expiry", PasswordExpiryPolicyViewSet) router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)

View File

@ -4,6 +4,9 @@ from django.apps import AppConfig, apps
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.sites import AlreadyRegistered from django.contrib.admin.sites import AlreadyRegistered
from guardian.admin import GuardedModelAdmin from guardian.admin import GuardedModelAdmin
from structlog import get_logger
LOGGER = get_logger()
def admin_autoregister(app: AppConfig): def admin_autoregister(app: AppConfig):
@ -17,4 +20,5 @@ def admin_autoregister(app: AppConfig):
for _app in apps.get_app_configs(): for _app in apps.get_app_configs():
if _app.label.startswith("authentik_"): if _app.label.startswith("authentik_"):
LOGGER.debug("Registering application for dj-admin", application=_app.label)
admin_autoregister(_app) admin_autoregister(_app)

View File

@ -1,5 +1,4 @@
"""Application API Views""" """Application API Views"""
from django.core.cache import cache
from django.db.models import QuerySet from django.db.models import QuerySet
from django.http.response import Http404 from django.http.response import Http404
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
@ -11,7 +10,6 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger
from authentik.admin.api.metrics import get_events_per_1h from authentik.admin.api.metrics import get_events_per_1h
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
@ -19,13 +17,6 @@ from authentik.core.models import Application
from authentik.events.models import EventAction from authentik.events.models import EventAction
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
LOGGER = get_logger()
def user_app_cache_key(user_pk: str) -> str:
"""Cache key where application list for user is saved"""
return f"user_app_cache_{user_pk}"
class ApplicationSerializer(ModelSerializer): class ApplicationSerializer(ModelSerializer):
"""Application Serializer""" """Application Serializer"""
@ -77,35 +68,16 @@ class ApplicationViewSet(ModelViewSet):
queryset = backend().filter_queryset(self.request, queryset, self) queryset = backend().filter_queryset(self.request, queryset, self)
return queryset return queryset
def _get_allowed_applications(self, queryset: QuerySet) -> list[Application]:
applications = []
for application in queryset:
engine = PolicyEngine(application, self.request.user, self.request)
engine.build()
if engine.passing:
applications.append(application)
return applications
def list(self, request: Request) -> Response: def list(self, request: Request) -> Response:
"""Custom list method that checks Policy based access instead of guardian""" """Custom list method that checks Policy based access instead of guardian"""
queryset = self._filter_queryset_for_list(self.get_queryset()) queryset = self._filter_queryset_for_list(self.get_queryset())
self.paginate_queryset(queryset) self.paginate_queryset(queryset)
should_cache = request.GET.get("search", "") == ""
allowed_applications = [] allowed_applications = []
if not should_cache: for application in queryset:
allowed_applications = self._get_allowed_applications(queryset) engine = PolicyEngine(application, self.request.user, self.request)
if should_cache: engine.build()
LOGGER.debug("Caching allowed application list") if engine.passing:
allowed_applications = cache.get(user_app_cache_key(self.request.user.pk)) allowed_applications.append(application)
if not allowed_applications:
allowed_applications = self._get_allowed_applications(queryset)
cache.set(
user_app_cache_key(self.request.user.pk),
allowed_applications,
timeout=86400,
)
serializer = self.get_serializer(allowed_applications, many=True) serializer = self.get_serializer(allowed_applications, many=True)
return self.get_paginated_response(serializer.data) return self.get_paginated_response(serializer.data)

View File

@ -2,48 +2,29 @@
from rest_framework.serializers import ModelSerializer, SerializerMethodField from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import PropertyMapping from authentik.core.models import PropertyMapping
class PropertyMappingSerializer(ModelSerializer, MetaNameSerializer): class PropertyMappingSerializer(ModelSerializer):
"""PropertyMapping Serializer""" """PropertyMapping Serializer"""
object_type = SerializerMethodField(method_name="get_type") __type__ = SerializerMethodField(method_name="get_type")
def get_type(self, obj): def get_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object""" """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", "") 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: class Meta:
model = PropertyMapping model = PropertyMapping
fields = [ fields = ["pk", "name", "expression", "__type__"]
"pk",
"name",
"expression",
"object_type",
"verbose_name",
"verbose_name_plural",
]
class PropertyMappingViewSet(ReadOnlyModelViewSet): class PropertyMappingViewSet(ReadOnlyModelViewSet):
"""PropertyMapping Viewset""" """PropertyMapping Viewset"""
queryset = PropertyMapping.objects.none() queryset = PropertyMapping.objects.all()
serializer_class = PropertyMappingSerializer serializer_class = PropertyMappingSerializer
search_fields = [
"name",
]
filterset_fields = {"managed": ["isnull"]}
ordering = ["name"]
def get_queryset(self): def get_queryset(self):
return PropertyMapping.objects.select_subclasses() return PropertyMapping.objects.select_subclasses()

View File

@ -1,5 +1,4 @@
"""Provider API Views""" """Provider API Views"""
from rest_framework.fields import ReadOnlyField
from rest_framework.serializers import ModelSerializer, SerializerMethodField from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
@ -10,15 +9,18 @@ from authentik.core.models import Provider
class ProviderSerializer(ModelSerializer, MetaNameSerializer): class ProviderSerializer(ModelSerializer, MetaNameSerializer):
"""Provider Serializer""" """Provider Serializer"""
assigned_application_slug = ReadOnlyField(source="application.slug")
assigned_application_name = ReadOnlyField(source="application.name")
object_type = SerializerMethodField() object_type = SerializerMethodField()
def get_object_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""" """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", "") 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: class Meta:
model = Provider model = Provider
@ -29,8 +31,6 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
"authorization_flow", "authorization_flow",
"property_mappings", "property_mappings",
"object_type", "object_type",
"assigned_application_slug",
"assigned_application_name",
"verbose_name", "verbose_name",
"verbose_name_plural", "verbose_name_plural",
] ]
@ -39,15 +39,11 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
class ProviderViewSet(ModelViewSet): class ProviderViewSet(ModelViewSet):
"""Provider Viewset""" """Provider Viewset"""
queryset = Provider.objects.none() queryset = Provider.objects.all()
serializer_class = ProviderSerializer serializer_class = ProviderSerializer
filterset_fields = { filterset_fields = {
"application": ["isnull"], "application": ["isnull"],
} }
search_fields = [
"name",
"application__name",
]
def get_queryset(self): def get_queryset(self):
return Provider.objects.select_subclasses() return Provider.objects.select_subclasses()

View File

@ -31,7 +31,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
class SourceViewSet(ReadOnlyModelViewSet): class SourceViewSet(ReadOnlyModelViewSet):
"""Source Viewset""" """Source Viewset"""
queryset = Source.objects.none() queryset = Source.objects.all()
serializer_class = SourceSerializer serializer_class = SourceSerializer
lookup_field = "slug" lookup_field = "slug"

View File

@ -34,7 +34,7 @@ class UserSerializer(ModelSerializer):
class UserViewSet(ModelViewSet): class UserViewSet(ModelViewSet):
"""User Viewset""" """User Viewset"""
queryset = User.objects.none() queryset = User.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
def get_queryset(self): def get_queryset(self):

View File

@ -1,6 +1,4 @@
"""authentik core app config""" """authentik core app config"""
from importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
@ -11,6 +9,3 @@ class AuthentikCoreConfig(AppConfig):
label = "authentik_core" label = "authentik_core"
verbose_name = "authentik Core" verbose_name = "authentik Core"
mountpoint = "" mountpoint = ""
def ready(self):
import_module("authentik.core.signals")

View File

@ -1,7 +1,7 @@
"""Channels base classes""" """Channels base classes"""
from channels.exceptions import DenyConnection from channels.exceptions import DenyConnection
from channels.generic.websocket import JsonWebsocketConsumer from channels.generic.websocket import JsonWebsocketConsumer
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.api.auth import token_from_header from authentik.api.auth import token_from_header
from authentik.core.models import User from authentik.core.models import User

View File

@ -28,7 +28,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
event = Event.new( event = Event.new(
EventAction.PROPERTY_MAPPING_EXCEPTION, EventAction.PROPERTY_MAPPING_EXCEPTION,
expression=expression_source, expression=expression_source,
message=error_string, error=error_string,
) )
if "user" in self._context: if "user" in self._context:
event.set_user(self._context["user"]) event.set_user(self._context["user"])

View File

@ -1,35 +0,0 @@
# 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,
),
),
]

View File

@ -15,14 +15,13 @@ from django.utils.translation import gettext_lazy as _
from guardian.mixins import GuardianUserMixin from guardian.mixins import GuardianUserMixin
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.signals import password_changed from authentik.core.signals import password_changed
from authentik.core.types import UILoginButton from authentik.core.types import UILoginButton
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.models import CreatedUpdatedModel, SerializerModel from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.managed.models import ManagedModel
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
LOGGER = get_logger() LOGGER = get_logger()
@ -314,7 +313,7 @@ class TokenIntents(models.TextChoices):
INTENT_RECOVERY = "recovery" INTENT_RECOVERY = "recovery"
class Token(ManagedModel, ExpiringModel): class Token(ExpiringModel):
"""Token used to authenticate the User for API Access or confirm another Stage like Email.""" """Token used to authenticate the User for API Access or confirm another Stage like Email."""
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -342,7 +341,7 @@ class Token(ManagedModel, ExpiringModel):
] ]
class PropertyMapping(SerializerModel, ManagedModel): class PropertyMapping(models.Model):
"""User-defined key -> x mapping which can be used by providers to expose extra data.""" """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) pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -356,11 +355,6 @@ class PropertyMapping(SerializerModel, ManagedModel):
"""Return Form class used to edit this object""" """Return Form class used to edit this object"""
raise NotImplementedError raise NotImplementedError
@property
def serializer(self) -> Type[Serializer]:
"""Get serializer for this model"""
raise NotImplementedError
def evaluate( def evaluate(
self, user: Optional[User], request: Optional[HttpRequest], **kwargs self, user: Optional[User], request: Optional[HttpRequest], **kwargs
) -> Any: ) -> Any:

View File

@ -1,24 +1,5 @@
"""authentik core signals""" """authentik core signals"""
from django.core.cache import cache
from django.core.signals import Signal from django.core.signals import Signal
from django.db.models.signals import post_save
from django.dispatch import receiver
# Arguments: user: User, password: str # Arguments: user: User, password: str
password_changed = Signal() 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)

View File

@ -8,10 +8,10 @@ from dbbackup.db.exceptions import CommandConnectorError
from django.contrib.humanize.templatetags.humanize import naturaltime from django.contrib.humanize.templatetags.humanize import naturaltime
from django.core import management from django.core import management
from django.utils.timezone import now from django.utils.timezone import now
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.core.models import ExpiringModel from authentik.core.models import ExpiringModel
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -9,14 +9,14 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title> <title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title>
<link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}"> <link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}"> <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-addons.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-addons.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<script src="{% url 'javascript-catalog' %}?v={{ ak_version }}"></script> <script src="{% url 'javascript-catalog' %}"></script>
<script src="{% static 'dist/main.js' %}?v={{ ak_version }}" type="module"></script> <script src="{% static 'dist/main.js' %}" type="module"></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}
</head> </head>

View File

@ -1,6 +1,5 @@
{% load i18n %} {% load i18n %}
{% load authentik_user_settings %} {% load authentik_user_settings %}
{% load authentik_utils %}
<div class="pf-c-page"> <div class="pf-c-page">
<main role="main" class="pf-c-page__main" tabindex="-1"> <main role="main" class="pf-c-page__main" tabindex="-1">
@ -13,45 +12,47 @@
<p>{% trans "Configure settings relevant to your user profile." %}</p> <p>{% trans "Configure settings relevant to your user profile." %}</p>
</div> </div>
</section> </section>
<ak-tabs> <section class="pf-c-page__main-section">
<section slot="page-1" data-tab-title="{% trans 'User details' %}" class="pf-c-page__main-section pf-m-no-padding-mobile"> <div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-display-flex pf-u-justify-content-center"> <div class="pf-u-w-75">
<div class="pf-u-w-75"> <ak-site-shell url="{% url 'authentik_core:user-details' %}">
<ak-site-shell url="{% url 'authentik_core:user-details' %}"> <div slot="body"></div>
<div slot="body"></div> </ak-site-shell>
</ak-site-shell>
</div>
</div> </div>
</section> </div>
<section slot="page-2" data-tab-title="{% trans 'Tokens' %}" class="pf-c-page__main-section pf-m-no-padding-mobile"> </section>
<ak-site-shell url="{% url 'authentik_core:user-tokens' %}"> <section class="pf-c-page__main-section">
<div slot="body"></div> <div class="pf-u-display-flex pf-u-justify-content-center">
</ak-site-shell> <div class="pf-u-w-75">
</section> <ak-site-shell url="{% url 'authentik_core:user-tokens' %}">
{% user_stages as user_stages_loc %} <div slot="body"></div>
{% for stage, stage_link in user_stages_loc.items %} </ak-site-shell>
<section slot="page-{{ stage.pk }}" data-tab-title="{{ stage|verbose_name }}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<ak-site-shell url="{{ stage_link }}">
<div slot="body"></div>
</ak-site-shell>
</div>
</div> </div>
</section> </div>
{% endfor %} </section>
{% user_sources as user_sources_loc %} {% user_stages as user_stages_loc %}
{% for source, source_link in user_sources_loc.item %} {% for stage in user_stages_loc %}
<section slot="page-{{ source.pk }}" data-tab-title="{{ source|verbose_name }}" class="pf-c-page__main-section pf-m-no-padding-mobile"> <section class="pf-c-page__main-section">
<div class="pf-u-display-flex pf-u-justify-content-center"> <div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75"> <div class="pf-u-w-75">
<ak-site-shell url="{{ source_link }}"> <ak-site-shell url="{{ stage }}">
<div slot="body"></div> <div slot="body"></div>
</ak-site-shell> </ak-site-shell>
</div>
</div> </div>
</section> </div>
{% endfor %} </section>
</ak-tabs> {% endfor %}
{% user_sources as user_sources_loc %}
{% for source in user_sources_loc %}
<section class="pf-c-page__main-section">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<ak-site-shell url="{{ source }}">
<div slot="body"></div>
</ak-site-shell>
</div>
</div>
</section>
{% endfor %}
</main> </main>
</div> </div>

View File

@ -13,26 +13,26 @@ register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def user_stages(context: RequestContext) -> dict[Stage, str]: def user_stages(context: RequestContext) -> list[str]:
"""Return list of all stages which apply to user""" """Return list of all stages which apply to user"""
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses() _all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
matching_stages: dict[Stage, str] = {} matching_stages: list[str] = []
for stage in _all_stages: for stage in _all_stages:
user_settings = stage.ui_user_settings user_settings = stage.ui_user_settings
if not user_settings: if not user_settings:
continue continue
matching_stages[stage] = user_settings matching_stages.append(user_settings)
return matching_stages return matching_stages
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def user_sources(context: RequestContext) -> dict[Source, str]: def user_sources(context: RequestContext) -> list[str]:
"""Return a list of all sources which are enabled for the user""" """Return a list of all sources which are enabled for the user"""
user = context.get("request").user user = context.get("request").user
_all_sources: Iterable[Source] = Source.objects.filter( _all_sources: Iterable[Source] = Source.objects.filter(
enabled=True enabled=True
).select_subclasses() ).select_subclasses()
matching_sources: dict[Source, str] = {} matching_sources: list[str] = []
for source in _all_sources: for source in _all_sources:
user_settings = source.ui_user_settings user_settings = source.ui_user_settings
if not user_settings: if not user_settings:
@ -40,5 +40,5 @@ def user_sources(context: RequestContext) -> dict[Source, str]:
policy_engine = PolicyEngine(source, user, context.get("request")) policy_engine = PolicyEngine(source, user, context.get("request"))
policy_engine.build() policy_engine.build()
if policy_engine.passing: if policy_engine.passing:
matching_sources[source] = user_settings matching_sources.append(user_settings)
return matching_sources return matching_sources

View File

@ -3,7 +3,7 @@
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.views import View from django.views import View
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.core.middleware import ( from authentik.core.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_ORIGINAL_USER,

View File

@ -50,7 +50,6 @@ class EventViewSet(ReadOnlyModelViewSet):
serializer_class = EventSerializer serializer_class = EventSerializer
ordering = ["-created"] ordering = ["-created"]
search_fields = [ search_fields = [
"event_uuid",
"user", "user",
"action", "action",
"app", "app",

View File

@ -1,53 +0,0 @@
"""Notification API Views"""
from rest_framework import mixins
from rest_framework.fields import ReadOnlyField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.events.api.event import EventSerializer
from authentik.events.models import Notification
class NotificationSerializer(ModelSerializer):
"""Notification Serializer"""
body = ReadOnlyField()
severity = ReadOnlyField()
event = EventSerializer()
class Meta:
model = Notification
fields = [
"pk",
"severity",
"body",
"created",
"event",
"seen",
]
class NotificationViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""Notification Viewset"""
queryset = Notification.objects.all()
serializer_class = NotificationSerializer
filterset_fields = [
"severity",
"body",
"created",
"event",
"seen",
]
def get_queryset(self):
if not self.request:
return super().get_queryset()
return Notification.objects.filter(user=self.request.user)

View File

@ -1,28 +0,0 @@
"""NotificationRule API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.events.models import NotificationRule
class NotificationRuleSerializer(ModelSerializer):
"""NotificationRule Serializer"""
class Meta:
model = NotificationRule
depth = 2
fields = [
"pk",
"name",
"transports",
"severity",
"group",
]
class NotificationRuleViewSet(ModelViewSet):
"""NotificationRule Viewset"""
queryset = NotificationRule.objects.all()
serializer_class = NotificationRuleSerializer

View File

@ -1,66 +0,0 @@
"""NotificationTransport API Views"""
from django.http.response import Http404
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.events.models import (
Notification,
NotificationSeverity,
NotificationTransport,
NotificationTransportError,
TransportMode,
)
class NotificationTransportSerializer(ModelSerializer):
"""NotificationTransport Serializer"""
mode_verbose = SerializerMethodField()
def get_mode_verbose(self, instance: NotificationTransport):
"""Return selected mode with a UI Label"""
return TransportMode(instance.mode).label
class Meta:
model = NotificationTransport
fields = [
"pk",
"name",
"mode",
"mode_verbose",
"webhook_url",
]
class NotificationTransportViewSet(ModelViewSet):
"""NotificationTransport Viewset"""
queryset = NotificationTransport.objects.all()
serializer_class = NotificationTransportSerializer
@action(detail=True, methods=["post"])
# pylint: disable=invalid-name
def test(self, request: Request, pk=None) -> Response:
"""Send example notification using selected transport. Requires
Modify permissions."""
transports = get_objects_for_user(
request.user, "authentik_events.change_notificationtransport"
).filter(pk=pk)
if not transports.exists():
raise Http404
transport: NotificationTransport = transports.first()
notification = Notification(
severity=NotificationSeverity.NOTICE,
body=f"Test Notification from transport {transport.name}",
user=request.user,
)
try:
return Response(transport.send(notification))
except NotificationTransportError as exc:
return Response(str(exc.__cause__ or None), status=503)

View File

@ -1,48 +0,0 @@
"""authentik events NotificationTransport forms"""
from django import forms
from django.utils.translation import gettext_lazy as _
from authentik.events.models import NotificationRule, NotificationTransport
class NotificationTransportForm(forms.ModelForm):
"""NotificationTransport Form"""
class Meta:
model = NotificationTransport
fields = [
"name",
"mode",
"webhook_url",
"send_once",
]
widgets = {
"name": forms.TextInput(),
"webhook_url": forms.TextInput(),
}
labels = {
"webhook_url": _("Webhook URL"),
}
help_texts = {
"webhook_url": _(
("Only required when the Generic or Slack Webhook is used.")
),
}
class NotificationRuleForm(forms.ModelForm):
"""NotificationRule Form"""
class Meta:
model = NotificationRule
fields = [
"name",
"group",
"transports",
"severity",
]
widgets = {
"name": forms.TextInput(),
}

View File

@ -6,10 +6,9 @@ from django.contrib.auth.models import User
from django.db.models import Model from django.db.models import Model
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import post_save, pre_delete
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from guardian.models import UserObjectPermission
from authentik.core.middleware import LOCAL from authentik.core.middleware import LOCAL
from authentik.events.models import Event, EventAction, Notification from authentik.events.models import Event, EventAction
from authentik.events.signals import EventNewThread from authentik.events.signals import EventNewThread
from authentik.events.utils import model_to_dict from authentik.events.utils import model_to_dict
@ -64,7 +63,7 @@ class AuditMiddleware:
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_ user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
): ):
"""Signal handler for all object's post_save""" """Signal handler for all object's post_save"""
if isinstance(instance, (Event, Notification, UserObjectPermission)): if isinstance(instance, Event):
return return
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
@ -76,7 +75,7 @@ class AuditMiddleware:
user: User, request: HttpRequest, sender, instance: Model, **_ user: User, request: HttpRequest, sender, instance: Model, **_
): ):
"""Signal handler for all object's pre_delete""" """Signal handler for all object's pre_delete"""
if isinstance(instance, (Event, Notification, UserObjectPermission)): if isinstance(instance, Event):
return return
EventNewThread( EventNewThread(

View File

@ -1,148 +0,0 @@
# Generated by Django 3.1.4 on 2021-01-11 16:36
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("authentik_policies", "0004_policy_execution_logging"),
("authentik_core", "0016_auto_20201202_2234"),
("authentik_events", "0009_auto_20201227_1210"),
]
operations = [
migrations.CreateModel(
name="NotificationTransport",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.TextField(unique=True)),
(
"mode",
models.TextField(
choices=[
("webhook", "Generic Webhook"),
("webhook_slack", "Slack Webhook (Slack/Discord)"),
("email", "Email"),
]
),
),
("webhook_url", models.TextField(blank=True)),
],
options={
"verbose_name": "Notification Transport",
"verbose_name_plural": "Notification Transports",
},
),
migrations.CreateModel(
name="NotificationRule",
fields=[
(
"policybindingmodel_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_policies.policybindingmodel",
),
),
("name", models.TextField(unique=True)),
(
"severity",
models.TextField(
choices=[
("notice", "Notice"),
("warning", "Warning"),
("alert", "Alert"),
],
default="notice",
help_text="Controls which severity level the created notifications will have.",
),
),
(
"group",
models.ForeignKey(
blank=True,
help_text="Define which group of users this notification should be sent and shown to. If left empty, Notification won't ben sent.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_core.group",
),
),
(
"transports",
models.ManyToManyField(
help_text="Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",
to="authentik_events.NotificationTransport",
),
),
],
options={
"verbose_name": "Notification Rule",
"verbose_name_plural": "Notification Rules",
},
bases=("authentik_policies.policybindingmodel",),
),
migrations.CreateModel(
name="Notification",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"severity",
models.TextField(
choices=[
("notice", "Notice"),
("warning", "Warning"),
("alert", "Alert"),
]
),
),
("body", models.TextField()),
("created", models.DateTimeField(auto_now_add=True)),
("seen", models.BooleanField(default=False)),
(
"event",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_events.event",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Notification",
"verbose_name_plural": "Notifications",
},
),
]

View File

@ -1,165 +0,0 @@
# Generated by Django 3.1.4 on 2021-01-10 18:57
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.events.models import EventAction, NotificationSeverity, TransportMode
def notify_configuration_error(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Group = apps.get_model("authentik_core", "Group")
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
EventMatcherPolicy = apps.get_model(
"authentik_policies_event_matcher", "EventMatcherPolicy"
)
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
admin_group = (
Group.objects.using(db_alias)
.filter(name="authentik Admins", is_superuser=True)
.first()
)
policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
name="default-match-configuration-error",
defaults={"action": EventAction.CONFIGURATION_ERROR},
)
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
name="default-notify-configuration-error",
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
)
trigger.transports.set(
NotificationTransport.objects.using(db_alias).filter(
name="default-email-transport"
)
)
trigger.save()
PolicyBinding.objects.using(db_alias).update_or_create(
target=trigger,
policy=policy,
defaults={
"order": 0,
},
)
def notify_update(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Group = apps.get_model("authentik_core", "Group")
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
EventMatcherPolicy = apps.get_model(
"authentik_policies_event_matcher", "EventMatcherPolicy"
)
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
admin_group = (
Group.objects.using(db_alias)
.filter(name="authentik Admins", is_superuser=True)
.first()
)
policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
name="default-match-update",
defaults={"action": EventAction.UPDATE_AVAILABLE},
)
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
name="default-notify-update",
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
)
trigger.transports.set(
NotificationTransport.objects.using(db_alias).filter(
name="default-email-transport"
)
)
trigger.save()
PolicyBinding.objects.using(db_alias).update_or_create(
target=trigger,
policy=policy,
defaults={
"order": 0,
},
)
def notify_exception(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Group = apps.get_model("authentik_core", "Group")
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
EventMatcherPolicy = apps.get_model(
"authentik_policies_event_matcher", "EventMatcherPolicy"
)
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
admin_group = (
Group.objects.using(db_alias)
.filter(name="authentik Admins", is_superuser=True)
.first()
)
policy_policy_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
name="default-match-policy-exception",
defaults={"action": EventAction.POLICY_EXCEPTION},
)
policy_pm_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
name="default-match-property-mapping-exception",
defaults={"action": EventAction.PROPERTY_MAPPING_EXCEPTION},
)
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
name="default-notify-exception",
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
)
trigger.transports.set(
NotificationTransport.objects.using(db_alias).filter(
name="default-email-transport"
)
)
trigger.save()
PolicyBinding.objects.using(db_alias).update_or_create(
target=trigger,
policy=policy_policy_exc,
defaults={
"order": 0,
},
)
PolicyBinding.objects.using(db_alias).update_or_create(
target=trigger,
policy=policy_pm_exc,
defaults={
"order": 1,
},
)
def transport_email_global(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
NotificationTransport.objects.using(db_alias).update_or_create(
name="default-email-transport",
defaults={"mode": TransportMode.EMAIL},
)
class Migration(migrations.Migration):
dependencies = [
(
"authentik_events",
"0010_notification_notificationtransport_notificationrule",
),
("authentik_core", "0016_auto_20201202_2234"),
("authentik_policies_event_matcher", "0003_auto_20210110_1907"),
("authentik_policies", "0004_policy_execution_logging"),
]
operations = [
migrations.RunPython(transport_email_global),
migrations.RunPython(notify_configuration_error),
migrations.RunPython(notify_update),
migrations.RunPython(notify_exception),
]

View File

@ -1,52 +0,0 @@
# 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"),
]
),
),
]

View File

@ -1,6 +1,6 @@
"""authentik events models""" """authentik events models"""
from inspect import getmodule, stack from inspect import getmodule, stack
from smtplib import SMTPException
from typing import Optional, Union from typing import Optional, Union
from uuid import uuid4 from uuid import uuid4
@ -9,28 +9,19 @@ from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from requests import RequestException, post from structlog import get_logger
from structlog.stdlib import get_logger
from authentik import __version__
from authentik.core.middleware import ( from authentik.core.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_ORIGINAL_USER,
SESSION_IMPERSONATE_USER, SESSION_IMPERSONATE_USER,
) )
from authentik.core.models import Group, User from authentik.core.models import User
from authentik.events.utils import cleanse_dict, get_user, sanitize_dict from authentik.events.utils import cleanse_dict, get_user, sanitize_dict
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.http import get_client_ip from authentik.lib.utils.http import get_client_ip
from authentik.policies.models import PolicyBindingModel
from authentik.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger("authentik.events") LOGGER = get_logger("authentik.events")
class NotificationTransportError(SentryIgnoredException):
"""Error raised when a notification fails to be delivered"""
class EventAction(models.TextChoices): class EventAction(models.TextChoices):
"""All possible actions to save into the events log""" """All possible actions to save into the events log"""
@ -56,9 +47,6 @@ class EventAction(models.TextChoices):
POLICY_EXCEPTION = "policy_exception" POLICY_EXCEPTION = "policy_exception"
PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception" PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception"
SYSTEM_TASK_EXECUTION = "system_task_execution"
SYSTEM_TASK_EXCEPTION = "system_task_exception"
CONFIGURATION_ERROR = "configuration_error" CONFIGURATION_ERROR = "configuration_error"
MODEL_CREATED = "model_created" MODEL_CREATED = "model_created"
@ -116,12 +104,10 @@ class Event(models.Model):
Events independently from requests. Events independently from requests.
`user` arguments optionally overrides user from requests.""" `user` arguments optionally overrides user from requests."""
if hasattr(request, "user"): if hasattr(request, "user"):
original_user = None self.user = get_user(
if hasattr(request, "session"): request.user,
original_user = request.session.get( request.session.get(SESSION_IMPERSONATE_ORIGINAL_USER, None),
SESSION_IMPERSONATE_ORIGINAL_USER, None )
)
self.user = get_user(request.user, original_user)
if user: if user:
self.user = get_user(user) self.user = get_user(user)
# Check if we're currently impersonating, and add that user # Check if we're currently impersonating, and add that user
@ -141,7 +127,9 @@ class Event(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self._state.adding: if not self._state.adding:
raise ValidationError("you may not edit an existing Event") raise ValidationError(
"you may not edit an existing %s" % self._meta.model_name
)
LOGGER.debug( LOGGER.debug(
"Created Event", "Created Event",
action=self.action, action=self.action,
@ -151,231 +139,7 @@ class Event(models.Model):
) )
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@property
def summary(self) -> str:
"""Return a summary of this event."""
if "message" in self.context:
return self.context["message"]
return f"{self.action}: {self.context}"
def __str__(self) -> str:
return f"<Event action={self.action} user={self.user} context={self.context}>"
class Meta: class Meta:
verbose_name = _("Event") verbose_name = _("Event")
verbose_name_plural = _("Events") verbose_name_plural = _("Events")
class TransportMode(models.TextChoices):
"""Modes that a notification transport can send a notification"""
WEBHOOK = "webhook", _("Generic Webhook")
WEBHOOK_SLACK = "webhook_slack", _("Slack Webhook (Slack/Discord)")
EMAIL = "email", _("Email")
class NotificationTransport(models.Model):
"""Action which is executed when a Rule matches"""
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
name = models.TextField(unique=True)
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"""
if self.mode == TransportMode.WEBHOOK:
return self.send_webhook(notification)
if self.mode == TransportMode.WEBHOOK_SLACK:
return self.send_webhook_slack(notification)
if self.mode == TransportMode.EMAIL:
return self.send_email(notification)
raise ValueError(f"Invalid mode {self.mode} set")
def send_webhook(self, notification: "Notification") -> list[str]:
"""Send notification to generic webhook"""
try:
response = post(
self.webhook_url,
json={
"body": notification.body,
"severity": notification.severity,
"user_email": notification.user.email,
"user_username": notification.user.username,
},
)
response.raise_for_status()
except RequestException as exc:
raise NotificationTransportError(exc.response.text) from exc
return [
response.status_code,
response.text,
]
def send_webhook_slack(self, notification: "Notification") -> list[str]:
"""Send notification to slack or slack-compatible endpoints"""
fields = [
{
"title": _("Severity"),
"value": notification.severity,
"short": True,
},
{
"title": _("Dispatched for user"),
"value": str(notification.user),
"short": True,
},
]
if notification.event:
for key, value in notification.event.context.items():
if not isinstance(value, str):
continue
# https://birdie0.github.io/discord-webhooks-guide/other/field_limits.html
if len(fields) >= 25:
continue
fields.append({"title": key[:256], "value": value[:1024]})
body = {
"username": "authentik",
"icon_url": "https://goauthentik.io/img/icon.png",
"attachments": [
{
"author_name": "authentik",
"author_link": "https://goauthentik.io",
"author_icon": "https://goauthentik.io/img/icon.png",
"title": notification.body,
"color": "#fd4b2d",
"fields": fields,
"footer": f"authentik v{__version__}",
}
],
}
if notification.event:
body["attachments"][0]["title"] = notification.event.action
try:
response = post(self.webhook_url, json=body)
response.raise_for_status()
except RequestException as exc:
raise NotificationTransportError(exc.response.text) from exc
return [
response.status_code,
response.text,
]
def send_email(self, notification: "Notification") -> list[str]:
"""Send notification via global email configuration"""
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=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.
try:
from authentik.stages.email.tasks import send_mail
# pyright: reportGeneralTypeIssues=false
return send_mail(mail.__dict__) # pylint: disable=no-value-for-parameter
except (SMTPException, ConnectionError, OSError) as exc:
raise NotificationTransportError from exc
def __str__(self) -> str:
return f"Notification Transport {self.name}"
class Meta:
verbose_name = _("Notification Transport")
verbose_name_plural = _("Notification Transports")
class NotificationSeverity(models.TextChoices):
"""Severity images that a notification can have"""
NOTICE = "notice", _("Notice")
WARNING = "warning", _("Warning")
ALERT = "alert", _("Alert")
class Notification(models.Model):
"""Event Notification"""
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
severity = models.TextField(choices=NotificationSeverity.choices)
body = models.TextField()
created = models.DateTimeField(auto_now_add=True)
event = models.ForeignKey(Event, on_delete=models.SET_NULL, null=True, blank=True)
seen = models.BooleanField(default=False)
user = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self) -> str:
body_trunc = (self.body[:75] + "..") if len(self.body) > 75 else self.body
return f"Notification for user {self.user}: {body_trunc}"
class Meta:
verbose_name = _("Notification")
verbose_name_plural = _("Notifications")
class NotificationRule(PolicyBindingModel):
"""Decide when to create a Notification based on policies attached to this object."""
name = models.TextField(unique=True)
transports = models.ManyToManyField(
NotificationTransport,
help_text=_(
(
"Select which transports should be used to notify the user. If none are "
"selected, the notification will only be shown in the authentik UI."
)
),
)
severity = models.TextField(
choices=NotificationSeverity.choices,
default=NotificationSeverity.NOTICE,
help_text=_(
"Controls which severity level the created notifications will have."
),
)
group = models.ForeignKey(
Group,
help_text=_(
(
"Define which group of users this notification should be sent and shown to. "
"If left empty, Notification won't ben sent."
)
),
null=True,
blank=True,
on_delete=models.SET_NULL,
)
def __str__(self) -> str:
return f"Notification Rule {self.name}"
class Meta:
verbose_name = _("Notification Rule")
verbose_name_plural = _("Notification Rules")

View File

@ -7,16 +7,12 @@ from django.contrib.auth.signals import (
user_logged_out, user_logged_out,
user_login_failed, user_login_failed,
) )
from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.http import HttpRequest from django.http import HttpRequest
from authentik.core.models import User from authentik.core.models import User
from authentik.core.signals import password_changed from authentik.core.signals import password_changed
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.events.tasks import event_notification_handler
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.invitation.models import Invitation from authentik.stages.invitation.models import Invitation
from authentik.stages.invitation.signals import invitation_used from authentik.stages.invitation.signals import invitation_used
from authentik.stages.user_write.signals import user_write from authentik.stages.user_write.signals import user_write
@ -48,11 +44,6 @@ class EventNewThread(Thread):
def on_user_logged_in(sender, request: HttpRequest, user: User, **_): def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
"""Log successful login""" """Log successful login"""
thread = EventNewThread(EventAction.LOGIN, request) thread = EventNewThread(EventAction.LOGIN, request)
if SESSION_KEY_PLAN in request.session:
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
if PLAN_CONTEXT_SOURCE in flow_plan.context:
# Login request came from an external source, save it in the context
thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE]
thread.user = user thread.user = user
thread.run() thread.run()
@ -104,10 +95,3 @@ def on_password_changed(sender, user: User, password: str, **_):
"""Log password change""" """Log password change"""
thread = EventNewThread(EventAction.PASSWORD_SET, None, user=user) thread = EventNewThread(EventAction.PASSWORD_SET, None, user=user)
thread.run() thread.run()
@receiver(post_save, sender=Event)
# pylint: disable=unused-argument
def event_post_save_notification(sender, instance: Event, **_):
"""Start task to check if any policies trigger an notification on this event"""
event_notification_handler.delay(instance.event_uuid.hex)

View File

@ -1,101 +0,0 @@
"""Event notification tasks"""
from guardian.shortcuts import get_anonymous_user
from structlog import get_logger
from authentik.events.models import (
Event,
Notification,
NotificationRule,
NotificationTransport,
NotificationTransportError,
)
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.policies.engine import PolicyEngine, PolicyEngineMode
from authentik.policies.models import PolicyBinding
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@CELERY_APP.task()
def event_notification_handler(event_uuid: str):
"""Start task for each trigger definition"""
for trigger in NotificationRule.objects.all():
event_trigger_handler.apply_async(
args=[event_uuid, trigger.name], queue="authentik_events"
)
@CELERY_APP.task()
def event_trigger_handler(event_uuid: str, trigger_name: str):
"""Check if policies attached to NotificationRule match event"""
event: Event = Event.objects.get(event_uuid=event_uuid)
trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name)
if "policy_uuid" in event.context:
policy_uuid = event.context["policy_uuid"]
if PolicyBinding.objects.filter(
target__in=NotificationRule.objects.all().values_list(
"pbm_uuid", flat=True
),
policy=policy_uuid,
).exists():
# If policy that caused this event to be created is attached
# to *any* NotificationRule, we return early.
# This is the most effective way to prevent infinite loops.
LOGGER.debug(
"e(trigger): attempting to prevent infinite loop", trigger=trigger
)
return
if not trigger.group:
LOGGER.debug("e(trigger): trigger has no group", trigger=trigger)
return
LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
policy_engine = PolicyEngine(trigger, get_anonymous_user())
policy_engine.mode = PolicyEngineMode.MODE_OR
policy_engine.empty_result = False
policy_engine.use_cache = False
policy_engine.request.context["event"] = event
policy_engine.build()
result = policy_engine.result
if not result.passing:
return
LOGGER.debug("e(trigger): event trigger matched", trigger=trigger)
# Create the notification objects
for transport in trigger.transports.all():
for user in trigger.group.users.all():
LOGGER.debug("created notif")
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(
bind=True,
autoretry_for=(NotificationTransportError,),
retry_backoff=True,
base=MonitoredTask,
)
def notification_transport(
self: MonitoredTask, notification_pk: int, transport_pk: int
):
"""Send notification over specified transport"""
self.save_on_success = False
try:
notification: Notification = Notification.objects.get(pk=notification_pk)
transport: NotificationTransport = NotificationTransport.objects.get(
pk=transport_pk
)
transport.send(notification)
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
except NotificationTransportError as exc:
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
raise exc

View File

@ -1,24 +0,0 @@
"""Event API tests"""
from django.shortcuts import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.events.models import Event, EventAction
class TestEventsAPI(APITestCase):
"""Test Event API"""
def test_top_n(self):
"""Test top_per_user"""
user = User.objects.get(username="akadmin")
self.client.force_login(user)
event = Event.new(EventAction.AUTHORIZE_APPLICATION)
event.save() # We save to ensure nothing is un-saveable
response = self.client.get(
reverse("authentik_api:event-top-per-user"),
data={"filter_action": EventAction.AUTHORIZE_APPLICATION},
)
self.assertEqual(response.status_code, 200)

View File

@ -1,10 +1,9 @@
"""event tests""" """events event tests"""
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Group
from authentik.events.models import Event from authentik.events.models import Event
from authentik.policies.dummy.models import DummyPolicy from authentik.policies.dummy.models import DummyPolicy
@ -14,24 +13,14 @@ class TestEvents(TestCase):
def test_new_with_model(self): def test_new_with_model(self):
"""Create a new Event passing a model as kwarg""" """Create a new Event passing a model as kwarg"""
test_model = Group.objects.create(name="test") event = Event.new("unittest", test={"model": get_anonymous_user()})
event = Event.new("unittest", test={"model": test_model})
event.save() # We save to ensure nothing is un-saveable event.save() # We save to ensure nothing is un-saveable
model_content_type = ContentType.objects.get_for_model(test_model) model_content_type = ContentType.objects.get_for_model(get_anonymous_user())
self.assertEqual( self.assertEqual(
event.context.get("test").get("model").get("app"), event.context.get("test").get("model").get("app"),
model_content_type.app_label, model_content_type.app_label,
) )
def test_new_with_user(self):
"""Create a new Event passing a user as kwarg"""
event = Event.new("unittest", test={"model": get_anonymous_user()})
event.save() # We save to ensure nothing is un-saveable
self.assertEqual(
event.context.get("test").get("model").get("username"),
get_anonymous_user().username,
)
def test_new_with_uuid_model(self): def test_new_with_uuid_model(self):
"""Create a new Event passing a model (with UUID PK) as kwarg""" """Create a new Event passing a model (with UUID PK) as kwarg"""
temp_model = DummyPolicy.objects.create(name="test", result=True) temp_model = DummyPolicy.objects.create(name="test", result=True)

View File

@ -1,48 +0,0 @@
"""Event Middleware tests"""
from django.shortcuts import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Application, User
from authentik.events.models import Event, EventAction
class TestEventsMiddleware(APITestCase):
"""Test Event Middleware"""
def setUp(self) -> None:
super().setUp()
self.user = User.objects.get(username="akadmin")
self.client.force_login(self.user)
def test_create(self):
"""Test model creation event"""
self.client.post(
reverse("authentik_api:application-list"),
data={"name": "test-create", "slug": "test-create"},
)
self.assertTrue(Application.objects.filter(name="test-create").exists())
self.assertTrue(
Event.objects.filter(
action=EventAction.MODEL_CREATED,
context__model__model_name="application",
context__model__app="authentik_core",
context__model__name="test-create",
).exists()
)
def test_delete(self):
"""Test model creation event"""
Application.objects.create(name="test-delete", slug="test-delete")
self.client.delete(
reverse("authentik_api:application-detail", kwargs={"slug": "test-delete"})
)
self.assertFalse(Application.objects.filter(name="test").exists())
self.assertTrue(
Event.objects.filter(
action=EventAction.MODEL_DELETED,
context__model__model_name="application",
context__model__app="authentik_core",
context__model__name="test-delete",
).exists()
)

View File

@ -1,114 +0,0 @@
"""Notification tests"""
from unittest.mock import MagicMock, patch
from django.test import TestCase
from authentik.core.models import Group, User
from authentik.events.models import (
Event,
EventAction,
Notification,
NotificationRule,
NotificationTransport,
)
from authentik.policies.event_matcher.models import EventMatcherPolicy
from authentik.policies.exceptions import PolicyException
from authentik.policies.models import PolicyBinding
class TestEventsNotifications(TestCase):
"""Test Event Notifications"""
def setUp(self) -> None:
self.group = Group.objects.create(name="test-group")
self.user = User.objects.create(name="test-user", username="test")
self.group.users.add(self.user)
self.group.save()
def test_trigger_empty(self):
"""Test trigger without any policies attached"""
transport = NotificationTransport.objects.create(name="transport")
trigger = NotificationRule.objects.create(name="trigger", group=self.group)
trigger.transports.add(transport)
trigger.save()
execute_mock = MagicMock()
with patch("authentik.events.models.NotificationTransport.send", execute_mock):
Event.new(EventAction.CUSTOM_PREFIX).save()
self.assertEqual(execute_mock.call_count, 0)
def test_trigger_single(self):
"""Test simple transport triggering"""
transport = NotificationTransport.objects.create(name="transport")
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(execute_mock.call_count, 1)
def test_trigger_no_group(self):
"""Test trigger without group"""
trigger = NotificationRule.objects.create(name="trigger")
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(execute_mock.call_count, 0)
def test_policy_error_recursive(self):
"""Test Policy error which would cause recursion"""
transport = NotificationTransport.objects.create(name="transport")
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()
passes = MagicMock(side_effect=PolicyException)
with patch(
"authentik.policies.event_matcher.models.EventMatcherPolicy.passes", passes
):
with patch(
"authentik.events.models.NotificationTransport.send", execute_mock
):
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)

View File

@ -5,15 +5,12 @@ from typing import Any, Dict, Optional
from uuid import UUID from uuid import UUID
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.handlers.wsgi import WSGIRequest
from django.db import models from django.db import models
from django.db.models.base import Model from django.db.models.base import Model
from django.http.request import HttpRequest
from django.views.debug import SafeExceptionReporterFilter from django.views.debug import SafeExceptionReporterFilter
from guardian.utils import get_anonymous_user from guardian.utils import get_anonymous_user
from authentik.core.models import User from authentik.core.models import User
from authentik.policies.types import PolicyRequest
# Special keys which are *not* cleaned, even when the default filter # Special keys which are *not* cleaned, even when the default filter
# is matched # is matched
@ -77,22 +74,13 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
final_dict = {} final_dict = {}
for key, value in source.items(): for key, value in source.items():
if is_dataclass(value): if is_dataclass(value):
# Because asdict calls `copy.deepcopy(obj)` on everything thats not tuple/dict,
# and deepcopy doesn't work with HttpRequests (neither django nor rest_framework).
# Currently, the only dataclass that actually holds an http request is a PolicyRequest
if isinstance(value, PolicyRequest):
value.http_request = None
value = asdict(value) value = asdict(value)
if isinstance(value, dict): if isinstance(value, dict):
final_dict[key] = sanitize_dict(value) final_dict[key] = sanitize_dict(value)
elif isinstance(value, User):
final_dict[key] = sanitize_dict(get_user(value))
elif isinstance(value, models.Model): elif isinstance(value, models.Model):
final_dict[key] = sanitize_dict(model_to_dict(value)) final_dict[key] = sanitize_dict(model_to_dict(value))
elif isinstance(value, UUID): elif isinstance(value, UUID):
final_dict[key] = value.hex final_dict[key] = value.hex
elif isinstance(value, (HttpRequest, WSGIRequest)):
continue
else: else:
final_dict[key] = value final_dict[key] = value
return final_dict return final_dict

View File

@ -1,11 +1,9 @@
"""flow exceptions""" """flow exceptions"""
from authentik.lib.sentry import SentryIgnoredException
class FlowNonApplicableException(BaseException):
class FlowNonApplicableException(SentryIgnoredException):
"""Flow does not apply to current user (denied by policy).""" """Flow does not apply to current user (denied by policy)."""
class EmptyFlowException(SentryIgnoredException): class EmptyFlowException(BaseException):
"""Flow has no stages.""" """Flow has no stages."""

View File

@ -1,13 +1,13 @@
"""authentik benchmark command""" """authentik benchmark command"""
from csv import DictWriter from csv import DictWriter
from multiprocessing import Manager, cpu_count, get_context from multiprocessing import Manager, Process, cpu_count
from sys import stdout from sys import stdout
from time import time from time import time
from django import db from django import db
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.test import RequestFactory from django.test import RequestFactory
from structlog.stdlib import get_logger from structlog import get_logger
from authentik import __version__ from authentik import __version__
from authentik.core.models import User from authentik.core.models import User
@ -15,11 +15,9 @@ from authentik.flows.models import Flow
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
LOGGER = get_logger() LOGGER = get_logger()
FORK_CTX = get_context("fork")
PROCESS_CLASS = FORK_CTX.Process
class FlowPlanProcess(PROCESS_CLASS): # pragma: no cover class FlowPlanProcess(Process): # pragma: no cover
"""Test process which executes flow planner""" """Test process which executes flow planner"""
def __init__(self, index, return_dict, flow, user) -> None: def __init__(self, index, return_dict, flow, user) -> None:

View File

@ -3,7 +3,7 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from django.http.request import HttpRequest from django.http.request import HttpRequest
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.models import Stage from authentik.flows.models import Stage

View File

@ -8,7 +8,7 @@ from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.lib.models import InheritanceForeignKey, SerializerModel from authentik.lib.models import InheritanceForeignKey, SerializerModel
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel

View File

@ -6,7 +6,7 @@ from django.core.cache import cache
from django.http import HttpRequest from django.http import HttpRequest
from sentry_sdk.hub import Hub from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger from structlog import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import cleanse_dict from authentik.events.models import cleanse_dict
@ -16,11 +16,11 @@ from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
LOGGER = get_logger() LOGGER = get_logger()
PLAN_CONTEXT_PENDING_USER = "pending_user" PLAN_CONTEXT_PENDING_USER = "pending_user"
PLAN_CONTEXT_SSO = "is_sso" PLAN_CONTEXT_SSO = "is_sso"
PLAN_CONTEXT_REDIRECT = "redirect" PLAN_CONTEXT_REDIRECT = "redirect"
PLAN_CONTEXT_APPLICATION = "application" PLAN_CONTEXT_APPLICATION = "application"
PLAN_CONTEXT_SOURCE = "source"
def cache_key(flow: Flow, user: Optional[User] = None) -> str: def cache_key(flow: Flow, user: Optional[User] = None) -> str:
@ -87,13 +87,10 @@ class FlowPlanner:
flow: Flow flow: Flow
_logger: BoundLogger
def __init__(self, flow: Flow): def __init__(self, flow: Flow):
self.use_cache = True self.use_cache = True
self.allow_empty_flows = False self.allow_empty_flows = False
self.flow = flow self.flow = flow
self._logger = get_logger().bind(flow=flow)
def plan( def plan(
self, request: HttpRequest, default_context: Optional[Dict[str, Any]] = None self, request: HttpRequest, default_context: Optional[Dict[str, Any]] = None
@ -105,9 +102,7 @@ class FlowPlanner:
span.set_data("flow", self.flow) span.set_data("flow", self.flow)
span.set_data("request", request) span.set_data("request", request)
self._logger.debug( LOGGER.debug("f(plan): Starting planning process", flow=self.flow)
"f(plan): starting planning process",
)
# Bit of a workaround here, if there is a pending user set in the default context # Bit of a workaround here, if there is a pending user set in the default context
# we use that user for our cache key # we use that user for our cache key
# to make sure they don't get the generic response # to make sure they don't get the generic response
@ -124,21 +119,20 @@ class FlowPlanner:
engine.build() engine.build()
result = engine.result result = engine.result
if not result.passing: if not result.passing:
raise FlowNonApplicableException(",".join(result.messages)) raise FlowNonApplicableException(result.messages)
# User is passing so far, check if we have a cached plan # User is passing so far, check if we have a cached plan
cached_plan_key = cache_key(self.flow, user) cached_plan_key = cache_key(self.flow, user)
cached_plan = cache.get(cached_plan_key, None) cached_plan = cache.get(cached_plan_key, None)
if cached_plan and self.use_cache: if cached_plan and self.use_cache:
self._logger.debug( LOGGER.debug(
"f(plan): taking plan from cache", "f(plan): Taking plan from cache",
flow=self.flow,
key=cached_plan_key, key=cached_plan_key,
) )
# Reset the context as this isn't factored into caching # Reset the context as this isn't factored into caching
cached_plan.context = default_context or {} cached_plan.context = default_context or {}
return cached_plan return cached_plan
self._logger.debug( LOGGER.debug("f(plan): building plan", flow=self.flow)
"f(plan): building plan",
)
plan = self._build_plan(user, request, default_context) plan = self._build_plan(user, request, default_context)
cache.set(cache_key(self.flow, user), plan) cache.set(cache_key(self.flow, user), plan)
if not plan.stages and not self.allow_empty_flows: if not plan.stages and not self.allow_empty_flows:
@ -170,34 +164,39 @@ class FlowPlanner:
stage = binding.stage stage = binding.stage
marker = StageMarker() marker = StageMarker()
if binding.evaluate_on_plan: if binding.evaluate_on_plan:
self._logger.debug( LOGGER.debug(
"f(plan): evaluating on plan", "f(plan): evaluating on plan",
stage=binding.stage, stage=binding.stage,
flow=self.flow,
) )
engine = PolicyEngine(binding, user, request) engine = PolicyEngine(binding, user, request)
engine.request.context = plan.context engine.request.context = plan.context
engine.build() engine.build()
if engine.passing: if engine.passing:
self._logger.debug( LOGGER.debug(
"f(plan): stage passing", "f(plan): Stage passing",
stage=binding.stage, stage=binding.stage,
flow=self.flow,
) )
else: else:
stage = None stage = None
else: else:
self._logger.debug( LOGGER.debug(
"f(plan): not evaluating on plan", "f(plan): not evaluating on plan",
stage=binding.stage, stage=binding.stage,
flow=self.flow,
) )
if binding.re_evaluate_policies and stage: if binding.re_evaluate_policies and stage:
self._logger.debug( LOGGER.debug(
"f(plan): stage has re-evaluate marker", "f(plan): Stage has re-evaluate marker",
stage=binding.stage, stage=binding.stage,
flow=self.flow,
) )
marker = ReevaluateMarker(binding=binding, user=user) marker = ReevaluateMarker(binding=binding, user=user)
if stage: if stage:
plan.append(stage, marker) plan.append(stage, marker)
self._logger.debug( LOGGER.debug(
"f(plan): finished building", "f(plan): Finished building",
flow=self.flow,
) )
return plan return plan

View File

@ -2,7 +2,7 @@
from django.core.cache import cache from django.core.cache import cache
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from structlog.stdlib import get_logger from structlog import get_logger
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -15,7 +15,7 @@ from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from structlog.stdlib import BoundLogger, get_logger from structlog import get_logger
from authentik.core.models import USER_ATTRIBUTE_DEBUG from authentik.core.models import USER_ATTRIBUTE_DEBUG
from authentik.events.models import cleanse_dict from authentik.events.models import cleanse_dict
@ -49,48 +49,45 @@ class FlowExecutorView(View):
current_stage: Stage current_stage: Stage
current_stage_view: View current_stage_view: View
_logger: BoundLogger
def setup(self, request: HttpRequest, flow_slug: str): def setup(self, request: HttpRequest, flow_slug: str):
super().setup(request, flow_slug=flow_slug) super().setup(request, flow_slug=flow_slug)
self.flow = get_object_or_404(Flow.objects.select_related(), 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: def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
"""When a flow is non-applicable check if user is on the correct domain""" """When a flow is non-applicable check if user is on the correct domain"""
if NEXT_ARG_NAME in self.request.GET: if NEXT_ARG_NAME in self.request.GET:
if not is_url_absolute(self.request.GET.get(NEXT_ARG_NAME)): if not is_url_absolute(self.request.GET.get(NEXT_ARG_NAME)):
self._logger.debug("f(exec): Redirecting to next on fail") LOGGER.debug("f(exec): Redirecting to next on fail")
return redirect(self.request.GET.get(NEXT_ARG_NAME)) return redirect(self.request.GET.get(NEXT_ARG_NAME))
message = exc.__doc__ if exc.__doc__ else str(exc) message = exc.__doc__ if exc.__doc__ else str(exc)
return self.stage_invalid(error_message=message) return self.stage_invalid(error_message=message)
# pylint: disable=unused-argument
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse: def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
# Early check if theres an active Plan for the current session # Early check if theres an active Plan for the current session
if SESSION_KEY_PLAN in self.request.session: if SESSION_KEY_PLAN in self.request.session:
self.plan = self.request.session[SESSION_KEY_PLAN] self.plan = self.request.session[SESSION_KEY_PLAN]
if self.plan.flow_pk != self.flow.pk.hex: if self.plan.flow_pk != self.flow.pk.hex:
self._logger.warning( LOGGER.warning(
"f(exec): Found existing plan for other flow, deleteing plan", "f(exec): Found existing plan for other flow, deleteing plan",
flow_slug=flow_slug,
) )
# Existing plan is deleted from session and instance # Existing plan is deleted from session and instance
self.plan = None self.plan = None
self.cancel() self.cancel()
self._logger.debug("f(exec): Continuing existing plan") LOGGER.debug("f(exec): Continuing existing plan", flow_slug=flow_slug)
# Don't check session again as we've either already loaded the plan or we need to plan # Don't check session again as we've either already loaded the plan or we need to plan
if not self.plan: if not self.plan:
self._logger.debug("f(exec): No active Plan found, initiating planner") LOGGER.debug(
"f(exec): No active Plan found, initiating planner", flow_slug=flow_slug
)
try: try:
self.plan = self._initiate_plan() self.plan = self._initiate_plan()
except FlowNonApplicableException as exc: except FlowNonApplicableException as exc:
self._logger.warning( LOGGER.warning("f(exec): Flow not applicable to current user", exc=exc)
"f(exec): Flow not applicable to current user", exc=exc
)
return to_stage_response(self.request, self.handle_invalid_flow(exc)) return to_stage_response(self.request, self.handle_invalid_flow(exc))
except EmptyFlowException as exc: except EmptyFlowException as exc:
self._logger.warning("f(exec): Flow is empty", exc=exc) LOGGER.warning("f(exec): Flow is empty", exc=exc)
# To match behaviour with loading an empty flow plan from cache, # To match behaviour with loading an empty flow plan from cache,
# we don't show an error message here, but rather call _flow_done() # we don't show an error message here, but rather call _flow_done()
return self._flow_done() return self._flow_done()
@ -98,10 +95,10 @@ class FlowExecutorView(View):
# as it hasn't been successfully passed yet # as it hasn't been successfully passed yet
next_stage = self.plan.next(self.request) next_stage = self.plan.next(self.request)
if not next_stage: if not next_stage:
self._logger.debug("f(exec): no more stages, flow is done.") LOGGER.debug("f(exec): no more stages, flow is done.")
return self._flow_done() return self._flow_done()
self.current_stage = next_stage self.current_stage = next_stage
self._logger.debug( LOGGER.debug(
"f(exec): Current stage", "f(exec): Current stage",
current_stage=self.current_stage, current_stage=self.current_stage,
flow_slug=self.flow.slug, flow_slug=self.flow.slug,
@ -115,30 +112,32 @@ class FlowExecutorView(View):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""pass get request to current stage""" """pass get request to current stage"""
self._logger.debug( LOGGER.debug(
"f(exec): Passing GET", "f(exec): Passing GET",
view_class=class_to_path(self.current_stage_view.__class__), view_class=class_to_path(self.current_stage_view.__class__),
stage=self.current_stage, stage=self.current_stage,
flow_slug=self.flow.slug,
) )
try: try:
stage_response = self.current_stage_view.get(request, *args, **kwargs) stage_response = self.current_stage_view.get(request, *args, **kwargs)
return to_stage_response(request, stage_response) return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
self._logger.exception(exc) LOGGER.exception(exc)
return to_stage_response(request, FlowErrorResponse(request, exc)) return to_stage_response(request, FlowErrorResponse(request, exc))
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""pass post request to current stage""" """pass post request to current stage"""
self._logger.debug( LOGGER.debug(
"f(exec): Passing POST", "f(exec): Passing POST",
view_class=class_to_path(self.current_stage_view.__class__), view_class=class_to_path(self.current_stage_view.__class__),
stage=self.current_stage, stage=self.current_stage,
flow_slug=self.flow.slug,
) )
try: try:
stage_response = self.current_stage_view.post(request, *args, **kwargs) stage_response = self.current_stage_view.post(request, *args, **kwargs)
return to_stage_response(request, stage_response) return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
self._logger.exception(exc) LOGGER.exception(exc)
return to_stage_response(request, FlowErrorResponse(request, exc)) return to_stage_response(request, FlowErrorResponse(request, exc))
def _initiate_plan(self) -> FlowPlan: def _initiate_plan(self) -> FlowPlan:
@ -164,23 +163,26 @@ class FlowExecutorView(View):
def stage_ok(self) -> HttpResponse: def stage_ok(self) -> HttpResponse:
"""Callback called by stages upon successful completion. """Callback called by stages upon successful completion.
Persists updated plan and context to session.""" Persists updated plan and context to session."""
self._logger.debug( LOGGER.debug(
"f(exec): Stage ok", "f(exec): Stage ok",
stage_class=class_to_path(self.current_stage_view.__class__), stage_class=class_to_path(self.current_stage_view.__class__),
flow_slug=self.flow.slug,
) )
self.plan.pop() self.plan.pop()
self.request.session[SESSION_KEY_PLAN] = self.plan self.request.session[SESSION_KEY_PLAN] = self.plan
if self.plan.stages: if self.plan.stages:
self._logger.debug( LOGGER.debug(
"f(exec): Continuing with next stage", "f(exec): Continuing with next stage",
reamining=len(self.plan.stages), reamining=len(self.plan.stages),
flow_slug=self.flow.slug,
) )
return redirect_with_qs( return redirect_with_qs(
"authentik_flows:flow-executor", self.request.GET, **self.kwargs "authentik_flows:flow-executor", self.request.GET, **self.kwargs
) )
# User passed all stages # User passed all stages
self._logger.debug( LOGGER.debug(
"f(exec): User passed all stages", "f(exec): User passed all stages",
flow_slug=self.flow.slug,
context=cleanse_dict(self.plan.context), context=cleanse_dict(self.plan.context),
) )
return self._flow_done() return self._flow_done()
@ -191,7 +193,7 @@ class FlowExecutorView(View):
Optionally, an exception can be passed, which will be shown if the current user Optionally, an exception can be passed, which will be shown if the current user
is a superuser.""" is a superuser."""
self._logger.debug("f(exec): Stage invalid") LOGGER.debug("f(exec): Stage invalid", flow_slug=self.flow.slug)
self.cancel() self.cancel()
response = AccessDeniedResponse( response = AccessDeniedResponse(
self.request, template="flows/denied_shell.html" self.request, template="flows/denied_shell.html"

View File

@ -5,15 +5,13 @@ from contextlib import contextmanager
from glob import glob from glob import glob
from json import dumps from json import dumps
from time import time from time import time
from typing import Any from typing import Any, Dict
from urllib.parse import urlparse from urllib.parse import urlparse
import yaml import yaml
from django.conf import ImproperlyConfigured from django.conf import ImproperlyConfigured
from django.http import HttpRequest from django.http import HttpRequest
from authentik import __version__
SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob( SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob(
"/etc/authentik/config.d/*.yml", recursive=True "/etc/authentik/config.d/*.yml", recursive=True
) )
@ -21,9 +19,10 @@ ENV_PREFIX = "AUTHENTIK"
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local") ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
def context_processor(request: HttpRequest) -> dict[str, Any]: def context_processor(request: HttpRequest) -> Dict[str, Any]:
"""Context Processor that injects config object into every template""" """Context Processor that injects config object into every template"""
return {"config": CONFIG.raw, "ak_version": __version__} kwargs = {"config": CONFIG.raw}
return kwargs
class ConfigLoader: class ConfigLoader:

View File

@ -21,17 +21,6 @@ error_reporting:
environment: customer environment: customer
send_pii: false send_pii: false
# Global email settings
email:
host: localhost
port: 25
username: ""
password: ""
use_tls: false
use_ssl: false
timeout: 10
from: authentik@localhost
outposts: outposts:
docker_image_base: "beryju/authentik" # this is prepended to -proxy:version docker_image_base: "beryju/authentik" # this is prepended to -proxy:version

View File

@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
from requests import Session from requests import Session
from sentry_sdk.hub import Hub from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.core.models import User from authentik.core.models import User
@ -60,7 +60,7 @@ class BaseEvaluator:
@staticmethod @staticmethod
def expr_func_is_group_member(user: User, **group_filters) -> bool: def expr_func_is_group_member(user: User, **group_filters) -> bool:
"""Check if `user` is member of group with name `group_name`""" """Check if `user` is member of group with name `group_name`"""
return user.ak_groups.filter(**group_filters).exists() return user.groups.filter(**group_filters).exists()
def wrap_expression(self, expression: str, params: Iterable[str]) -> str: def wrap_expression(self, expression: str, params: Iterable[str]) -> str:
"""Wrap expression in a function, call it, and save the result as `result`""" """Wrap expression in a function, call it, and save the result as `result`"""

View File

@ -1,6 +1,7 @@
"""logging helpers""" """logging helpers"""
from logging import Logger from logging import Logger
from os import getpid from os import getpid
from typing import Callable
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -8,3 +9,15 @@ def add_process_id(logger: Logger, method_name: str, event_dict):
"""Add the current process ID""" """Add the current process ID"""
event_dict["pid"] = getpid() event_dict["pid"] = getpid()
return event_dict 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

View File

@ -11,7 +11,7 @@ from ldap3.core.exceptions import LDAPException
from redis.exceptions import ConnectionError as RedisConnectionError from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import RedisError, ResponseError from redis.exceptions import RedisError, ResponseError
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from structlog.stdlib import get_logger from structlog import get_logger
from websockets.exceptions import WebSocketException from websockets.exceptions import WebSocketException
LOGGER = get_logger() LOGGER = get_logger()
@ -59,5 +59,6 @@ def before_send(event, hint):
if "exc_info" in hint: if "exc_info" in hint:
_, exc_value, _ = hint["exc_info"] _, exc_value, _ = hint["exc_info"]
if isinstance(exc_value, ignored_classes): if isinstance(exc_value, ignored_classes):
LOGGER.info("Supressing error %r", exc_value)
return None return None
return event return event

View File

@ -8,8 +8,6 @@ from typing import Any, Dict, List, Optional
from celery import Task from celery import Task
from django.core.cache import cache from django.core.cache import cache
from authentik.events.models import Event, EventAction
class TaskResultStatus(Enum): class TaskResultStatus(Enum):
"""Possible states of tasks""" """Possible states of tasks"""
@ -54,11 +52,6 @@ class TaskInfo:
task_description: Optional[str] = field(default=None) task_description: Optional[str] = field(default=None)
@property
def html_name(self) -> list[str]:
"""Get task_name, but split on underscores, so we can join in the html template."""
return self.task_name.split("_")
@staticmethod @staticmethod
def all() -> Dict[str, "TaskInfo"]: def all() -> Dict[str, "TaskInfo"]:
"""Get all TaskInfo objects""" """Get all TaskInfo objects"""
@ -140,13 +133,6 @@ class MonitoredTask(Task):
task_call_args=args, task_call_args=args,
task_call_kwargs=kwargs, task_call_kwargs=kwargs,
).save(self.result_timeout_hours) ).save(self.result_timeout_hours)
Event.new(
EventAction.SYSTEM_TASK_EXCEPTION,
message=(
f"Task {self.__name__} encountered an error: "
"\n".join(self._result.messages)
),
).save()
return super().on_failure(exc, task_id, args, kwargs, einfo=einfo) return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
def run(self, *args, **kwargs): def run(self, *args, **kwargs):

View File

@ -8,7 +8,7 @@ from django.http.request import HttpRequest
from django.template import Context from django.template import Context
from django.templatetags.static import static from django.templatetags.static import static
from django.utils.html import escape, mark_safe from django.utils.html import escape, mark_safe
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG

View File

@ -5,7 +5,7 @@ from django.http import HttpResponse
from django.shortcuts import redirect, reverse from django.shortcuts import redirect, reverse
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
from django.utils.http import urlencode from django.utils.http import urlencode
from structlog.stdlib import get_logger from structlog import get_logger
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -1,16 +0,0 @@
"""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

View File

@ -1,56 +0,0 @@
"""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

View File

@ -1,31 +0,0 @@
"""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

View File

@ -1,10 +0,0 @@
"""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"},
},
}

View File

@ -1,20 +0,0 @@
"""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)]))

View File

@ -12,7 +12,7 @@ from django.db import ProgrammingError
from docker.constants import DEFAULT_UNIX_SOCKET from docker.constants import DEFAULT_UNIX_SOCKET
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
from structlog.stdlib import get_logger from structlog import get_logger
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -8,7 +8,7 @@ from channels.exceptions import DenyConnection
from dacite import from_dict from dacite import from_dict
from dacite.data import Data from dacite.data import Data
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.core.channels import AuthJsonConsumer from authentik.core.channels import AuthJsonConsumer
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState

View File

@ -1,32 +1,21 @@
"""Base Controller""" """Base Controller"""
from dataclasses import dataclass from typing import Dict, List
from structlog.stdlib import get_logger from structlog import get_logger
from structlog.testing import capture_logs from structlog.testing import capture_logs
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.outposts.models import Outpost, OutpostServiceConnection from authentik.outposts.models import Outpost, OutpostServiceConnection
FIELD_MANAGER = "goauthentik.io"
class ControllerException(SentryIgnoredException): class ControllerException(SentryIgnoredException):
"""Exception raised when anything fails during controller run""" """Exception raised when anything fails during controller run"""
@dataclass
class DeploymentPort:
"""Info about deployment's single port."""
port: int
name: str
protocol: str
class BaseController: class BaseController:
"""Base Outpost deployment controller""" """Base Outpost deployment controller"""
deployment_ports: list[DeploymentPort] deployment_ports: Dict[str, int]
outpost: Outpost outpost: Outpost
connection: OutpostServiceConnection connection: OutpostServiceConnection
@ -35,14 +24,14 @@ class BaseController:
self.outpost = outpost self.outpost = outpost
self.connection = connection self.connection = connection
self.logger = get_logger() self.logger = get_logger()
self.deployment_ports = [] self.deployment_ports = {}
# pylint: disable=invalid-name # pylint: disable=invalid-name
def up(self): def up(self):
"""Called by scheduled task to reconcile deployment/service/etc""" """Called by scheduled task to reconcile deployment/service/etc"""
raise NotImplementedError raise NotImplementedError
def up_with_logs(self) -> list[str]: def up_with_logs(self) -> List[str]:
"""Call .up() but capture all log output and return it.""" """Call .up() but capture all log output and return it."""
with capture_logs() as logs: with capture_logs() as logs:
self.up() self.up()

View File

@ -68,10 +68,7 @@ class DockerController(BaseController):
"image": image_name, "image": image_name,
"name": f"authentik-proxy-{self.outpost.uuid.hex}", "name": f"authentik-proxy-{self.outpost.uuid.hex}",
"detach": True, "detach": True,
"ports": { "ports": {x: x for _, x in self.deployment_ports.items()},
f"{port.port}/{port.protocol.lower()}": port.port
for port in self.deployment_ports
},
"environment": self._get_env(), "environment": self._get_env(),
"labels": self._get_labels(), "labels": self._get_labels(),
} }
@ -142,10 +139,7 @@ class DockerController(BaseController):
def get_static_deployment(self) -> str: def get_static_deployment(self) -> str:
"""Generate docker-compose yaml for proxy, version 3.5""" """Generate docker-compose yaml for proxy, version 3.5"""
ports = [ ports = [f"{x}:{x}" for _, x in self.deployment_ports.items()]
f"{port.port}:{port.port}/{port.protocol.lower()}"
for port in self.deployment_ports
]
image_prefix = CONFIG.y("outposts.docker_image_base") image_prefix = CONFIG.y("outposts.docker_image_base")
compose = { compose = {
"version": "3.5", "version": "3.5",

View File

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Generic, TypeVar
from kubernetes.client import V1ObjectMeta from kubernetes.client import V1ObjectMeta
from kubernetes.client.rest import ApiException from kubernetes.client.rest import ApiException
from structlog.stdlib import get_logger from structlog import get_logger
from authentik import __version__ from authentik import __version__
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
@ -93,8 +93,7 @@ class KubernetesObjectReconciler(Generic[T]):
def reconcile(self, current: T, reference: T): def reconcile(self, current: T, reference: T):
"""Check what operations should be done, should be raised as """Check what operations should be done, should be raised as
ReconcileTrigger""" ReconcileTrigger"""
if current.metadata.labels != reference.metadata.labels: raise NotImplementedError
raise NeedsUpdate()
def create(self, reference: T): def create(self, reference: T):
"""API Wrapper to create object""" """API Wrapper to create object"""

View File

@ -18,7 +18,6 @@ from kubernetes.client import (
from authentik import __version__ from authentik import __version__
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import ( from authentik.outposts.controllers.k8s.base import (
KubernetesObjectReconciler, KubernetesObjectReconciler,
NeedsUpdate, NeedsUpdate,
@ -44,7 +43,6 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
return f"authentik-outpost-{self.controller.outpost.uuid.hex}" return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
def reconcile(self, current: V1Deployment, reference: V1Deployment): def reconcile(self, current: V1Deployment, reference: V1Deployment):
super().reconcile(current, reference)
if current.spec.replicas != reference.spec.replicas: if current.spec.replicas != reference.spec.replicas:
raise NeedsUpdate() raise NeedsUpdate()
if ( if (
@ -65,14 +63,8 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
"""Get deployment object for outpost""" """Get deployment object for outpost"""
# Generate V1ContainerPort objects # Generate V1ContainerPort objects
container_ports = [] container_ports = []
for port in self.controller.deployment_ports: for port_name, port in self.controller.deployment_ports.items():
container_ports.append( container_ports.append(V1ContainerPort(container_port=port, name=port_name))
V1ContainerPort(
container_port=port.port,
name=port.name,
protocol=port.protocol.upper(),
)
)
meta = self.get_object_meta(name=self.name) meta = self.get_object_meta(name=self.name)
secret_name = f"authentik-outpost-{self.controller.outpost.uuid.hex}-api" secret_name = f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
image_prefix = CONFIG.y("outposts.docker_image_base") image_prefix = CONFIG.y("outposts.docker_image_base")
@ -126,9 +118,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
) )
def create(self, reference: V1Deployment): def create(self, reference: V1Deployment):
return self.api.create_namespaced_deployment( return self.api.create_namespaced_deployment(self.namespace, reference)
self.namespace, reference, field_manager=FIELD_MANAGER
)
def delete(self, reference: V1Deployment): def delete(self, reference: V1Deployment):
return self.api.delete_namespaced_deployment( return self.api.delete_namespaced_deployment(
@ -140,8 +130,5 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
def update(self, current: V1Deployment, reference: V1Deployment): def update(self, current: V1Deployment, reference: V1Deployment):
return self.api.patch_namespaced_deployment( return self.api.patch_namespaced_deployment(
current.metadata.name, current.metadata.name, self.namespace, reference
self.namespace,
reference,
field_manager=FIELD_MANAGER,
) )

View File

@ -4,7 +4,6 @@ from typing import TYPE_CHECKING
from kubernetes.client import CoreV1Api, V1Secret from kubernetes.client import CoreV1Api, V1Secret
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import ( from authentik.outposts.controllers.k8s.base import (
KubernetesObjectReconciler, KubernetesObjectReconciler,
NeedsUpdate, NeedsUpdate,
@ -31,7 +30,6 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
return f"authentik-outpost-{self.controller.outpost.uuid.hex}-api" return f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
def reconcile(self, current: V1Secret, reference: V1Secret): def reconcile(self, current: V1Secret, reference: V1Secret):
super().reconcile(current, reference)
for key in reference.data.keys(): for key in reference.data.keys():
if current.data[key] != reference.data[key]: if current.data[key] != reference.data[key]:
raise NeedsUpdate() raise NeedsUpdate()
@ -53,9 +51,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
) )
def create(self, reference: V1Secret): def create(self, reference: V1Secret):
return self.api.create_namespaced_secret( return self.api.create_namespaced_secret(self.namespace, reference)
self.namespace, reference, field_manager=FIELD_MANAGER
)
def delete(self, reference: V1Secret): def delete(self, reference: V1Secret):
return self.api.delete_namespaced_secret( return self.api.delete_namespaced_secret(
@ -67,8 +63,5 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
def update(self, current: V1Secret, reference: V1Secret): def update(self, current: V1Secret, reference: V1Secret):
return self.api.patch_namespaced_secret( return self.api.patch_namespaced_secret(
current.metadata.name, current.metadata.name, self.namespace, reference
self.namespace,
reference,
field_manager=FIELD_MANAGER,
) )

View File

@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import ( from authentik.outposts.controllers.k8s.base import (
KubernetesObjectReconciler, KubernetesObjectReconciler,
NeedsUpdate, NeedsUpdate,
@ -26,7 +25,6 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
return f"authentik-outpost-{self.controller.outpost.uuid.hex}" return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
def reconcile(self, current: V1Service, reference: V1Service): def reconcile(self, current: V1Service, reference: V1Service):
super().reconcile(current, reference)
if len(current.spec.ports) != len(reference.spec.ports): if len(current.spec.ports) != len(reference.spec.ports):
raise NeedsUpdate() raise NeedsUpdate()
for port in reference.spec.ports: for port in reference.spec.ports:
@ -37,15 +35,8 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
"""Get deployment object for outpost""" """Get deployment object for outpost"""
meta = self.get_object_meta(name=self.name) meta = self.get_object_meta(name=self.name)
ports = [] ports = []
for port in self.controller.deployment_ports: for port_name, port in self.controller.deployment_ports.items():
ports.append( ports.append(V1ServicePort(name=port_name, port=port))
V1ServicePort(
name=port.name,
port=port.port,
protocol=port.protocol.upper(),
target_port=port.port,
)
)
selector_labels = DeploymentReconciler(self.controller).get_pod_meta() selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
return V1Service( return V1Service(
metadata=meta, metadata=meta,
@ -53,9 +44,7 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
) )
def create(self, reference: V1Service): def create(self, reference: V1Service):
return self.api.create_namespaced_service( return self.api.create_namespaced_service(self.namespace, reference)
self.namespace, reference, field_manager=FIELD_MANAGER
)
def delete(self, reference: V1Service): def delete(self, reference: V1Service):
return self.api.delete_namespaced_service( return self.api.delete_namespaced_service(
@ -67,8 +56,5 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
def update(self, current: V1Service, reference: V1Service): def update(self, current: V1Service, reference: V1Service):
return self.api.patch_namespaced_service( return self.api.patch_namespaced_service(
current.metadata.name, current.metadata.name, self.namespace, reference
self.namespace,
reference,
field_manager=FIELD_MANAGER,
) )

View File

@ -24,7 +24,7 @@ from kubernetes.config.incluster_config import load_incluster_config
from kubernetes.config.kube_config import load_kube_config_from_dict from kubernetes.config.kube_config import load_kube_config_from_dict
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from packaging.version import LegacyVersion, Version, parse from packaging.version import LegacyVersion, Version, parse
from structlog.stdlib import get_logger from structlog import get_logger
from urllib3.exceptions import HTTPError from urllib3.exceptions import HTTPError
from authentik import __version__ from authentik import __version__
@ -363,7 +363,6 @@ class Outpost(models.Model):
intent=TokenIntents.INTENT_API, intent=TokenIntents.INTENT_API,
description=f"Autogenerated by authentik for Outpost {self.name}", description=f"Autogenerated by authentik for Outpost {self.name}",
expiring=False, expiring=False,
managed="goauthentik.io/outpost",
) )
def get_required_objects(self) -> Iterable[models.Model]: def get_required_objects(self) -> Iterable[models.Model]:

View File

@ -2,24 +2,17 @@
from django.db.models import Model from django.db.models import Model
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.core.models import Provider
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.utils.reflection import class_to_path from authentik.lib.utils.reflection import class_to_path
from authentik.outposts.models import Outpost, OutpostServiceConnection from authentik.outposts.models import Outpost
from authentik.outposts.tasks import outpost_post_save, outpost_pre_delete from authentik.outposts.tasks import outpost_post_save, outpost_pre_delete
LOGGER = get_logger() LOGGER = get_logger()
UPDATE_TRIGGERING_MODELS = (
Outpost,
OutpostServiceConnection,
Provider,
CertificateKeyPair,
)
@receiver(post_save) @receiver(post_save)
# pylint: disable=unused-argument
def post_save_update(sender, instance: Model, **_): def post_save_update(sender, instance: Model, **_):
"""If an Outpost is saved, Ensure that token is created/updated """If an Outpost is saved, Ensure that token is created/updated
@ -29,8 +22,6 @@ def post_save_update(sender, instance: Model, **_):
return return
if instance.__module__ == "__fake__": if instance.__module__ == "__fake__":
return return
if sender not in UPDATE_TRIGGERING_MODELS:
return
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk) outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)

View File

@ -6,9 +6,9 @@ from channels.layers import get_channel_layer
from django.core.cache import cache from django.core.cache import cache
from django.db.models.base import Model from django.db.models.base import Model
from django.utils.text import slugify from django.utils.text import slugify
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.lib.utils.reflection import path_to_class from authentik.lib.utils.reflection import path_to_class
from authentik.outposts.controllers.base import ControllerException from authentik.outposts.controllers.base import ControllerException
from authentik.outposts.models import ( from authentik.outposts.models import (
@ -49,15 +49,9 @@ def outpost_service_connection_state(connection_pk: Any):
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=MonitoredTask)
def outpost_service_connection_monitor(self: MonitoredTask): def outpost_service_connection_monitor(self: MonitoredTask):
"""Regularly check the state of Outpost Service Connections""" """Regularly check the state of Outpost Service Connections"""
connections = OutpostServiceConnection.objects.all() for connection in OutpostServiceConnection.objects.all():
for connection in connections.iterator():
outpost_service_connection_state.delay(connection.pk) outpost_service_connection_state.delay(connection.pk)
self.set_status( self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
TaskResult(
TaskResultStatus.SUCCESSFUL,
[f"Successfully updated {len(connections)} connections."],
)
)
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=MonitoredTask)
@ -130,12 +124,14 @@ def outpost_post_save(model_class: str, model_pk: Any):
_ = instance.token _ = instance.token
LOGGER.debug("Trigger reconcile for outpost") LOGGER.debug("Trigger reconcile for outpost")
outpost_controller.delay(instance.pk) outpost_controller.delay(instance.pk)
return
if isinstance(instance, (OutpostModel, Outpost)): if isinstance(instance, (OutpostModel, Outpost)):
LOGGER.debug( LOGGER.debug(
"triggering outpost update from outpostmodel/outpost", instance=instance "triggering outpost update from outpostmodel/outpost", instance=instance
) )
outpost_send_update(instance) outpost_send_update(instance)
return
if isinstance(instance, OutpostServiceConnection): if isinstance(instance, OutpostServiceConnection):
LOGGER.debug("triggering ServiceConnection state update", instance=instance) LOGGER.debug("triggering ServiceConnection state update", instance=instance)

View File

@ -7,7 +7,7 @@ from django.db import models
from django.forms import ModelForm from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.policies.models import Policy from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult

View File

@ -1,6 +1,5 @@
"""authentik policy engine""" """authentik policy engine"""
from enum import Enum from multiprocessing import Pipe, set_start_method
from multiprocessing import Pipe, current_process
from multiprocessing.connection import Connection from multiprocessing.connection import Connection
from typing import Iterator, List, Optional from typing import Iterator, List, Optional
@ -8,14 +7,17 @@ from django.core.cache import cache
from django.http import HttpRequest from django.http import HttpRequest
from sentry_sdk.hub import Hub from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger from structlog import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
from authentik.policies.process import PolicyProcess, cache_key from authentik.policies.process import PolicyProcess, cache_key
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
CURRENT_PROCESS = current_process() LOGGER = get_logger()
# This is only really needed for macOS, because Python 3.8 changed the default to spawn
# spawn causes issues with objects that aren't picklable, and also the django setup
set_start_method("fork")
class PolicyProcessInfo: class PolicyProcessInfo:
@ -35,24 +37,12 @@ class PolicyProcessInfo:
self.result = None self.result = None
class PolicyEngineMode(Enum):
"""Decide how results of multiple policies should be combined."""
MODE_AND = "and"
MODE_OR = "or"
class PolicyEngine: class PolicyEngine:
"""Orchestrate policy checking, launch tasks and return result""" """Orchestrate policy checking, launch tasks and return result"""
use_cache: bool use_cache: bool
request: PolicyRequest request: PolicyRequest
logger: BoundLogger
mode: PolicyEngineMode
# Allow objects with no policies attached to pass
empty_result: bool
__pbm: PolicyBindingModel __pbm: PolicyBindingModel
__cached_policies: List[PolicyResult] __cached_policies: List[PolicyResult]
__processes: List[PolicyProcessInfo] __processes: List[PolicyProcessInfo]
@ -62,16 +52,10 @@ class PolicyEngine:
def __init__( def __init__(
self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None 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.
self.empty_result = True
if not isinstance(pbm, PolicyBindingModel): # pragma: no cover if not isinstance(pbm, PolicyBindingModel): # pragma: no cover
raise ValueError(f"{pbm} is not instance of PolicyBindingModel") raise ValueError(f"{pbm} is not instance of PolicyBindingModel")
self.__pbm = pbm self.__pbm = pbm
self.request = PolicyRequest(user) self.request = PolicyRequest(user)
self.request.obj = pbm
if request: if request:
self.request.http_request = request self.request.http_request = request
self.__cached_policies = [] self.__cached_policies = []
@ -81,10 +65,8 @@ class PolicyEngine:
def _iter_bindings(self) -> Iterator[PolicyBinding]: def _iter_bindings(self) -> Iterator[PolicyBinding]:
"""Make sure all Policies are their respective classes""" """Make sure all Policies are their respective classes"""
return ( return PolicyBinding.objects.filter(target=self.__pbm, enabled=True).order_by(
PolicyBinding.objects.filter(target=self.__pbm, enabled=True) "order"
.order_by("order")
.iterator()
) )
def _check_policy_type(self, policy: Policy): def _check_policy_type(self, policy: Policy):
@ -106,29 +88,24 @@ class PolicyEngine:
key = cache_key(binding, self.request) key = cache_key(binding, self.request)
cached_policy = cache.get(key, None) cached_policy = cache.get(key, None)
if cached_policy and self.use_cache: if cached_policy and self.use_cache:
self.logger.debug( LOGGER.debug(
"P_ENG: Taking result from cache", "P_ENG: Taking result from cache",
policy=binding.policy, policy=binding.policy,
cache_key=key, cache_key=key,
) )
self.__cached_policies.append(cached_policy) self.__cached_policies.append(cached_policy)
continue continue
self.logger.debug("P_ENG: Evaluating policy", policy=binding.policy) LOGGER.debug("P_ENG: Evaluating policy", policy=binding.policy)
our_end, task_end = Pipe(False) our_end, task_end = Pipe(False)
task = PolicyProcess(binding, self.request, task_end) 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) task.start()
if not CURRENT_PROCESS._config.get("daemon"):
task.run()
else:
task.start()
self.__processes.append( self.__processes.append(
PolicyProcessInfo(process=task, connection=our_end, binding=binding) PolicyProcessInfo(process=task, connection=our_end, binding=binding)
) )
# If all policies are cached, we have an empty list here. # If all policies are cached, we have an empty list here.
for proc_info in self.__processes: for proc_info in self.__processes:
if proc_info.process.is_alive(): proc_info.process.join(proc_info.binding.timeout)
proc_info.process.join(proc_info.binding.timeout)
# Only call .recv() if no result is saved, otherwise we just deadlock here # Only call .recv() if no result is saved, otherwise we just deadlock here
if not proc_info.result: if not proc_info.result:
proc_info.result = proc_info.connection.recv() proc_info.result = proc_info.connection.recv()
@ -141,19 +118,24 @@ class PolicyEngine:
x.result for x in self.__processes if x.result x.result for x in self.__processes if x.result
] ]
all_results = list(process_results + self.__cached_policies) all_results = list(process_results + self.__cached_policies)
final_result = PolicyResult(False)
final_result.messages = []
final_result.source_results = all_results
if len(all_results) < self.__expected_result_count: # pragma: no cover if len(all_results) < self.__expected_result_count: # pragma: no cover
raise AssertionError("Got less results than polices") raise AssertionError("Got less results than polices")
# No results, no policies attached -> passing for result in all_results:
if len(all_results) == 0: LOGGER.debug(
return PolicyResult(self.empty_result) "P_ENG: result", passing=result.passing, messages=result.messages
passing = False )
if self.mode == PolicyEngineMode.MODE_AND: if result.messages:
passing = all([x.passing for x in all_results]) final_result.messages.extend(result.messages)
if self.mode == PolicyEngineMode.MODE_OR: if not result.passing:
passing = any([x.passing for x in all_results]) final_result.messages = tuple(final_result.messages)
result = PolicyResult(passing) final_result.passing = False
result.messages = tuple([y for x in all_results for y in x.messages]) return final_result
return result final_result.messages = tuple(final_result.messages)
final_result.passing = True
return final_result
@property @property
def passing(self) -> bool: def passing(self) -> bool:

View File

@ -1,25 +0,0 @@
"""Event Matcher Policy API"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.policies.event_matcher.models import EventMatcherPolicy
from authentik.policies.forms import GENERAL_SERIALIZER_FIELDS
class EventMatcherPolicySerializer(ModelSerializer):
"""Event Matcher Policy Serializer"""
class Meta:
model = EventMatcherPolicy
fields = GENERAL_SERIALIZER_FIELDS + [
"action",
"client_ip",
"app",
]
class EventMatcherPolicyViewSet(ModelViewSet):
"""Event Matcher Policy Viewset"""
queryset = EventMatcherPolicy.objects.all()
serializer_class = EventMatcherPolicySerializer

View File

@ -1,11 +0,0 @@
"""authentik Event Matcher policy app config"""
from django.apps import AppConfig
class AuthentikPoliciesEventMatcherConfig(AppConfig):
"""authentik Event Matcher policy app config"""
name = "authentik.policies.event_matcher"
label = "authentik_policies_event_matcher"
verbose_name = "authentik Policies.Event Matcher"

View File

@ -1,25 +0,0 @@
"""authentik Event Matcher Policy forms"""
from django import forms
from django.utils.translation import gettext_lazy as _
from authentik.policies.event_matcher.models import EventMatcherPolicy
from authentik.policies.forms import GENERAL_FIELDS
class EventMatcherPolicyForm(forms.ModelForm):
"""EventMatcherPolicy Form"""
class Meta:
model = EventMatcherPolicy
fields = GENERAL_FIELDS + [
"action",
"client_ip",
"app",
]
widgets = {
"name": forms.TextInput(),
"client_ip": forms.TextInput(),
}
labels = {"client_ip": _("Client IP")}

View File

@ -1,70 +0,0 @@
# Generated by Django 3.1.4 on 2020-12-24 10:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_policies", "0004_policy_execution_logging"),
]
operations = [
migrations.CreateModel(
name="EventMatcherPolicy",
fields=[
(
"policy_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_policies.policy",
),
),
(
"action",
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_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
(
"property_mapping_exception",
"Property Mapping Exception",
),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
],
),
),
("client_ip", models.TextField(blank=True)),
],
options={
"verbose_name": "Group Membership Policy",
"verbose_name_plural": "Group Membership Policies",
},
bases=("authentik_policies.policy",),
),
]

View File

@ -1,43 +0,0 @@
# Generated by Django 3.1.4 on 2020-12-30 20:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies_event_matcher", "0001_initial"),
]
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"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
],
),
),
]

Some files were not shown because too many files have changed in this diff Show More