Compare commits
102 Commits
version/20
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
| adc4cd9c0d | |||
| abed254ca1 | |||
| edfab0995f | |||
| 528dedf99d | |||
| 5d7eec3049 | |||
| ad44567ebe | |||
| ac82002339 | |||
| df92111296 | |||
| da8417a141 | |||
| 7f32355e3e | |||
| 5afe88a605 | |||
| 320dab3425 | |||
| ca44f8bd60 | |||
| 5fd408ca82 | |||
| becb9e34b5 | |||
| 4917ab9985 | |||
| bd92505bc2 | |||
| 30033d1f90 | |||
| 3e5dfcbd0f | |||
| bf0141acc6 | |||
| 0c8d513567 | |||
| d07704fdf1 | |||
| 086a8753c0 | |||
| ae7a6e2fd6 | |||
| 6a4ddcaba7 | |||
| 2c9b596f01 | |||
| 7257108091 | |||
| 91f7b289cc | |||
| 77a507d2f8 | |||
| 3e60e956f4 | |||
| 84ec70c2a2 | |||
| 72846f0ae1 | |||
| dd53e7e9b1 | |||
| 9df16a9ae0 | |||
| 02dd44eeec | |||
| 2f78e14381 | |||
| ef6f692526 | |||
| 2dd575874b | |||
| 84c2ebabaa | |||
| 3e26170f4b | |||
| 4709dca33c | |||
| 6064a481fb | |||
| 3979b0bde7 | |||
| 4280847bcc | |||
| ade8644da6 | |||
| 3c3fd53999 | |||
| 7b823f23ae | |||
| a67bea95d4 | |||
| 775e0ef2fa | |||
| d102c59654 | |||
| 03448a9169 | |||
| 1e6c081e5c | |||
| 8b9ce4a745 | |||
| 014d93d485 | |||
| 680b182d95 | |||
| b2a832175e | |||
| b3ce8331f5 | |||
| ef0f618234 | |||
| b8a7186a55 | |||
| b39530f873 | |||
| 7937c84f2b | |||
| 621843c60c | |||
| c19da839b1 | |||
| fea1f3be6f | |||
| 6f5ec7838f | |||
| 94300492e7 | |||
| 5d3931c128 | |||
| 262a8b5ae8 | |||
| fe069c5e55 | |||
| c6e60c0ebc | |||
| 90b457c5ee | |||
| 5e724e4299 | |||
| b4c8dd6b91 | |||
| 63d163cc65 | |||
| 2b1356bb91 | |||
| ba9edd6c44 | |||
| 3b2b3262d7 | |||
| 5431e7fe9d | |||
| 7d9c74ce04 | |||
| 60c3cf890a | |||
| 4ec5df6b12 | |||
| 0403f6d373 | |||
| b7f4d15a94 | |||
| 56450887ca | |||
| 9bd613a31d | |||
| 3fe0483dbf | |||
| 63a28ca1e9 | |||
| 2543b075be | |||
| b8bdf7a035 | |||
| a3ff7cea23 | |||
| bb776c2710 | |||
| c9ad87d419 | |||
| 0d81eaffff | |||
| 6930c84425 | |||
| eaaeaccf5d | |||
| efbbd0adcf | |||
| c8d9771640 | |||
| 2b98637ca5 | |||
| e3f7185564 | |||
| d1198fc6c1 | |||
| 8cb5f8fbee | |||
| fad5b09aee |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.6.2
|
||||
current_version = 2021.6.4
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
@ -21,6 +21,8 @@ values =
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
[bumpversion:file:schema.yml]
|
||||
|
||||
[bumpversion:file:.github/workflows/release.yml]
|
||||
|
||||
[bumpversion:file:authentik/__init__.py]
|
||||
|
||||
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@ -33,14 +33,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.6.2,
|
||||
beryju/authentik:2021.6.4,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.6.2,
|
||||
ghcr.io/goauthentik/server:2021.6.4,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik:latest
|
||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||
@ -75,14 +75,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-proxy:2021.6.2,
|
||||
beryju/authentik-proxy:2021.6.4,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.6.2,
|
||||
ghcr.io/goauthentik/proxy:2021.6.4,
|
||||
ghcr.io/goauthentik/proxy:latest
|
||||
file: outpost/proxy.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-proxy:latest
|
||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||
@ -117,14 +117,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-ldap:2021.6.2,
|
||||
beryju/authentik-ldap:2021.6.4,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.6.2,
|
||||
ghcr.io/goauthentik/ldap:2021.6.4,
|
||||
ghcr.io/goauthentik/ldap:latest
|
||||
file: outpost/ldap.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-ldap:latest
|
||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||
@ -157,7 +157,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2.1.5
|
||||
uses: actions/setup-node@v2.2.0
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Build web api client and web ui
|
||||
@ -176,6 +176,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.6.2
|
||||
version: authentik@2021.6.4
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
finalize: false
|
||||
|
||||
184
Pipfile.lock
generated
184
Pipfile.lock
generated
@ -76,11 +76,11 @@
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
|
||||
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
|
||||
"sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9",
|
||||
"sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.3.4"
|
||||
"version": "==3.4.1"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
@ -122,19 +122,19 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:2c2f70608934b03f9c08f4cd185de223b5abd18245dd4d4800e1fbc2a2523e31",
|
||||
"sha256:fccfa81cda69bb2317ed97e7149d7d84d19e6ec3bfbe3f721139e7ac0c407c73"
|
||||
"sha256:3b35689c215c982fe9f7ef78d748aa9b0cd15c3b2eb04f9b460aaa63fe2fbd03",
|
||||
"sha256:b1cbeb92123799001b97f2ee1cdf470e21f1be08314ae28fc7ea357925186f1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.17.98"
|
||||
"version": "==1.17.105"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:b2a49de4ee04b690142c8e7240f0f5758e3f7673dd39cf398efe893bf5e11c3f",
|
||||
"sha256:b955b23fe2fbdbbc8e66f37fe2970de6b5d8169f940b200bcf434751709d38f6"
|
||||
"sha256:b0fda4edf8eb105453890700d49011ada576d0cc7326a0699dfabe9e872f552c",
|
||||
"sha256:b5ba72d22212b0355f339c2a98b3296b3b2202a48e6a2b1366e866bc65a64b67"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.20.98"
|
||||
"version": "==1.20.105"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -165,11 +165,11 @@
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
"sha256:54436cd97b031bf2e08064223240e2a83d601d9414bcb1b702f94c6c33c29485",
|
||||
"sha256:b5399d76cf70d5cfac3ec993f8796ec1aa90d4cef55972295751f384758a80d7"
|
||||
"sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0",
|
||||
"sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.1.1"
|
||||
"version": "==5.1.2"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
@ -242,11 +242,11 @@
|
||||
},
|
||||
"channels-redis": {
|
||||
"hashes": [
|
||||
"sha256:18d63f6462a58011740dc8eeb57ea4b31ec220eb551cb71b27de9c6779a549de",
|
||||
"sha256:2fb31a63b05373f6402da2e6a91a22b9e66eb8b56626c6bfc93e156c734c5ae6"
|
||||
"sha256:0a18ce279c15ba79b7985bb12b2d6dd0ac8a14e4ad6952681f4422a4cc4a5ea9",
|
||||
"sha256:1abd5820ff1ed4ac627f8a219ad389e4c87e52e47a230929a7a474e95dd2c6c2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.0"
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@ -342,11 +342,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:66c9d8db8cc6fe938a28b7887c1596e42d522e27618562517cc8929eb7e7f296",
|
||||
"sha256:ea735cbbbb3b2fba6d4da4784a0043d84c67c92f1fdf15ad6db69900e792c10f"
|
||||
"sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd",
|
||||
"sha256:c58b5f19c5ae0afe6d75cbdd7df561e6eb929339985dbbda2565e1cabb19a62e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.4"
|
||||
"version": "==3.2.5"
|
||||
},
|
||||
"django-dbbackup": {
|
||||
"git": "https://github.com/django-dbbackup/django-dbbackup.git",
|
||||
@ -473,11 +473,11 @@
|
||||
},
|
||||
"google-auth": {
|
||||
"hashes": [
|
||||
"sha256:b3a67fa9ba5b768861dacf374c2135eb09fa14a0e40c851c3b8ea7abe6fc8fef",
|
||||
"sha256:e34e5f5de5610b202f9b40ebd9f8b27571d5c5537db9afed3a72b2db5a345039"
|
||||
"sha256:9266252e11393943410354cf14a77bcca24dd2ccd9c4e1aef23034fe0fbae630",
|
||||
"sha256:c7c215c74348ef24faef2f7b62f6d8e6b38824fe08b1e7b7b09a02d397eda7b3"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.32.0"
|
||||
"version": "==1.32.1"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@ -778,11 +778,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
||||
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.9"
|
||||
"version": "==21.0"
|
||||
},
|
||||
"prometheus-client": {
|
||||
"hashes": [
|
||||
@ -948,10 +948,30 @@
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
||||
"sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2",
|
||||
"sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7",
|
||||
"sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea",
|
||||
"sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426",
|
||||
"sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710",
|
||||
"sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1",
|
||||
"sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396",
|
||||
"sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2",
|
||||
"sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680",
|
||||
"sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35",
|
||||
"sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427",
|
||||
"sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b",
|
||||
"sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b",
|
||||
"sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f",
|
||||
"sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef",
|
||||
"sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c",
|
||||
"sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4",
|
||||
"sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d",
|
||||
"sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78",
|
||||
"sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b",
|
||||
"sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.17.3"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.18.0"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
@ -1167,11 +1187,11 @@
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
|
||||
"sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
|
||||
"sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
|
||||
"sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.26.5"
|
||||
"version": "==1.26.6"
|
||||
},
|
||||
"uvicorn": {
|
||||
"extras": [
|
||||
@ -1403,11 +1423,11 @@
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
|
||||
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
|
||||
"sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892",
|
||||
"sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9"
|
||||
],
|
||||
"markers": "python_version ~= '3.6'",
|
||||
"version": "==2.5.6"
|
||||
"version": "==2.6.2"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@ -1612,11 +1632,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
||||
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.9"
|
||||
"version": "==21.0"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
@ -1651,11 +1671,11 @@
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:0a049c5d47b629d9070c3932d13bff482b12119b6a241a93bc460b0be16953c8",
|
||||
"sha256:792b38ff30903884e4a9eab814ee3523731abd3c463f3ba48d7b627e87013484"
|
||||
"sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a",
|
||||
"sha256:5d46330e6b8886c31b5e3aba5ff48c10f4aa5e76cbf9002c6544306221e63fbc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8.3"
|
||||
"version": "==2.9.3"
|
||||
},
|
||||
"pylint-django": {
|
||||
"hashes": [
|
||||
@ -1733,49 +1753,45 @@
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5",
|
||||
"sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79",
|
||||
"sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31",
|
||||
"sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500",
|
||||
"sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11",
|
||||
"sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14",
|
||||
"sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3",
|
||||
"sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439",
|
||||
"sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c",
|
||||
"sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82",
|
||||
"sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711",
|
||||
"sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093",
|
||||
"sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a",
|
||||
"sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb",
|
||||
"sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8",
|
||||
"sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17",
|
||||
"sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000",
|
||||
"sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d",
|
||||
"sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480",
|
||||
"sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc",
|
||||
"sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0",
|
||||
"sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9",
|
||||
"sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765",
|
||||
"sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e",
|
||||
"sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a",
|
||||
"sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07",
|
||||
"sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f",
|
||||
"sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac",
|
||||
"sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7",
|
||||
"sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed",
|
||||
"sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968",
|
||||
"sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7",
|
||||
"sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2",
|
||||
"sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4",
|
||||
"sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87",
|
||||
"sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8",
|
||||
"sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10",
|
||||
"sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29",
|
||||
"sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605",
|
||||
"sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6",
|
||||
"sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"
|
||||
"sha256:0e46c1191b2eb293a6912269ed08b4512e7e241bbf591f97e527492e04c77e93",
|
||||
"sha256:18040755606b0c21281493ec309214bd61e41a170509e5014f41d6a5a586e161",
|
||||
"sha256:1806370b2bef4d4193eebe8ee59a9fd7547836a34917b7badbe6561a8594d9cb",
|
||||
"sha256:1ccbd41dbee3a31e18938096510b7d4ee53aa9fce2ee3dcc8ec82ae264f6acfd",
|
||||
"sha256:1d386402ae7f3c9b107ae5863f7ecccb0167762c82a687ae6526b040feaa5ac6",
|
||||
"sha256:210c359e6ee5b83f7d8c529ba3c75ba405481d50f35a420609b0db827e2e3bb5",
|
||||
"sha256:268fe9dd1deb4a30c8593cabd63f7a241dfdc5bd9dd0233906c718db22cdd49a",
|
||||
"sha256:361be4d311ac995a8c7ad577025a3ae3a538531b1f2cf32efd8b7e5d33a13e5a",
|
||||
"sha256:3f7a92e60930f8fca2623d9e326c173b7cf2c8b7e4fdcf984b75a1d2fb08114d",
|
||||
"sha256:444723ebaeb7fa8125f29c01a31101a3854ac3de293e317944022ae5effa53a4",
|
||||
"sha256:494d0172774dc0beeea984b94c95389143db029575f7ca908edd74469321ea99",
|
||||
"sha256:4b1999ef60c45357598935c12508abf56edbbb9c380df6f336de38a6c3a294ae",
|
||||
"sha256:4fc86b729ab88fe8ac3ec92287df253c64aa71560d76da5acd8a2e245839c629",
|
||||
"sha256:5049d00dbb78f9d166d1c704e93934d42cce0570842bb1a61695123d6b01de09",
|
||||
"sha256:56bef6b414949e2c9acf96cb5d78de8b529c7b99752619494e78dc76f99fd005",
|
||||
"sha256:59845101de68fd5d3a1145df9ea022e85ecd1b49300ea68307ad4302320f6f61",
|
||||
"sha256:6b8b629f93246e507287ee07e26744beaffb4c56ed520576deac8b615bd76012",
|
||||
"sha256:6c72ebb72e64e9bd195cb35a9b9bbfb955fd953b295255b8ae3e4ad4a146b615",
|
||||
"sha256:7743798dfb573d006f1143d745bf17efad39775a5190b347da5d83079646be56",
|
||||
"sha256:78a2a885345a2d60b5e68099e877757d5ed12e46ba1e87507175f14f80892af3",
|
||||
"sha256:849802379a660206277675aa5a5c327f5c910c690649535863ddf329b0ba8c87",
|
||||
"sha256:8cf6728f89b071bd3ab37cb8a0e306f4de897553a0ed07442015ee65fbf53d62",
|
||||
"sha256:a1b6a3f600d6aff97e3f28c34192c9ed93fee293bd96ef327b64adb51a74b2f6",
|
||||
"sha256:a548bb51c4476332ce4139df8e637386730f79a92652a907d12c696b6252b64d",
|
||||
"sha256:a8a5826d8a1b64e2ff9af488cc179e1a4d0f144d11ce486a9f34ea38ccedf4ef",
|
||||
"sha256:b024ee43ee6b310fad5acaee23e6485b21468718cb792a9d1693eecacc3f0b7e",
|
||||
"sha256:b092754c06852e8a8b022004aff56c24b06310189186805800d09313c37ce1f8",
|
||||
"sha256:b1dbeef938281f240347d50f28ae53c4b046a23389cd1fc4acec5ea0eae646a1",
|
||||
"sha256:bf819c5b77ff44accc9a24e31f1f7ceaaf6c960816913ed3ef8443b9d20d81b6",
|
||||
"sha256:c11f2fca544b5e30a0e813023196a63b1cb9869106ef9a26e9dae28bce3e4e26",
|
||||
"sha256:ce269e903b00d1ab4746793e9c50a57eec5d5388681abef074d7b9a65748fca5",
|
||||
"sha256:d0cf2651a8804f6325747c7e55e3be0f90ee2848e25d6b817aa2728d263f9abb",
|
||||
"sha256:e07e92935040c67f49571779d115ecb3e727016d42fb36ee0d8757db4ca12ee0",
|
||||
"sha256:e80d2851109e56420b71f9702ad1646e2f0364528adbf6af85527bc61e49f394",
|
||||
"sha256:ed77b97896312bc2deafe137ca2626e8b63808f5bedb944f73665c68093688a7",
|
||||
"sha256:f32f47fb22c988c0b35756024b61d156e5c4011cb8004aa53d93b03323c45657",
|
||||
"sha256:fdad3122b69cdabdb3da4c2a4107875913ac78dab0117fc73f988ad589c66b66"
|
||||
],
|
||||
"version": "==2021.4.4"
|
||||
"version": "==2021.7.1"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@ -1838,11 +1854,11 @@
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
|
||||
"sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
|
||||
"sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
|
||||
"sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.26.5"
|
||||
"version": "==1.26.6"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.6.2"
|
||||
__version__ = "2021.6.4"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
@ -19,7 +19,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
|
||||
auth_credentials = raw_header.decode()
|
||||
if auth_credentials == "" or " " not in auth_credentials:
|
||||
return None
|
||||
auth_type, auth_credentials = auth_credentials.split()
|
||||
auth_type, _, auth_credentials = auth_credentials.partition(" ")
|
||||
if auth_type.lower() not in ["basic", "bearer"]:
|
||||
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
|
||||
raise AuthenticationFailed("Unsupported authentication type")
|
||||
|
||||
@ -2,12 +2,11 @@
|
||||
from json import loads
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http.response import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.http import urlencode
|
||||
from django_filters.filters import BooleanFilter, CharFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from guardian.utils import get_anonymous_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||
@ -173,7 +172,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
@extend_schema(
|
||||
responses={
|
||||
"200": LinkSerializer(many=False),
|
||||
"404": OpenApiResponse(description="No recovery flow found."),
|
||||
"404": LinkSerializer(many=False),
|
||||
},
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||
@ -184,7 +183,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
# Check that there is a recovery flow, if not return an error
|
||||
flow = tenant.flow_recovery
|
||||
if not flow:
|
||||
raise Http404
|
||||
return Response({"link": ""}, status=404)
|
||||
user: User = self.get_object()
|
||||
token, __ = Token.objects.get_or_create(
|
||||
identifier=f"{user.uid}-password-reset",
|
||||
|
||||
@ -14,7 +14,9 @@ def is_dict(value: Any):
|
||||
"""Ensure a value is a dictionary, useful for JSONFields"""
|
||||
if isinstance(value, dict):
|
||||
return
|
||||
raise ValidationError("Value must be a dictionary.")
|
||||
raise ValidationError(
|
||||
"Value must be a dictionary, and not have any duplicate keys."
|
||||
)
|
||||
|
||||
|
||||
class PassiveSerializer(Serializer):
|
||||
|
||||
@ -5,14 +5,13 @@ from typing import Any, Optional, Type
|
||||
from urllib.parse import urlencode
|
||||
from uuid import uuid4
|
||||
|
||||
import django.db.models.options as options
|
||||
from deepmerge import always_merger
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
from django.core import validators
|
||||
from django.db import models
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.db.models import Q, QuerySet, options
|
||||
from django.http import HttpRequest
|
||||
from django.templatetags.static import static
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
@ -213,7 +213,7 @@ class SourceFlowManager:
|
||||
planner = FlowPlanner(flow)
|
||||
plan = planner.plan(self.request, kwargs)
|
||||
for stage in self.get_stages_to_append(flow):
|
||||
plan.append(stage)
|
||||
plan.append_stage(stage=stage)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
<script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script>
|
||||
<style>
|
||||
.pf-c-background-image::before {
|
||||
background-image: url("{{ flow.background_url }}");
|
||||
--ak-flow-background: url("{{ flow.background_url }}");
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
{% block head %}
|
||||
<style>
|
||||
.pf-c-background-image::before {
|
||||
background-image: url("/static/dist/assets/images/flow_background.jpg");
|
||||
--ak-flow-background: url("/static/dist/assets/images/flow_background.jpg");
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -97,7 +97,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"fingerprint",
|
||||
"fingerprint_sha256",
|
||||
"fingerprint_sha1",
|
||||
"certificate_data",
|
||||
"key_data",
|
||||
"cert_expiry",
|
||||
|
||||
@ -16,11 +16,6 @@ from authentik.crypto.models import CertificateKeyPair
|
||||
class CertificateBuilder:
|
||||
"""Build self-signed certificates"""
|
||||
|
||||
__public_key = None
|
||||
__private_key = None
|
||||
__builder = None
|
||||
__certificate = None
|
||||
|
||||
common_name: str
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@ -68,12 +68,19 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
||||
return self._private_key
|
||||
|
||||
@property
|
||||
def fingerprint(self) -> str:
|
||||
def fingerprint_sha256(self) -> str:
|
||||
"""Get SHA256 Fingerprint of certificate_data"""
|
||||
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
@property
|
||||
def fingerprint_sha1(self) -> str:
|
||||
"""Get SHA1 Fingerprint of certificate_data"""
|
||||
return hexlify(
|
||||
self.certificate.fingerprint(hashes.SHA1()), ":" # nosec
|
||||
).decode("utf-8")
|
||||
|
||||
@property
|
||||
def kid(self):
|
||||
"""Get Key ID used for JWKS"""
|
||||
|
||||
@ -6,11 +6,11 @@ from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, DictField, IntegerField
|
||||
from rest_framework.fields import DictField, IntegerField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
|
||||
from authentik.events.models import Event, EventAction
|
||||
@ -19,11 +19,6 @@ from authentik.events.models import Event, EventAction
|
||||
class EventSerializer(ModelSerializer):
|
||||
"""Event Serializer"""
|
||||
|
||||
# Since we only use this serializer for read-only operations,
|
||||
# no checking of the action is done here.
|
||||
# This allows clients to check wildcards, prefixes and custom types
|
||||
action = CharField()
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Event
|
||||
@ -96,7 +91,7 @@ class EventsFilter(django_filters.FilterSet):
|
||||
fields = ["action", "client_ip", "username"]
|
||||
|
||||
|
||||
class EventViewSet(ReadOnlyModelViewSet):
|
||||
class EventViewSet(ModelViewSet):
|
||||
"""Event Read-Only Viewset"""
|
||||
|
||||
queryset = Event.objects.all()
|
||||
|
||||
@ -46,7 +46,7 @@ class NotificationTransportTestSerializer(Serializer):
|
||||
|
||||
messages = ListField(child=CharField())
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
def create(self, validated_data: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
|
||||
@ -27,10 +27,9 @@ class GeoIPDict(TypedDict):
|
||||
class GeoIPReader:
|
||||
"""Slim wrapper around GeoIP API"""
|
||||
|
||||
__reader: Optional[Reader] = None
|
||||
__last_mtime: float = 0.0
|
||||
|
||||
def __init__(self):
|
||||
self.__reader: Optional[Reader] = None
|
||||
self.__last_mtime: float = 0.0
|
||||
self.__open()
|
||||
|
||||
def __open(self):
|
||||
|
||||
@ -3,6 +3,7 @@ from functools import partial
|
||||
from typing import Callable
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
@ -13,6 +14,7 @@ from authentik.core.models import User
|
||||
from authentik.events.models import Event, EventAction, Notification
|
||||
from authentik.events.signals import EventNewThread
|
||||
from authentik.events.utils import model_to_dict
|
||||
from authentik.lib.sentry import before_send
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
|
||||
|
||||
@ -62,12 +64,21 @@ class AuditMiddleware:
|
||||
|
||||
if settings.DEBUG:
|
||||
return
|
||||
thread = EventNewThread(
|
||||
EventAction.SYSTEM_EXCEPTION,
|
||||
request,
|
||||
message=exception_to_string(exception),
|
||||
)
|
||||
thread.run()
|
||||
# Special case for SuspiciousOperation, we have a special event action for that
|
||||
if isinstance(exception, SuspiciousOperation):
|
||||
thread = EventNewThread(
|
||||
EventAction.SUSPICIOUS_REQUEST,
|
||||
request,
|
||||
message=str(exception),
|
||||
)
|
||||
thread.run()
|
||||
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
||||
thread = EventNewThread(
|
||||
EventAction.SYSTEM_EXCEPTION,
|
||||
request,
|
||||
message=exception_to_string(exception),
|
||||
)
|
||||
thread.run()
|
||||
|
||||
@staticmethod
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
@ -105,7 +105,11 @@ def notification_transport(
|
||||
"""Send notification over specified transport"""
|
||||
self.save_on_success = False
|
||||
try:
|
||||
notification: Notification = Notification.objects.get(pk=notification_pk)
|
||||
notification: Notification = Notification.objects.filter(
|
||||
pk=notification_pk
|
||||
).first()
|
||||
if not notification:
|
||||
return
|
||||
transport: NotificationTransport = NotificationTransport.objects.get(
|
||||
pk=transport_pk
|
||||
)
|
||||
|
||||
@ -25,6 +25,7 @@ class FlowStageBindingSerializer(ModelSerializer):
|
||||
"re_evaluate_policies",
|
||||
"order",
|
||||
"policy_engine_mode",
|
||||
"invalid_response_action",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -5,8 +5,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from django.http.request import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.flows.models import FlowStageBinding
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.models import PolicyBinding
|
||||
|
||||
@ -22,11 +21,14 @@ class StageMarker:
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def process(
|
||||
self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest]
|
||||
) -> Optional[Stage]:
|
||||
self,
|
||||
plan: "FlowPlan",
|
||||
binding: FlowStageBinding,
|
||||
http_request: HttpRequest,
|
||||
) -> Optional[FlowStageBinding]:
|
||||
"""Process callback for this marker. This should be overridden by sub-classes.
|
||||
If a stage should be removed, return None."""
|
||||
return stage
|
||||
return binding
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -34,24 +36,34 @@ class ReevaluateMarker(StageMarker):
|
||||
"""Reevaluate Marker, forces stage's policies to be evaluated again."""
|
||||
|
||||
binding: PolicyBinding
|
||||
user: User
|
||||
|
||||
def process(
|
||||
self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest]
|
||||
) -> Optional[Stage]:
|
||||
self,
|
||||
plan: "FlowPlan",
|
||||
binding: FlowStageBinding,
|
||||
http_request: HttpRequest,
|
||||
) -> Optional[FlowStageBinding]:
|
||||
"""Re-evaluate policies bound to stage, and if they fail, remove from plan"""
|
||||
engine = PolicyEngine(self.binding, self.user)
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
|
||||
LOGGER.debug(
|
||||
"f(plan_inst)[re-eval marker]: running re-evaluation",
|
||||
binding=binding,
|
||||
policy_binding=self.binding,
|
||||
)
|
||||
engine = PolicyEngine(
|
||||
self.binding, plan.context.get(PLAN_CONTEXT_PENDING_USER, http_request.user)
|
||||
)
|
||||
engine.use_cache = False
|
||||
if http_request:
|
||||
engine.request.set_http_request(http_request)
|
||||
engine.request.set_http_request(http_request)
|
||||
engine.request.context = plan.context
|
||||
engine.build()
|
||||
result = engine.result
|
||||
if result.passing:
|
||||
return stage
|
||||
return binding
|
||||
LOGGER.warning(
|
||||
"f(plan_inst)[re-eval marker]: stage failed re-evaluation",
|
||||
stage=stage,
|
||||
"f(plan_inst)[re-eval marker]: binding failed re-evaluation",
|
||||
binding=binding,
|
||||
messages=result.messages,
|
||||
)
|
||||
return None
|
||||
|
||||
@ -135,7 +135,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0017_auto_20210329_1334"),
|
||||
("authentik_stages_user_write", "__latest__"),
|
||||
("authentik_stages_user_write", "0002_auto_20200918_1653"),
|
||||
("authentik_stages_user_login", "__latest__"),
|
||||
("authentik_stages_password", "0002_passwordstage_change_flow"),
|
||||
("authentik_policies", "0001_initial"),
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-27 16:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0020_flow_compatibility_mode"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="flowstagebinding",
|
||||
name="invalid_response_action",
|
||||
field=models.TextField(
|
||||
choices=[("retry", "Retry"), ("continue", "Continue")],
|
||||
default="retry",
|
||||
help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor while CONTINUE continues with the next stage.",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-03 13:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0021_flowstagebinding_invalid_response_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="flowstagebinding",
|
||||
name="invalid_response_action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("retry", "Retry"),
|
||||
("restart", "Restart"),
|
||||
("restart_with_context", "Restart With Context"),
|
||||
],
|
||||
default="retry",
|
||||
help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context.",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -27,6 +27,14 @@ class NotConfiguredAction(models.TextChoices):
|
||||
CONFIGURE = "configure"
|
||||
|
||||
|
||||
class InvalidResponseAction(models.TextChoices):
|
||||
"""Configure how the flow executor should handle invalid responses to challenges"""
|
||||
|
||||
RETRY = "retry"
|
||||
RESTART = "restart"
|
||||
RESTART_WITH_CONTEXT = "restart_with_context"
|
||||
|
||||
|
||||
class FlowDesignation(models.TextChoices):
|
||||
"""Designation of what a Flow should be used for. At a later point, this
|
||||
should be replaced by a database entry."""
|
||||
@ -201,6 +209,17 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
|
||||
help_text=_("Evaluate policies when the Stage is present to the user."),
|
||||
)
|
||||
|
||||
invalid_response_action = models.TextField(
|
||||
choices=InvalidResponseAction.choices,
|
||||
default=InvalidResponseAction.RETRY,
|
||||
help_text=_(
|
||||
"Configure how the flow executor should handle an invalid response to a "
|
||||
"challenge. RETRY returns the error message and a similar challenge to the "
|
||||
"executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT "
|
||||
"restarts the flow while keeping the current context."
|
||||
),
|
||||
)
|
||||
|
||||
order = models.IntegerField()
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@ -52,33 +52,41 @@ class FlowPlan:
|
||||
|
||||
flow_pk: str
|
||||
|
||||
stages: list[Stage] = field(default_factory=list)
|
||||
bindings: list[FlowStageBinding] = field(default_factory=list)
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
markers: list[StageMarker] = field(default_factory=list)
|
||||
|
||||
def append(self, stage: Stage, marker: Optional[StageMarker] = None):
|
||||
def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
|
||||
"""Append `stage` to all stages, optionall with stage marker"""
|
||||
self.stages.append(stage)
|
||||
return self.append(FlowStageBinding(stage=stage), marker)
|
||||
|
||||
def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
|
||||
"""Append `stage` to all stages, optionall with stage marker"""
|
||||
self.bindings.append(binding)
|
||||
self.markers.append(marker or StageMarker())
|
||||
|
||||
def insert(self, stage: Stage, marker: Optional[StageMarker] = None):
|
||||
def insert_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
|
||||
"""Insert stage into plan, as immediate next stage"""
|
||||
self.stages.insert(1, stage)
|
||||
self.bindings.insert(1, FlowStageBinding(stage=stage, order=0))
|
||||
self.markers.insert(1, marker or StageMarker())
|
||||
|
||||
def next(self, http_request: Optional[HttpRequest]) -> Optional[Stage]:
|
||||
def next(self, http_request: Optional[HttpRequest]) -> Optional[FlowStageBinding]:
|
||||
"""Return next pending stage from the bottom of the list"""
|
||||
if not self.has_stages:
|
||||
return None
|
||||
stage = self.stages[0]
|
||||
binding = self.bindings[0]
|
||||
marker = self.markers[0]
|
||||
|
||||
if marker.__class__ is not StageMarker:
|
||||
LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker)
|
||||
marked_stage = marker.process(self, stage, http_request)
|
||||
LOGGER.debug(
|
||||
"f(plan_inst): stage has marker", binding=binding, marker=marker
|
||||
)
|
||||
marked_stage = marker.process(self, binding, http_request)
|
||||
if not marked_stage:
|
||||
LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage)
|
||||
self.stages.remove(stage)
|
||||
LOGGER.debug(
|
||||
"f(plan_inst): marker returned none, next stage", binding=binding
|
||||
)
|
||||
self.bindings.remove(binding)
|
||||
self.markers.remove(marker)
|
||||
if not self.has_stages:
|
||||
return None
|
||||
@ -89,12 +97,12 @@ class FlowPlan:
|
||||
def pop(self):
|
||||
"""Pop next pending stage from bottom of list"""
|
||||
self.markers.pop(0)
|
||||
self.stages.pop(0)
|
||||
self.bindings.pop(0)
|
||||
|
||||
@property
|
||||
def has_stages(self) -> bool:
|
||||
"""Check if there are any stages left in this plan"""
|
||||
return len(self.markers) + len(self.stages) > 0
|
||||
return len(self.markers) + len(self.bindings) > 0
|
||||
|
||||
|
||||
class FlowPlanner:
|
||||
@ -161,7 +169,7 @@ class FlowPlanner:
|
||||
plan = self._build_plan(user, request, default_context)
|
||||
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
|
||||
GAUGE_FLOWS_CACHED.update()
|
||||
if not plan.stages and not self.allow_empty_flows:
|
||||
if not plan.bindings and not self.allow_empty_flows:
|
||||
raise EmptyFlowException()
|
||||
return plan
|
||||
|
||||
@ -216,9 +224,9 @@ class FlowPlanner:
|
||||
"f(plan): stage has re-evaluate marker",
|
||||
stage=binding.stage,
|
||||
)
|
||||
marker = ReevaluateMarker(binding=binding, user=user)
|
||||
marker = ReevaluateMarker(binding=binding)
|
||||
if stage:
|
||||
plan.append(stage, marker)
|
||||
plan.append(binding, marker)
|
||||
HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug)
|
||||
self._logger.debug(
|
||||
"f(plan): finished building",
|
||||
|
||||
@ -16,6 +16,7 @@ from authentik.flows.challenge import (
|
||||
HttpChallengeResponse,
|
||||
WithUserInfoChallenge,
|
||||
)
|
||||
from authentik.flows.models import InvalidResponseAction
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.views import FlowExecutorView
|
||||
|
||||
@ -69,7 +70,13 @@ class ChallengeStageView(StageView):
|
||||
"""Return a challenge for the frontend to solve"""
|
||||
challenge = self._get_challenge(*args, **kwargs)
|
||||
if not challenge.is_valid():
|
||||
LOGGER.warning(challenge.errors, stage_view=self, challenge=challenge)
|
||||
LOGGER.warning(
|
||||
"f(ch): Invalid challenge",
|
||||
binding=self.executor.current_binding,
|
||||
errors=challenge.errors,
|
||||
stage_view=self,
|
||||
challenge=challenge,
|
||||
)
|
||||
return HttpChallengeResponse(challenge)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@ -77,6 +84,21 @@ class ChallengeStageView(StageView):
|
||||
"""Handle challenge response"""
|
||||
challenge: ChallengeResponse = self.get_response_instance(data=request.data)
|
||||
if not challenge.is_valid():
|
||||
if self.executor.current_binding.invalid_response_action in [
|
||||
InvalidResponseAction.RESTART,
|
||||
InvalidResponseAction.RESTART_WITH_CONTEXT,
|
||||
]:
|
||||
keep_context = (
|
||||
self.executor.current_binding.invalid_response_action
|
||||
== InvalidResponseAction.RESTART_WITH_CONTEXT
|
||||
)
|
||||
LOGGER.debug(
|
||||
"f(ch): Invalid response, restarting flow",
|
||||
binding=self.executor.current_binding,
|
||||
stage_view=self,
|
||||
keep_context=keep_context,
|
||||
)
|
||||
return self.executor.restart_flow(keep_context)
|
||||
return self.challenge_invalid(challenge)
|
||||
return self.challenge_valid(challenge)
|
||||
|
||||
@ -126,5 +148,10 @@ class ChallengeStageView(StageView):
|
||||
)
|
||||
challenge_response.initial_data["response_errors"] = full_errors
|
||||
if not challenge_response.is_valid():
|
||||
LOGGER.warning(challenge_response.errors)
|
||||
LOGGER.warning(
|
||||
"f(ch): invalid challenge response",
|
||||
binding=self.executor.current_binding,
|
||||
errors=challenge_response.errors,
|
||||
stage_view=self,
|
||||
)
|
||||
return HttpChallengeResponse(challenge_response)
|
||||
|
||||
@ -182,8 +182,8 @@ class TestFlowPlanner(TestCase):
|
||||
planner = FlowPlanner(flow)
|
||||
plan = planner.plan(request)
|
||||
|
||||
self.assertEqual(plan.stages[0], binding.stage)
|
||||
self.assertEqual(plan.stages[1], binding2.stage)
|
||||
self.assertEqual(plan.bindings[0], binding)
|
||||
self.assertEqual(plan.bindings[1], binding2)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||
|
||||
@ -11,15 +11,23 @@ from authentik.core.models import User
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.models import (
|
||||
Flow,
|
||||
FlowDesignation,
|
||||
FlowStageBinding,
|
||||
InvalidResponseAction,
|
||||
)
|
||||
from authentik.flows.planner import FlowPlan, FlowPlanner
|
||||
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
|
||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.policies.reputation.models import ReputationPolicy
|
||||
from authentik.policies.types import PolicyResult
|
||||
from authentik.stages.deny.models import DenyStage
|
||||
from authentik.stages.dummy.models import DummyStage
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
|
||||
POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False))
|
||||
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
||||
@ -52,8 +60,9 @@ class TestFlowExecutor(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
stage = DummyStage.objects.create(name="dummy")
|
||||
binding = FlowStageBinding(target=flow, stage=stage, order=0)
|
||||
plan = FlowPlan(
|
||||
flow_pk=flow.pk.hex + "a", stages=[stage], markers=[StageMarker()]
|
||||
flow_pk=flow.pk.hex + "a", bindings=[binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -163,7 +172,7 @@ class TestFlowExecutor(TestCase):
|
||||
# Check that two stages are in plan
|
||||
session = self.client.session
|
||||
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||
self.assertEqual(len(plan.stages), 2)
|
||||
self.assertEqual(len(plan.bindings), 2)
|
||||
# Second request, submit form, one stage left
|
||||
response = self.client.post(exec_url)
|
||||
# Second request redirects to the same URL
|
||||
@ -172,7 +181,7 @@ class TestFlowExecutor(TestCase):
|
||||
# Check that two stages are in plan
|
||||
session = self.client.session
|
||||
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||
self.assertEqual(len(plan.stages), 1)
|
||||
self.assertEqual(len(plan.bindings), 1)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
@ -213,8 +222,8 @@ class TestFlowExecutor(TestCase):
|
||||
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding.stage)
|
||||
self.assertEqual(plan.stages[1], binding2.stage)
|
||||
self.assertEqual(plan.bindings[0], binding)
|
||||
self.assertEqual(plan.bindings[1], binding2)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||
@ -267,9 +276,9 @@ class TestFlowExecutor(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding.stage)
|
||||
self.assertEqual(plan.stages[1], binding2.stage)
|
||||
self.assertEqual(plan.stages[2], binding3.stage)
|
||||
self.assertEqual(plan.bindings[0], binding)
|
||||
self.assertEqual(plan.bindings[1], binding2)
|
||||
self.assertEqual(plan.bindings[2], binding3)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||
@ -281,8 +290,8 @@ class TestFlowExecutor(TestCase):
|
||||
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding2.stage)
|
||||
self.assertEqual(plan.stages[1], binding3.stage)
|
||||
self.assertEqual(plan.bindings[0], binding2)
|
||||
self.assertEqual(plan.bindings[1], binding3)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], StageMarker)
|
||||
@ -338,9 +347,9 @@ class TestFlowExecutor(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding.stage)
|
||||
self.assertEqual(plan.stages[1], binding2.stage)
|
||||
self.assertEqual(plan.stages[2], binding3.stage)
|
||||
self.assertEqual(plan.bindings[0], binding)
|
||||
self.assertEqual(plan.bindings[1], binding2)
|
||||
self.assertEqual(plan.bindings[2], binding3)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||
@ -352,8 +361,8 @@ class TestFlowExecutor(TestCase):
|
||||
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding2.stage)
|
||||
self.assertEqual(plan.stages[1], binding3.stage)
|
||||
self.assertEqual(plan.bindings[0], binding2)
|
||||
self.assertEqual(plan.bindings[1], binding3)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], StageMarker)
|
||||
@ -364,7 +373,7 @@ class TestFlowExecutor(TestCase):
|
||||
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding3.stage)
|
||||
self.assertEqual(plan.bindings[0], binding3)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
|
||||
@ -438,10 +447,10 @@ class TestFlowExecutor(TestCase):
|
||||
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding.stage)
|
||||
self.assertEqual(plan.stages[1], binding2.stage)
|
||||
self.assertEqual(plan.stages[2], binding3.stage)
|
||||
self.assertEqual(plan.stages[3], binding4.stage)
|
||||
self.assertEqual(plan.bindings[0], binding)
|
||||
self.assertEqual(plan.bindings[1], binding2)
|
||||
self.assertEqual(plan.bindings[2], binding3)
|
||||
self.assertEqual(plan.bindings[3], binding4)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||
@ -512,3 +521,78 @@ class TestFlowExecutor(TestCase):
|
||||
|
||||
stage_view = StageView(executor)
|
||||
self.assertEqual(ident, stage_view.get_pending_user(for_display=True).username)
|
||||
|
||||
def test_invalid_restart(self):
|
||||
"""Test flow that restarts on invalid entry"""
|
||||
flow = Flow.objects.create(
|
||||
name="restart-on-invalid",
|
||||
slug="restart-on-invalid",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
# Stage 0 is a deny stage that is added dynamically
|
||||
# when the reputation policy says so
|
||||
deny_stage = DenyStage.objects.create(name="deny")
|
||||
reputation_policy = ReputationPolicy.objects.create(
|
||||
name="reputation", threshold=-1, check_ip=False
|
||||
)
|
||||
deny_binding = FlowStageBinding.objects.create(
|
||||
target=flow,
|
||||
stage=deny_stage,
|
||||
order=0,
|
||||
evaluate_on_plan=False,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
PolicyBinding.objects.create(
|
||||
policy=reputation_policy, target=deny_binding, order=0
|
||||
)
|
||||
|
||||
# Stage 1 is an identification stage
|
||||
ident_stage = IdentificationStage.objects.create(
|
||||
name="ident",
|
||||
user_fields=[UserFields.E_MAIL],
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
target=flow,
|
||||
stage=ident_stage,
|
||||
order=1,
|
||||
invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
|
||||
)
|
||||
exec_url = reverse(
|
||||
"authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||
)
|
||||
# First request, run the planner
|
||||
response = self.client.get(exec_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
{
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
"component": "ak-stage-identification",
|
||||
"flow_info": {
|
||||
"background": flow.background_url,
|
||||
"cancel_url": reverse("authentik_flows:cancel"),
|
||||
"title": "",
|
||||
},
|
||||
"password_fields": False,
|
||||
"primary_action": "Log in",
|
||||
"sources": [],
|
||||
"user_fields": [UserFields.E_MAIL],
|
||||
},
|
||||
)
|
||||
response = self.client.post(
|
||||
exec_url, {"uid_field": "invalid-string"}, follow=True
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
{
|
||||
"component": "ak-stage-access-denied",
|
||||
"error_message": None,
|
||||
"flow_info": {
|
||||
"background": flow.background_url,
|
||||
"cancel_url": reverse("authentik_flows:cancel"),
|
||||
"title": "",
|
||||
},
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
},
|
||||
)
|
||||
|
||||
@ -40,15 +40,11 @@ def transaction_rollback():
|
||||
class FlowImporter:
|
||||
"""Import Flow from json"""
|
||||
|
||||
__import: FlowBundle
|
||||
|
||||
__pk_map: dict[Any, Model]
|
||||
|
||||
logger: BoundLogger
|
||||
|
||||
def __init__(self, json_input: str):
|
||||
self.__pk_map: dict[Any, Model] = {}
|
||||
self.logger = get_logger()
|
||||
self.__pk_map = {}
|
||||
import_dict = loads(json_input)
|
||||
try:
|
||||
self.__import = from_dict(FlowBundle, import_dict)
|
||||
|
||||
@ -4,6 +4,7 @@ from typing import Any, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.cache import cache
|
||||
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.http.request import QueryDict
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
@ -37,7 +38,13 @@ from authentik.flows.challenge import (
|
||||
WithUserInfoChallenge,
|
||||
)
|
||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
|
||||
from authentik.flows.models import (
|
||||
ConfigurableStage,
|
||||
Flow,
|
||||
FlowDesignation,
|
||||
FlowStageBinding,
|
||||
Stage,
|
||||
)
|
||||
from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_PENDING_USER,
|
||||
PLAN_CONTEXT_REDIRECT,
|
||||
@ -107,6 +114,7 @@ class FlowExecutorView(APIView):
|
||||
flow: Flow
|
||||
|
||||
plan: Optional[FlowPlan] = None
|
||||
current_binding: FlowStageBinding
|
||||
current_stage: Stage
|
||||
current_stage_view: View
|
||||
|
||||
@ -126,7 +134,7 @@ class FlowExecutorView(APIView):
|
||||
message = exc.__doc__ if exc.__doc__ else str(exc)
|
||||
return self.stage_invalid(error_message=message)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=unused-argument, too-many-return-statements
|
||||
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
||||
# Early check if theres an active Plan for the current session
|
||||
if SESSION_KEY_PLAN in self.request.session:
|
||||
@ -159,11 +167,23 @@ class FlowExecutorView(APIView):
|
||||
request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", ""))
|
||||
# We don't save the Plan after getting the next stage
|
||||
# as it hasn't been successfully passed yet
|
||||
next_stage = self.plan.next(self.request)
|
||||
if not next_stage:
|
||||
try:
|
||||
# This is the first time we actually access any attribute on the selected plan
|
||||
# if the cached plan is from an older version, it might have different attributes
|
||||
# in which case we just delete the plan and invalidate everything
|
||||
next_binding = self.plan.next(self.request)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
self._logger.warning(
|
||||
"f(exec): found incompatible flow plan, invalidating run", exc=exc
|
||||
)
|
||||
keys = cache.keys("flow_*")
|
||||
cache.delete_many(keys)
|
||||
return self.stage_invalid()
|
||||
if not next_binding:
|
||||
self._logger.debug("f(exec): no more stages, flow is done.")
|
||||
return self._flow_done()
|
||||
self.current_stage = next_stage
|
||||
self.current_binding = next_binding
|
||||
self.current_stage = next_binding.stage
|
||||
self._logger.debug(
|
||||
"f(exec): Current stage",
|
||||
current_stage=self.current_stage,
|
||||
@ -268,8 +288,31 @@ class FlowExecutorView(APIView):
|
||||
planner = FlowPlanner(self.flow)
|
||||
plan = planner.plan(self.request)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
try:
|
||||
# Call the has_stages getter to check that
|
||||
# there are no issues with the class we might've gotten
|
||||
# from the cache. If there are errors, just delete all cached flows
|
||||
_ = plan.has_stages
|
||||
except Exception: # pylint: disable=broad-except
|
||||
keys = cache.keys("flow_*")
|
||||
cache.delete_many(keys)
|
||||
return self._initiate_plan()
|
||||
return plan
|
||||
|
||||
def restart_flow(self, keep_context=False) -> HttpResponse:
|
||||
"""Restart the currently active flow, optionally keeping the current context"""
|
||||
planner = FlowPlanner(self.flow)
|
||||
default_context = None
|
||||
if keep_context:
|
||||
default_context = self.plan.context
|
||||
plan = planner.plan(self.request, default_context)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
kwargs = self.kwargs
|
||||
kwargs.update({"flow_slug": self.flow.slug})
|
||||
return redirect_with_qs(
|
||||
"authentik_api:flow-executor", self.request.GET, **kwargs
|
||||
)
|
||||
|
||||
def _flow_done(self) -> HttpResponse:
|
||||
"""User Successfully passed all stages"""
|
||||
# Since this is wrapped by the ExecutorShell, the next argument is saved in the session
|
||||
@ -293,10 +336,10 @@ class FlowExecutorView(APIView):
|
||||
)
|
||||
self.plan.pop()
|
||||
self.request.session[SESSION_KEY_PLAN] = self.plan
|
||||
if self.plan.stages:
|
||||
if self.plan.bindings:
|
||||
self._logger.debug(
|
||||
"f(exec): Continuing with next stage",
|
||||
remaining=len(self.plan.stages),
|
||||
remaining=len(self.plan.bindings),
|
||||
)
|
||||
kwargs = self.kwargs
|
||||
kwargs.update({"flow_slug": self.flow.slug})
|
||||
|
||||
@ -26,10 +26,9 @@ class ConfigLoader:
|
||||
|
||||
loaded_file = []
|
||||
|
||||
__config = {}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.__config = {}
|
||||
base_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
for path in SEARCH_PATHS:
|
||||
# Check if path is relative, and if so join with base_dir
|
||||
|
||||
@ -3,6 +3,7 @@ import re
|
||||
from textwrap import indent
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
from django.core.exceptions import FieldError
|
||||
from requests import Session
|
||||
from rest_framework.serializers import ValidationError
|
||||
from sentry_sdk.hub import Hub
|
||||
@ -29,10 +30,10 @@ class BaseEvaluator:
|
||||
# update website/docs/expressions/_objects.md
|
||||
# update website/docs/expressions/_functions.md
|
||||
self._globals = {
|
||||
"regex_match": BaseEvaluator.expr_filter_regex_match,
|
||||
"regex_replace": BaseEvaluator.expr_filter_regex_replace,
|
||||
"ak_is_group_member": BaseEvaluator.expr_func_is_group_member,
|
||||
"ak_user_by": BaseEvaluator.expr_func_user_by,
|
||||
"regex_match": BaseEvaluator.expr_regex_match,
|
||||
"regex_replace": BaseEvaluator.expr_regex_replace,
|
||||
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
||||
"ak_user_by": BaseEvaluator.expr_user_by,
|
||||
"ak_logger": get_logger(),
|
||||
"requests": Session(),
|
||||
}
|
||||
@ -40,25 +41,28 @@ class BaseEvaluator:
|
||||
self._filename = "BaseEvalautor"
|
||||
|
||||
@staticmethod
|
||||
def expr_filter_regex_match(value: Any, regex: str) -> bool:
|
||||
def expr_regex_match(value: Any, regex: str) -> bool:
|
||||
"""Expression Filter to run re.search"""
|
||||
return re.search(regex, value) is None
|
||||
return re.search(regex, value) is not None
|
||||
|
||||
@staticmethod
|
||||
def expr_filter_regex_replace(value: Any, regex: str, repl: str) -> str:
|
||||
def expr_regex_replace(value: Any, regex: str, repl: str) -> str:
|
||||
"""Expression Filter to run re.sub"""
|
||||
return re.sub(regex, repl, value)
|
||||
|
||||
@staticmethod
|
||||
def expr_func_user_by(**filters) -> Optional[User]:
|
||||
def expr_user_by(**filters) -> Optional[User]:
|
||||
"""Get user by filters"""
|
||||
users = User.objects.filter(**filters)
|
||||
if users:
|
||||
return users.first()
|
||||
return None
|
||||
try:
|
||||
users = User.objects.filter(**filters)
|
||||
if users:
|
||||
return users.first()
|
||||
return None
|
||||
except FieldError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def expr_func_is_group_member(user: User, **group_filters) -> bool:
|
||||
def expr_is_group_member(user: User, **group_filters) -> bool:
|
||||
"""Check if `user` is member of group with name `group_name`"""
|
||||
return user.ak_groups.filter(**group_filters).exists()
|
||||
|
||||
|
||||
32
authentik/lib/tests/test_evaluator.py
Normal file
32
authentik/lib/tests/test_evaluator.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Test Evaluator base functions"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||
|
||||
|
||||
class TestEvaluator(TestCase):
|
||||
"""Test Evaluator base functions"""
|
||||
|
||||
def test_regex_match(self):
|
||||
"""Test expr_regex_match"""
|
||||
self.assertFalse(BaseEvaluator.expr_regex_match("foo", "bar"))
|
||||
self.assertTrue(BaseEvaluator.expr_regex_match("foo", "foo"))
|
||||
|
||||
def test_regex_replace(self):
|
||||
"""Test expr_regex_replace"""
|
||||
self.assertEqual(BaseEvaluator.expr_regex_replace("foo", "o", "a"), "faa")
|
||||
|
||||
def test_user_by(self):
|
||||
"""Test expr_user_by"""
|
||||
self.assertIsNotNone(BaseEvaluator.expr_user_by(username="akadmin"))
|
||||
self.assertIsNone(BaseEvaluator.expr_user_by(username="bar"))
|
||||
self.assertIsNone(BaseEvaluator.expr_user_by(foo="bar"))
|
||||
|
||||
def test_is_group_member(self):
|
||||
"""Test expr_is_group_member"""
|
||||
self.assertFalse(
|
||||
BaseEvaluator.expr_is_group_member(
|
||||
User.objects.get(username="akadmin"), name="test"
|
||||
)
|
||||
)
|
||||
@ -51,7 +51,7 @@ class OutpostSerializer(ModelSerializer):
|
||||
raise ValidationError(
|
||||
(
|
||||
f"Outpost type {self.initial_data['type']} can't be used with "
|
||||
f"{type(provider)} providers."
|
||||
f"{provider.__class__.__name__} providers."
|
||||
)
|
||||
)
|
||||
return providers
|
||||
|
||||
@ -69,7 +69,7 @@ class OutpostConsumer(AuthJsonConsumer):
|
||||
self.last_uid = self.channel_name
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def disconnect(self, close_code):
|
||||
def disconnect(self, code):
|
||||
if self.outpost and self.last_uid:
|
||||
state = OutpostState.for_instance_uid(self.outpost, self.last_uid)
|
||||
if self.channel_name in state.channel_ids:
|
||||
|
||||
@ -36,8 +36,10 @@ class DockerController(BaseController):
|
||||
|
||||
def _get_env(self) -> dict[str, str]:
|
||||
return {
|
||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
|
||||
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
|
||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
|
||||
"AUTHENTIK_INSECURE": str(
|
||||
self.outpost.config.authentik_host_insecure
|
||||
).lower(),
|
||||
"AUTHENTIK_TOKEN": self.outpost.token.key,
|
||||
}
|
||||
|
||||
@ -45,11 +47,34 @@ class DockerController(BaseController):
|
||||
"""Check if container's env is equal to what we would set. Return true if container needs
|
||||
to be rebuilt."""
|
||||
should_be = self._get_env()
|
||||
container_env = container.attrs.get("Config", {}).get("Env", {})
|
||||
container_env = container.attrs.get("Config", {}).get("Env", [])
|
||||
for key, expected_value in should_be.items():
|
||||
if key not in container_env:
|
||||
continue
|
||||
if container_env[key] != expected_value:
|
||||
entry = f"{key.upper()}={expected_value}"
|
||||
if entry not in container_env:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _comp_ports(self, container: Container) -> bool:
|
||||
"""Check that the container has the correct ports exposed. Return true if container needs
|
||||
to be rebuilt."""
|
||||
# with TEST enabled, we use host-network
|
||||
if settings.TEST:
|
||||
return False
|
||||
# When the container isn't running, the API doesn't report any port mappings
|
||||
if container.status != "running":
|
||||
return False
|
||||
# {'3389/tcp': [
|
||||
# {'HostIp': '0.0.0.0', 'HostPort': '389'},
|
||||
# {'HostIp': '::', 'HostPort': '389'}
|
||||
# ]}
|
||||
for port in self.deployment_ports:
|
||||
key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
|
||||
if key not in container.ports:
|
||||
return True
|
||||
host_matching = False
|
||||
for host_port in container.ports[key]:
|
||||
host_matching = host_port.get("HostPort") == str(port.port)
|
||||
if not host_matching:
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -58,7 +83,7 @@ class DockerController(BaseController):
|
||||
try:
|
||||
return self.client.containers.get(container_name), False
|
||||
except NotFound:
|
||||
self.logger.info("Container does not exist, creating")
|
||||
self.logger.info("(Re-)creating container...")
|
||||
image_name = self.get_container_image()
|
||||
self.client.images.pull(image_name)
|
||||
container_args = {
|
||||
@ -86,6 +111,7 @@ class DockerController(BaseController):
|
||||
try:
|
||||
container, has_been_created = self._get_container()
|
||||
if has_been_created:
|
||||
container.start()
|
||||
return None
|
||||
# Check if the container is out of date, delete it and retry
|
||||
if len(container.image.tags) > 0:
|
||||
@ -98,6 +124,11 @@ class DockerController(BaseController):
|
||||
)
|
||||
self.down()
|
||||
return self.up()
|
||||
# Check container's ports
|
||||
if self._comp_ports(container):
|
||||
self.logger.info("Container has mis-matched ports, re-creating...")
|
||||
self.down()
|
||||
return self.up()
|
||||
# Check that container values match our values
|
||||
if self._comp_env(container):
|
||||
self.logger.info("Container has outdated config, re-creating...")
|
||||
@ -138,6 +169,7 @@ class DockerController(BaseController):
|
||||
self.logger.info("Container is not running, restarting...")
|
||||
container.start()
|
||||
return None
|
||||
self.logger.info("Container is running")
|
||||
return None
|
||||
except DockerException as exc:
|
||||
raise ControllerException(str(exc)) from exc
|
||||
|
||||
@ -405,7 +405,10 @@ class Outpost(models.Model):
|
||||
|
||||
def get_required_objects(self) -> Iterable[Union[models.Model, str]]:
|
||||
"""Get an iterator of all objects the user needs read access to"""
|
||||
objects: list[Union[models.Model, str]] = [self]
|
||||
objects: list[Union[models.Model, str]] = [
|
||||
self,
|
||||
"authentik_events.add_event",
|
||||
]
|
||||
for provider in (
|
||||
Provider.objects.filter(outpost=self).select_related().select_subclasses()
|
||||
):
|
||||
|
||||
@ -9,7 +9,7 @@ CELERY_BEAT_SCHEDULE = {
|
||||
},
|
||||
"outposts_service_connection_check": {
|
||||
"task": "authentik.outposts.tasks.outpost_service_connection_monitor",
|
||||
"schedule": crontab(minute="*/60"),
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
"outpost_token_ensurer": {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""authentik outpost signals"""
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete, pre_save
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
|
||||
from django.dispatch import receiver
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -46,6 +46,14 @@ def pre_save_outpost(sender, instance: Outpost, **_):
|
||||
outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Outpost.providers.through)
|
||||
# pylint: disable=unused-argument
|
||||
def m2m_changed_update(sender, instance: Model, action: str, **_):
|
||||
"""Update outpost on m2m change, when providers are added or removed"""
|
||||
if action in ["post_add", "post_remove", "post_clear"]:
|
||||
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_update(sender, instance: Model, **_):
|
||||
|
||||
@ -82,13 +82,13 @@ class PolicyBindingSerializer(ModelSerializer):
|
||||
"timeout",
|
||||
]
|
||||
|
||||
def validate(self, data: OrderedDict) -> OrderedDict:
|
||||
def validate(self, attrs: OrderedDict) -> OrderedDict:
|
||||
"""Check that either policy, group or user is set."""
|
||||
count = sum(
|
||||
[
|
||||
bool(data.get("policy", None)),
|
||||
bool(data.get("group", None)),
|
||||
bool(data.get("user", None)),
|
||||
bool(attrs.get("policy", None)),
|
||||
bool(attrs.get("group", None)),
|
||||
bool(attrs.get("user", None)),
|
||||
]
|
||||
)
|
||||
invalid = count > 1
|
||||
@ -97,7 +97,7 @@ class PolicyBindingSerializer(ModelSerializer):
|
||||
raise ValidationError("Only one of 'policy', 'group' or 'user' can be set.")
|
||||
if empty:
|
||||
raise ValidationError("One of 'policy', 'group' or 'user' must be set.")
|
||||
return data
|
||||
return attrs
|
||||
|
||||
|
||||
class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@ -62,12 +62,6 @@ class PolicyEngine:
|
||||
# Allow objects with no policies attached to pass
|
||||
empty_result: bool
|
||||
|
||||
__pbm: PolicyBindingModel
|
||||
__cached_policies: list[PolicyResult]
|
||||
__processes: list[PolicyProcessInfo]
|
||||
|
||||
__expected_result_count: int
|
||||
|
||||
def __init__(
|
||||
self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
|
||||
):
|
||||
@ -83,8 +77,8 @@ class PolicyEngine:
|
||||
self.request.obj = pbm
|
||||
if request:
|
||||
self.request.set_http_request(request)
|
||||
self.__cached_policies = []
|
||||
self.__processes = []
|
||||
self.__cached_policies: list[PolicyResult] = []
|
||||
self.__processes: list[PolicyProcessInfo] = []
|
||||
self.use_cache = True
|
||||
self.__expected_result_count = 0
|
||||
|
||||
|
||||
@ -33,21 +33,21 @@ class ReputationPolicy(Policy):
|
||||
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
remote_ip = get_client_ip(request.http_request)
|
||||
passing = True
|
||||
passing = False
|
||||
if self.check_ip:
|
||||
score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
|
||||
passing = passing and score <= self.threshold
|
||||
passing += passing or score <= self.threshold
|
||||
LOGGER.debug("Score for IP", ip=remote_ip, score=score, passing=passing)
|
||||
if self.check_username:
|
||||
score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0)
|
||||
passing = passing and score <= self.threshold
|
||||
passing += passing or score <= self.threshold
|
||||
LOGGER.debug(
|
||||
"Score for Username",
|
||||
username=request.user.username,
|
||||
score=score,
|
||||
passing=passing,
|
||||
)
|
||||
return PolicyResult(passing)
|
||||
return PolicyResult(bool(passing))
|
||||
|
||||
class Meta:
|
||||
|
||||
|
||||
@ -51,6 +51,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
|
||||
"expires",
|
||||
"scope",
|
||||
"id_token",
|
||||
"revoked",
|
||||
]
|
||||
depth = 2
|
||||
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-03 13:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_oauth2", "0014_alter_oauth2provider_rsa_key"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="authorizationcode",
|
||||
name="revoked",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="refreshtoken",
|
||||
name="revoked",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@ -278,7 +278,7 @@ class OAuth2Provider(Provider):
|
||||
"""Guess launch_url based on first redirect_uri"""
|
||||
if self.redirect_uris == "":
|
||||
return None
|
||||
main_url = self.redirect_uris.split("\n")[0]
|
||||
main_url = self.redirect_uris.split("\n", maxsplit=1)[0]
|
||||
launch_url = urlparse(main_url)
|
||||
return main_url.replace(launch_url.path, "")
|
||||
|
||||
@ -318,6 +318,7 @@ class BaseGrantModel(models.Model):
|
||||
provider = models.ForeignKey(OAuth2Provider, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
|
||||
_scope = models.TextField(default="", verbose_name=_("Scopes"))
|
||||
revoked = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def scope(self) -> list[str]:
|
||||
@ -473,9 +474,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
|
||||
# Convert datetimes into timestamps.
|
||||
now = int(time.time())
|
||||
iat_time = now
|
||||
exp_time = int(
|
||||
now + timedelta_from_string(self.provider.token_validity).seconds
|
||||
)
|
||||
exp_time = int(dateformat.format(self.expires, "U"))
|
||||
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
|
||||
auth_events = Event.objects.filter(
|
||||
action=EventAction.LOGIN, user=get_user(user)
|
||||
|
||||
@ -6,6 +6,8 @@ from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.oauth2.constants import (
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
@ -39,7 +41,8 @@ class TestToken(OAuthTestCase):
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
@ -53,11 +56,13 @@ class TestToken(OAuthTestCase):
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
"code": code.code,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"redirect_uri": "http://testserver",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
params = TokenParams.from_request(request)
|
||||
params = TokenParams.parse(
|
||||
request, provider, provider.client_id, provider.client_secret
|
||||
)
|
||||
self.assertEqual(params.provider, provider)
|
||||
|
||||
def test_request_refresh_token(self):
|
||||
@ -68,6 +73,7 @@ class TestToken(OAuthTestCase):
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
@ -87,7 +93,9 @@ class TestToken(OAuthTestCase):
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
params = TokenParams.from_request(request)
|
||||
params = TokenParams.parse(
|
||||
request, provider, provider.client_id, provider.client_secret
|
||||
)
|
||||
self.assertEqual(params.provider, provider)
|
||||
|
||||
def test_auth_code_view(self):
|
||||
@ -98,6 +106,7 @@ class TestToken(OAuthTestCase):
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
@ -141,6 +150,7 @@ class TestToken(OAuthTestCase):
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
@ -193,6 +203,7 @@ class TestToken(OAuthTestCase):
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
@ -230,3 +241,65 @@ class TestToken(OAuthTestCase):
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
def test_refresh_token_revoke(self):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
self.app.save()
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
)
|
||||
# Create initial refresh token
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||
"refresh_token": token.refresh_token,
|
||||
"redirect_uri": "http://testserver",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
new_token: RefreshToken = (
|
||||
RefreshToken.objects.filter(user=user).exclude(pk=token.pk).first()
|
||||
)
|
||||
# Post again with initial token -> get new refresh token
|
||||
# and revoke old one
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||
"refresh_token": new_token.refresh_token,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Post again with old token, is now revoked and should error
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||
"refresh_token": new_token.refresh_token,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertTrue(
|
||||
Event.objects.filter(action=EventAction.SUSPICIOUS_REQUEST).exists()
|
||||
)
|
||||
|
||||
@ -10,6 +10,7 @@ from django.http.response import HttpResponseRedirect
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.providers.oauth2.errors import BearerTokenError
|
||||
from authentik.providers.oauth2.models import RefreshToken
|
||||
|
||||
@ -50,7 +51,7 @@ def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: s
|
||||
if not allowed:
|
||||
LOGGER.warning(
|
||||
"CORS: Origin is not an allowed origin",
|
||||
requested=origin,
|
||||
requested=received_origin,
|
||||
allowed=allowed_origins,
|
||||
)
|
||||
return response
|
||||
@ -132,22 +133,31 @@ def protected_resource_view(scopes: list[str]):
|
||||
raise BearerTokenError("invalid_token")
|
||||
|
||||
try:
|
||||
kwargs["token"] = RefreshToken.objects.get(
|
||||
token: RefreshToken = RefreshToken.objects.get(
|
||||
access_token=access_token
|
||||
)
|
||||
except RefreshToken.DoesNotExist:
|
||||
LOGGER.debug("Token does not exist", access_token=access_token)
|
||||
raise BearerTokenError("invalid_token")
|
||||
|
||||
if kwargs["token"].is_expired:
|
||||
if token.is_expired:
|
||||
LOGGER.debug("Token has expired", access_token=access_token)
|
||||
raise BearerTokenError("invalid_token")
|
||||
|
||||
if not set(scopes).issubset(set(kwargs["token"].scope)):
|
||||
if token.revoked:
|
||||
LOGGER.warning("Revoked token was used", access_token=access_token)
|
||||
Event.new(
|
||||
action=EventAction.SUSPICIOUS_REQUEST,
|
||||
message="Revoked refresh token was used",
|
||||
token=access_token,
|
||||
).from_http(request)
|
||||
raise BearerTokenError("invalid_token")
|
||||
|
||||
if not set(scopes).issubset(set(token.scope)):
|
||||
LOGGER.warning(
|
||||
"Scope missmatch.",
|
||||
required=set(scopes),
|
||||
token_has=set(kwargs["token"].scope),
|
||||
token_has=set(token.scope),
|
||||
)
|
||||
raise BearerTokenError("insufficient_scope")
|
||||
except BearerTokenError as error:
|
||||
@ -156,7 +166,7 @@ def protected_resource_view(scopes: list[str]):
|
||||
"WWW-Authenticate"
|
||||
] = f'error="{error.code}", error_description="{error.description}"'
|
||||
return response
|
||||
|
||||
kwargs["token"] = token
|
||||
return view(request, *args, **kwargs)
|
||||
|
||||
return view_wrapper
|
||||
|
||||
@ -374,9 +374,9 @@ class OAuthFulfillmentStage(StageView):
|
||||
query_fragment["code"] = code.code
|
||||
|
||||
query_fragment["token_type"] = "bearer"
|
||||
query_fragment["expires_in"] = timedelta_from_string(
|
||||
self.provider.token_validity
|
||||
).seconds
|
||||
query_fragment["expires_in"] = int(
|
||||
timedelta_from_string(self.provider.token_validity).total_seconds()
|
||||
)
|
||||
query_fragment["state"] = self.params.state if self.params.state else ""
|
||||
|
||||
return query_fragment
|
||||
@ -468,14 +468,14 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||
# OpenID clients can specify a `prompt` parameter, and if its set to consent we
|
||||
# need to inject a consent stage
|
||||
if PROMPT_CONSNET in self.params.prompt:
|
||||
if not any(isinstance(x, ConsentStageView) for x in plan.stages):
|
||||
if not any(isinstance(x.stage, ConsentStageView) for x in plan.bindings):
|
||||
# Plan does not have any consent stage, so we add an in-memory one
|
||||
stage = ConsentStage(
|
||||
name="OAuth2 Provider In-memory consent stage",
|
||||
mode=ConsentMode.ALWAYS_REQUIRE,
|
||||
)
|
||||
plan.append(stage)
|
||||
plan.append(in_memory_stage(OAuthFulfillmentStage))
|
||||
plan.append_stage(stage)
|
||||
plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
|
||||
@ -8,6 +8,7 @@ from django.http import HttpRequest, HttpResponse
|
||||
from django.views import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.constants import (
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
@ -30,6 +31,7 @@ LOGGER = get_logger()
|
||||
|
||||
|
||||
@dataclass
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class TokenParams:
|
||||
"""Token params"""
|
||||
|
||||
@ -40,6 +42,8 @@ class TokenParams:
|
||||
state: str
|
||||
scope: list[str]
|
||||
|
||||
provider: OAuth2Provider
|
||||
|
||||
authorization_code: Optional[AuthorizationCode] = None
|
||||
refresh_token: Optional[RefreshToken] = None
|
||||
|
||||
@ -47,35 +51,34 @@ class TokenParams:
|
||||
|
||||
raw_code: InitVar[str] = ""
|
||||
raw_token: InitVar[str] = ""
|
||||
request: InitVar[Optional[HttpRequest]] = None
|
||||
|
||||
@staticmethod
|
||||
def from_request(request: HttpRequest) -> "TokenParams":
|
||||
"""Extract Token Parameters from http request"""
|
||||
client_id, client_secret = extract_client_auth(request)
|
||||
|
||||
def parse(
|
||||
request: HttpRequest,
|
||||
provider: OAuth2Provider,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
) -> "TokenParams":
|
||||
"""Parse params for request"""
|
||||
return TokenParams(
|
||||
# Init vars
|
||||
raw_code=request.POST.get("code", ""),
|
||||
raw_token=request.POST.get("refresh_token", ""),
|
||||
request=request,
|
||||
# Regular params
|
||||
provider=provider,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
redirect_uri=request.POST.get("redirect_uri", ""),
|
||||
grant_type=request.POST.get("grant_type", ""),
|
||||
raw_code=request.POST.get("code", ""),
|
||||
raw_token=request.POST.get("refresh_token", ""),
|
||||
state=request.POST.get("state", ""),
|
||||
scope=request.POST.get("scope", "").split(),
|
||||
# PKCE parameter.
|
||||
code_verifier=request.POST.get("code_verifier"),
|
||||
)
|
||||
|
||||
def __post_init__(self, raw_code, raw_token):
|
||||
try:
|
||||
provider: OAuth2Provider = OAuth2Provider.objects.get(
|
||||
client_id=self.client_id
|
||||
)
|
||||
self.provider = provider
|
||||
except OAuth2Provider.DoesNotExist:
|
||||
LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id)
|
||||
raise TokenError("invalid_client")
|
||||
|
||||
def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest):
|
||||
if self.provider.client_type == ClientTypes.CONFIDENTIAL:
|
||||
if self.provider.client_secret != self.client_secret:
|
||||
LOGGER.warning(
|
||||
@ -87,7 +90,6 @@ class TokenParams:
|
||||
|
||||
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
||||
self.__post_init_code(raw_code)
|
||||
|
||||
elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN:
|
||||
if not raw_token:
|
||||
LOGGER.warning("Missing refresh token")
|
||||
@ -107,7 +109,14 @@ class TokenParams:
|
||||
token=raw_token,
|
||||
)
|
||||
raise TokenError("invalid_grant")
|
||||
|
||||
if self.refresh_token.revoked:
|
||||
LOGGER.warning("Refresh token is revoked", token=raw_token)
|
||||
Event.new(
|
||||
action=EventAction.SUSPICIOUS_REQUEST,
|
||||
message="Revoked refresh token was used",
|
||||
token=raw_token,
|
||||
).from_http(request)
|
||||
raise TokenError("invalid_grant")
|
||||
else:
|
||||
LOGGER.warning("Invalid grant type", grant_type=self.grant_type)
|
||||
raise TokenError("unsupported_grant_type")
|
||||
@ -159,13 +168,14 @@ class TokenParams:
|
||||
class TokenView(View):
|
||||
"""Generate tokens for clients"""
|
||||
|
||||
provider: Optional[OAuth2Provider] = None
|
||||
params: Optional[TokenParams] = None
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
allowed_origins = []
|
||||
if self.params:
|
||||
allowed_origins = self.params.provider.redirect_uris.split("\n")
|
||||
if self.provider:
|
||||
allowed_origins = self.provider.redirect_uris.split("\n")
|
||||
cors_allow(self.request, response, *allowed_origins)
|
||||
return response
|
||||
|
||||
@ -175,19 +185,32 @@ class TokenView(View):
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Generate tokens for clients"""
|
||||
try:
|
||||
self.params = TokenParams.from_request(request)
|
||||
client_id, client_secret = extract_client_auth(request)
|
||||
try:
|
||||
self.provider = OAuth2Provider.objects.get(client_id=client_id)
|
||||
except OAuth2Provider.DoesNotExist:
|
||||
LOGGER.warning(
|
||||
"OAuth2Provider does not exist", client_id=self.client_id
|
||||
)
|
||||
raise TokenError("invalid_client")
|
||||
|
||||
if not self.provider:
|
||||
raise ValueError
|
||||
self.params = TokenParams.parse(
|
||||
request, self.provider, client_id, client_secret
|
||||
)
|
||||
|
||||
if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
||||
return TokenResponse(self.create_code_response_dic())
|
||||
return TokenResponse(self.create_code_response())
|
||||
if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
|
||||
return TokenResponse(self.create_refresh_response_dic())
|
||||
return TokenResponse(self.create_refresh_response())
|
||||
raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
|
||||
except TokenError as error:
|
||||
return TokenResponse(error.create_dict(), status=400)
|
||||
except UserAuthError as error:
|
||||
return TokenResponse(error.create_dict(), status=403)
|
||||
|
||||
def create_code_response_dic(self) -> dict[str, Any]:
|
||||
def create_code_response(self) -> dict[str, Any]:
|
||||
"""See https://tools.ietf.org/html/rfc6749#section-4.1"""
|
||||
|
||||
refresh_token = self.params.authorization_code.provider.create_refresh_token(
|
||||
@ -211,19 +234,19 @@ class TokenView(View):
|
||||
# We don't need to store the code anymore.
|
||||
self.params.authorization_code.delete()
|
||||
|
||||
response_dict = {
|
||||
return {
|
||||
"access_token": refresh_token.access_token,
|
||||
"refresh_token": refresh_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": timedelta_from_string(
|
||||
self.params.provider.token_validity
|
||||
).seconds,
|
||||
"expires_in": int(
|
||||
timedelta_from_string(
|
||||
self.params.provider.token_validity
|
||||
).total_seconds()
|
||||
),
|
||||
"id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
|
||||
}
|
||||
|
||||
return response_dict
|
||||
|
||||
def create_refresh_response_dic(self) -> dict[str, Any]:
|
||||
def create_refresh_response(self) -> dict[str, Any]:
|
||||
"""See https://tools.ietf.org/html/rfc6749#section-6"""
|
||||
|
||||
unauthorized_scopes = set(self.params.scope) - set(
|
||||
@ -251,17 +274,18 @@ class TokenView(View):
|
||||
# Store the refresh_token.
|
||||
refresh_token.save()
|
||||
|
||||
# Forget the old token.
|
||||
self.params.refresh_token.delete()
|
||||
# Mark old token as revoked
|
||||
self.params.refresh_token.revoked = True
|
||||
self.params.refresh_token.save()
|
||||
|
||||
dic = {
|
||||
return {
|
||||
"access_token": refresh_token.access_token,
|
||||
"refresh_token": refresh_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": timedelta_from_string(
|
||||
refresh_token.provider.token_validity
|
||||
).seconds,
|
||||
"expires_in": int(
|
||||
timedelta_from_string(
|
||||
refresh_token.provider.token_validity
|
||||
).total_seconds()
|
||||
),
|
||||
"id_token": self.params.provider.encode(refresh_token.id_token.to_dict()),
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""authentik OAuth2 OpenID Userinfo views"""
|
||||
from typing import Any, Optional
|
||||
|
||||
from deepmerge import always_merger
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.views import View
|
||||
@ -78,7 +79,7 @@ class UserInfoView(View):
|
||||
)
|
||||
continue
|
||||
LOGGER.debug("updated scope", scope=scope)
|
||||
final_claims.update(value)
|
||||
always_merger.merge(final_claims, value)
|
||||
return final_claims
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
|
||||
@ -79,7 +79,7 @@ class SAMLSSOView(PolicyAccessView):
|
||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
||||
},
|
||||
)
|
||||
plan.append(in_memory_stage(SAMLFlowFinalView))
|
||||
plan.append_stage(in_memory_stage(SAMLFlowFinalView))
|
||||
request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
|
||||
@ -15,7 +15,7 @@ class MessageConsumer(JsonWebsocketConsumer):
|
||||
cache.set(f"user_{self.session_key}_messages_{self.channel_name}", True, None)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def disconnect(self, close_code):
|
||||
def disconnect(self, code):
|
||||
cache.delete(f"user_{self.session_key}_messages_{self.channel_name}")
|
||||
|
||||
def event_update(self, event: dict):
|
||||
|
||||
@ -153,6 +153,7 @@ SPECTACULAR_SETTINGS = {
|
||||
"url": "https://github.com/goauthentik/authentik/blob/master/LICENSE",
|
||||
},
|
||||
"ENUM_NAME_OVERRIDES": {
|
||||
"EventActions": "authentik.events.models.EventAction",
|
||||
"ChallengeChoices": "authentik.flows.challenge.ChallengeTypes",
|
||||
"FlowDesignationEnum": "authentik.flows.models.FlowDesignation",
|
||||
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
|
||||
|
||||
@ -60,14 +60,21 @@ class LDAPPasswordChanger:
|
||||
def check_ad_password_complexity_enabled(self) -> bool:
|
||||
"""Check if DOMAIN_PASSWORD_COMPLEX is enabled"""
|
||||
root_dn = self.get_domain_root_dn()
|
||||
root_attrs = self._source.connection.extend.standard.paged_search(
|
||||
search_base=root_dn,
|
||||
search_filter="(objectClass=*)",
|
||||
search_scope=ldap3.BASE,
|
||||
attributes=["pwdProperties"],
|
||||
)
|
||||
try:
|
||||
root_attrs = self._source.connection.extend.standard.paged_search(
|
||||
search_base=root_dn,
|
||||
search_filter="(objectClass=*)",
|
||||
search_scope=ldap3.BASE,
|
||||
attributes=["pwdProperties"],
|
||||
)
|
||||
except ldap3.core.exceptions.LDAPAttributeError:
|
||||
return False
|
||||
root_attrs = list(root_attrs)[0]
|
||||
pwd_properties = PwdProperties(root_attrs["attributes"]["pwdProperties"])
|
||||
raw_pwd_properties = root_attrs.get("attributes", {}).get("pwdProperties", None)
|
||||
if raw_pwd_properties is None:
|
||||
return False
|
||||
|
||||
pwd_properties = PwdProperties(raw_pwd_properties)
|
||||
if PwdProperties.DOMAIN_PASSWORD_COMPLEX in pwd_properties:
|
||||
return True
|
||||
|
||||
|
||||
@ -36,7 +36,8 @@ class SourceType:
|
||||
class SourceTypeManager:
|
||||
"""Manager to hold all Source types."""
|
||||
|
||||
__sources: list[SourceType] = []
|
||||
def __init__(self) -> None:
|
||||
self.__sources: list[SourceType] = []
|
||||
|
||||
def type(self):
|
||||
"""Class decorator to register classes inline."""
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"""OAuth Callback Views"""
|
||||
from json import JSONDecodeError
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.conf import settings
|
||||
@ -10,6 +11,7 @@ from django.views.generic import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.views.base import OAuthClientMixin
|
||||
|
||||
@ -42,8 +44,16 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
if "error" in token:
|
||||
return self.handle_login_failure(token["error"])
|
||||
# Fetch profile info
|
||||
raw_info = client.get_profile_info(token)
|
||||
if raw_info is None:
|
||||
try:
|
||||
raw_info = client.get_profile_info(token)
|
||||
if raw_info is None:
|
||||
return self.handle_login_failure("Could not retrieve profile.")
|
||||
except JSONDecodeError as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message="Failed to JSON-decode profile.",
|
||||
raw_profile=exc.doc,
|
||||
).from_http(self.request)
|
||||
return self.handle_login_failure("Could not retrieve profile.")
|
||||
identifier = self.get_user_id(raw_info)
|
||||
if identifier is None:
|
||||
|
||||
@ -90,7 +90,7 @@ class InitiateView(View):
|
||||
planner.allow_empty_flows = True
|
||||
plan = planner.plan(self.request, kwargs)
|
||||
for stage in stages_to_append:
|
||||
plan.append(stage)
|
||||
plan.append_stage(stage)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
|
||||
@ -63,7 +63,7 @@ class AuthenticatorDuoStageView(ChallengeStageView):
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
"activation_barcode": enroll["activation_barcode"],
|
||||
"activation_code": enroll["activation_code"],
|
||||
"stage_uuid": stage.stage_uuid,
|
||||
"stage_uuid": str(stage.stage_uuid),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -74,12 +74,12 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||
duo, self.stage.request, self.stage.get_pending_user()
|
||||
)
|
||||
|
||||
def validate(self, data: dict):
|
||||
def validate(self, attrs: dict):
|
||||
# Checking if the given data is from a valid device class is done above
|
||||
# Here we only check if the any data was sent at all
|
||||
if "code" not in data and "webauthn" not in data and "duo" not in data:
|
||||
if "code" not in attrs and "webauthn" not in attrs and "duo" not in attrs:
|
||||
raise ValidationError("Empty response")
|
||||
return data
|
||||
return attrs
|
||||
|
||||
|
||||
class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
@ -148,7 +148,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
stage = Stage.objects.get_subclass(pk=stage.configuration_stage.pk)
|
||||
# plan.insert inserts at 1 index, so when stage_ok pops 0,
|
||||
# the configuration stage is next
|
||||
self.executor.plan.insert(stage)
|
||||
self.executor.plan.insert_stage(stage)
|
||||
return self.executor.stage_ok()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@ -163,7 +163,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def challenge_valid(
|
||||
self, challenge: AuthenticatorValidationChallengeResponse
|
||||
self, response: AuthenticatorValidationChallengeResponse
|
||||
) -> HttpResponse:
|
||||
# All validation is done by the serializer
|
||||
return self.executor.stage_ok()
|
||||
|
||||
@ -36,12 +36,14 @@ class TestCaptchaStage(TestCase):
|
||||
public_key=RECAPTCHA_PUBLIC_KEY,
|
||||
private_key=RECAPTCHA_PRIVATE_KEY,
|
||||
)
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
|
||||
def test_valid(self):
|
||||
"""Test valid captcha"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
|
||||
@ -39,9 +39,11 @@ class TestConsentStage(TestCase):
|
||||
stage = ConsentStage.objects.create(
|
||||
name="consent", mode=ConsentMode.ALWAYS_REQUIRE
|
||||
)
|
||||
FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
|
||||
binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
|
||||
|
||||
plan = FlowPlan(flow_pk=flow.pk.hex, stages=[stage], markers=[StageMarker()])
|
||||
plan = FlowPlan(
|
||||
flow_pk=flow.pk.hex, bindings=[binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -69,11 +71,11 @@ class TestConsentStage(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
stage = ConsentStage.objects.create(name="consent", mode=ConsentMode.PERMANENT)
|
||||
FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
|
||||
binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
|
||||
|
||||
plan = FlowPlan(
|
||||
flow_pk=flow.pk.hex,
|
||||
stages=[stage],
|
||||
bindings=[binding],
|
||||
markers=[StageMarker()],
|
||||
context={PLAN_CONTEXT_APPLICATION: self.application},
|
||||
)
|
||||
@ -110,11 +112,11 @@ class TestConsentStage(TestCase):
|
||||
stage = ConsentStage.objects.create(
|
||||
name="consent", mode=ConsentMode.EXPIRING, consent_expire_in="seconds=1"
|
||||
)
|
||||
FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
|
||||
binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
|
||||
|
||||
plan = FlowPlan(
|
||||
flow_pk=flow.pk.hex,
|
||||
stages=[stage],
|
||||
bindings=[binding],
|
||||
markers=[StageMarker()],
|
||||
context={PLAN_CONTEXT_APPLICATION: self.application},
|
||||
)
|
||||
|
||||
@ -26,12 +26,14 @@ class TestUserDenyStage(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
self.stage = DenyStage.objects.create(name="logout")
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
|
||||
def test_valid_password(self):
|
||||
"""Test with a valid pending user and backend"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
|
||||
@ -38,7 +38,7 @@ class EmailChallengeResponse(ChallengeResponse):
|
||||
|
||||
component = CharField(default="ak-stage-email")
|
||||
|
||||
def validate(self, data):
|
||||
def validate(self, attrs):
|
||||
raise ValidationError("")
|
||||
|
||||
|
||||
|
||||
@ -34,12 +34,14 @@ class TestEmailStageSending(TestCase):
|
||||
self.stage = EmailStage.objects.create(
|
||||
name="email",
|
||||
)
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
|
||||
def test_pending_user(self):
|
||||
"""Test with pending user"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
@ -67,7 +69,7 @@ class TestEmailStageSending(TestCase):
|
||||
def test_send_error(self):
|
||||
"""Test error during sending (sending will be retried)"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
|
||||
@ -35,12 +35,14 @@ class TestEmailStage(TestCase):
|
||||
self.stage = EmailStage.objects.create(
|
||||
name="email",
|
||||
)
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
|
||||
def test_rendering(self):
|
||||
"""Test with pending user"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
@ -56,7 +58,7 @@ class TestEmailStage(TestCase):
|
||||
def test_without_user(self):
|
||||
"""Test without pending user"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -71,7 +73,7 @@ class TestEmailStage(TestCase):
|
||||
def test_pending_user(self):
|
||||
"""Test with pending user"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
@ -102,7 +104,7 @@ class TestEmailStage(TestCase):
|
||||
# Make sure token exists
|
||||
self.test_pending_user()
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
|
||||
@ -73,9 +73,9 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
|
||||
pre_user: Optional[User] = None
|
||||
|
||||
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate that user exists, and optionally their password"""
|
||||
uid_field = data["uid_field"]
|
||||
uid_field = attrs["uid_field"]
|
||||
current_stage: IdentificationStage = self.stage.executor.current_stage
|
||||
|
||||
pre_user = self.stage.get_user(uid_field)
|
||||
@ -85,13 +85,25 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
identification_failed.send(
|
||||
sender=self, request=self.stage.request, uid_field=uid_field
|
||||
)
|
||||
# We set the pending_user even on failure so it's part of the context, even
|
||||
# when the input is invalid
|
||||
# This is so its part of the current flow plan, and on flow restart can be kept, and
|
||||
# policies can be applied.
|
||||
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
|
||||
username=uid_field,
|
||||
email=uid_field,
|
||||
)
|
||||
if not current_stage.show_matched_user:
|
||||
self.stage.executor.plan.context[
|
||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER
|
||||
] = uid_field
|
||||
raise ValidationError("Failed to authenticate.")
|
||||
self.pre_user = pre_user
|
||||
if not current_stage.password_stage:
|
||||
# No password stage select, don't validate the password
|
||||
return data
|
||||
return attrs
|
||||
|
||||
password = data["password"]
|
||||
password = attrs["password"]
|
||||
try:
|
||||
user = authenticate(
|
||||
self.stage.request,
|
||||
@ -104,7 +116,7 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
self.pre_user = user
|
||||
except PermissionDenied as exc:
|
||||
raise ValidationError(str(exc)) from exc
|
||||
return data
|
||||
return attrs
|
||||
|
||||
|
||||
class IdentificationStageView(ChallengeStageView):
|
||||
|
||||
@ -35,7 +35,9 @@ class TestUserLoginStage(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
self.stage = InvitationStage.objects.create(name="invitation")
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
@ -44,7 +46,7 @@ class TestUserLoginStage(TestCase):
|
||||
def test_without_invitation_fail(self):
|
||||
"""Test without any invitation, continue_flow_without_invitation not set."""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
|
||||
@ -75,7 +77,7 @@ class TestUserLoginStage(TestCase):
|
||||
self.stage.continue_flow_without_invitation = True
|
||||
self.stage.save()
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
|
||||
@ -103,7 +105,7 @@ class TestUserLoginStage(TestCase):
|
||||
def test_with_invitation_get(self):
|
||||
"""Test with invitation, check data in session"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -143,7 +145,7 @@ class TestUserLoginStage(TestCase):
|
||||
)
|
||||
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {INVITATION_TOKEN_KEY: invite.pk.hex}
|
||||
session = self.client.session
|
||||
|
||||
@ -39,7 +39,9 @@ class TestPasswordStage(TestCase):
|
||||
self.stage = PasswordStage.objects.create(
|
||||
name="password", backends=[BACKEND_DJANGO]
|
||||
)
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
@ -48,7 +50,7 @@ class TestPasswordStage(TestCase):
|
||||
def test_without_user(self):
|
||||
"""Test without user"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -84,7 +86,7 @@ class TestPasswordStage(TestCase):
|
||||
)
|
||||
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -101,7 +103,7 @@ class TestPasswordStage(TestCase):
|
||||
def test_valid_password(self):
|
||||
"""Test with a valid pending user and valid password"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
@ -129,7 +131,7 @@ class TestPasswordStage(TestCase):
|
||||
def test_invalid_password(self):
|
||||
"""Test with a valid pending user and invalid password"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
@ -148,7 +150,7 @@ class TestPasswordStage(TestCase):
|
||||
def test_invalid_password_lockout(self):
|
||||
"""Test with a valid pending user and invalid password (trigger logout counter)"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
@ -189,7 +191,7 @@ class TestPasswordStage(TestCase):
|
||||
"""Test with a valid pending user and valid password.
|
||||
Backend is patched to return PermissionError"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
|
||||
@ -90,6 +90,14 @@ class PromptChallengeResponse(ChallengeResponse):
|
||||
raise ValidationError(_("Passwords don't match."))
|
||||
|
||||
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
||||
# Check if we have any static or hidden fields, and ensure they
|
||||
# still have the same value
|
||||
static_hidden_fields: QuerySet[Prompt] = self.stage.fields.filter(
|
||||
type__in=[FieldTypes.HIDDEN, FieldTypes.STATIC]
|
||||
)
|
||||
for static_hidden in static_hidden_fields:
|
||||
attrs[static_hidden.field_key] = static_hidden.placeholder
|
||||
|
||||
# Check if we have two password fields, and make sure they are the same
|
||||
password_fields: QuerySet[Prompt] = self.stage.fields.filter(
|
||||
type=FieldTypes.PASSWORD
|
||||
@ -138,8 +146,6 @@ def password_single_validator_factory() -> Callable[[PromptChallenge, str], Any]
|
||||
class ListPolicyEngine(PolicyEngine):
|
||||
"""Slightly modified policy engine, which uses a list instead of a PolicyBindingModel"""
|
||||
|
||||
__list: list[Policy]
|
||||
|
||||
def __init__(
|
||||
self, policies: list[Policy], user: User, request: HttpRequest = None
|
||||
) -> None:
|
||||
|
||||
@ -78,6 +78,12 @@ class TestPromptStage(TestCase):
|
||||
required=True,
|
||||
placeholder="HIDDEN_PLACEHOLDER",
|
||||
)
|
||||
static_prompt = Prompt.objects.create(
|
||||
field_key="static_prompt",
|
||||
type=FieldTypes.STATIC,
|
||||
required=True,
|
||||
placeholder="static",
|
||||
)
|
||||
self.stage = PromptStage.objects.create(name="prompt-stage")
|
||||
self.stage.fields.set(
|
||||
[
|
||||
@ -88,6 +94,7 @@ class TestPromptStage(TestCase):
|
||||
password2_prompt,
|
||||
number_prompt,
|
||||
hidden_prompt,
|
||||
static_prompt,
|
||||
]
|
||||
)
|
||||
self.stage.save()
|
||||
@ -100,14 +107,17 @@ class TestPromptStage(TestCase):
|
||||
password2_prompt.field_key: "test",
|
||||
number_prompt.field_key: 3,
|
||||
hidden_prompt.field_key: hidden_prompt.placeholder,
|
||||
static_prompt.field_key: static_prompt.placeholder,
|
||||
}
|
||||
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
|
||||
def test_render(self):
|
||||
"""Test render of form, check if all prompts are rendered correctly"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -125,7 +135,7 @@ class TestPromptStage(TestCase):
|
||||
def test_valid_challenge_with_policy(self) -> PromptChallengeResponse:
|
||||
"""Test challenge_response validation"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
expr = "return request.context['password_prompt'] == request.context['password2_prompt']"
|
||||
expr_policy = ExpressionPolicy.objects.create(
|
||||
@ -142,7 +152,7 @@ class TestPromptStage(TestCase):
|
||||
def test_invalid_challenge(self) -> PromptChallengeResponse:
|
||||
"""Test challenge_response validation"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
expr = "False"
|
||||
expr_policy = ExpressionPolicy.objects.create(
|
||||
@ -159,7 +169,7 @@ class TestPromptStage(TestCase):
|
||||
def test_valid_challenge_request(self):
|
||||
"""Test a request with valid challenge_response data"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -196,7 +206,7 @@ class TestPromptStage(TestCase):
|
||||
def test_invalid_password(self):
|
||||
"""Test challenge_response validation"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
self.prompt_data["password2_prompt"] = "qwerqwerqr"
|
||||
challenge_response = PromptChallengeResponse(
|
||||
@ -215,7 +225,7 @@ class TestPromptStage(TestCase):
|
||||
def test_invalid_username(self):
|
||||
"""Test challenge_response validation"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
self.prompt_data["username_prompt"] = "akadmin"
|
||||
challenge_response = PromptChallengeResponse(
|
||||
@ -230,3 +240,17 @@ class TestPromptStage(TestCase):
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
def test_static_hidden_overwrite(self):
|
||||
"""Test that static and hidden fields ignore any value sent to them"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
self.prompt_data["hidden_prompt"] = "foo"
|
||||
self.prompt_data["static_prompt"] = "foo"
|
||||
challenge_response = PromptChallengeResponse(
|
||||
None, stage=self.stage, plan=plan, data=self.prompt_data
|
||||
)
|
||||
self.assertEqual(challenge_response.is_valid(), True)
|
||||
self.assertNotEqual(challenge_response.validated_data["hidden_prompt"], "foo")
|
||||
self.assertNotEqual(challenge_response.validated_data["static_prompt"], "foo")
|
||||
|
||||
@ -30,7 +30,9 @@ class TestUserDeleteStage(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
self.stage = UserDeleteStage.objects.create(name="delete")
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
@ -39,7 +41,7 @@ class TestUserDeleteStage(TestCase):
|
||||
def test_no_user(self):
|
||||
"""Test without user set"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -66,7 +68,7 @@ class TestUserDeleteStage(TestCase):
|
||||
def test_user_delete_get(self):
|
||||
"""Test Form render"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
|
||||
@ -30,12 +30,14 @@ class TestUserLoginStage(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
self.stage = UserLoginStage.objects.create(name="login")
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
|
||||
def test_valid_password(self):
|
||||
"""Test with a valid pending user and backend"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
@ -61,7 +63,7 @@ class TestUserLoginStage(TestCase):
|
||||
self.stage.session_duration = "seconds=2"
|
||||
self.stage.save()
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
@ -92,7 +94,7 @@ class TestUserLoginStage(TestCase):
|
||||
def test_without_user(self):
|
||||
"""Test a plan without any pending user, resulting in a denied"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
|
||||
@ -28,12 +28,14 @@ class TestUserLogoutStage(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
self.stage = UserLogoutStage.objects.create(name="logout")
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
|
||||
def test_valid_password(self):
|
||||
"""Test with a valid pending user and backend"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
|
||||
|
||||
@ -12,7 +12,7 @@ class UserWriteStageSerializer(StageSerializer):
|
||||
class Meta:
|
||||
|
||||
model = UserWriteStage
|
||||
fields = StageSerializer.Meta.fields
|
||||
fields = StageSerializer.Meta.fields + ["create_users_as_inactive"]
|
||||
|
||||
|
||||
class UserWriteStageViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-28 20:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_user_write", "0002_auto_20200918_1653"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userwritestage",
|
||||
name="create_users_as_inactive",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="When set, newly created users are inactive and cannot login.",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,6 +1,7 @@
|
||||
"""write stage models"""
|
||||
from typing import Type
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
@ -12,6 +13,11 @@ class UserWriteStage(Stage):
|
||||
"""Writes currently pending data into the pending user, or if no user exists,
|
||||
creates a new user with the data."""
|
||||
|
||||
create_users_as_inactive = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_("When set, newly created users are inactive and cannot login."),
|
||||
)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.stages.user_write.api import UserWriteStageSerializer
|
||||
|
||||
@ -24,6 +24,10 @@ LOGGER = get_logger()
|
||||
class UserWriteStageView(StageView):
|
||||
"""Finalise Enrollment flow by creating a user object."""
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Wrapper for post requests"""
|
||||
return self.get(request)
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Save data in the current flow to the currently pending user. If no user is pending,
|
||||
a new user is created."""
|
||||
@ -35,7 +39,9 @@ class UserWriteStageView(StageView):
|
||||
data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
|
||||
user_created = False
|
||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User()
|
||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
|
||||
is_active=not self.executor.current_stage.create_users_as_inactive
|
||||
)
|
||||
self.executor.plan.context[
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
] = class_to_path(ModelBackend)
|
||||
|
||||
@ -37,7 +37,9 @@ class TestUserWriteStage(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
self.stage = UserWriteStage.objects.create(name="write")
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.binding = FlowStageBinding.objects.create(
|
||||
target=self.flow, stage=self.stage, order=2
|
||||
)
|
||||
self.source = Source.objects.create(name="fake_source")
|
||||
|
||||
def test_user_create(self):
|
||||
@ -48,7 +50,7 @@ class TestUserWriteStage(TestCase):
|
||||
)
|
||||
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||
"username": "test-user",
|
||||
@ -92,7 +94,7 @@ class TestUserWriteStage(TestCase):
|
||||
for _ in range(8)
|
||||
)
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create(
|
||||
username="unittest", email="test@beryju.org"
|
||||
@ -135,7 +137,7 @@ class TestUserWriteStage(TestCase):
|
||||
def test_without_data(self):
|
||||
"""Test without data results in error"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -167,7 +169,7 @@ class TestUserWriteStage(TestCase):
|
||||
def test_blank_username(self):
|
||||
"""Test with blank username results in error"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||
@ -204,7 +206,7 @@ class TestUserWriteStage(TestCase):
|
||||
def test_duplicate_data(self):
|
||||
"""Test with duplicate data, should trigger error"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||
|
||||
@ -54,6 +54,9 @@ class CurrentTenantSerializer(PassiveSerializer):
|
||||
default=CONFIG.y("footer_links", []),
|
||||
)
|
||||
|
||||
flow_authentication = CharField(source="flow_authentication.slug", required=False)
|
||||
flow_invalidation = CharField(source="flow_invalidation.slug", required=False)
|
||||
flow_recovery = CharField(source="flow_recovery.slug", required=False)
|
||||
flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False)
|
||||
|
||||
|
||||
|
||||
@ -20,6 +20,8 @@ class TestTenants(TestCase):
|
||||
"branding_title": "authentik",
|
||||
"matched_domain": "authentik-default",
|
||||
"ui_footer_links": CONFIG.y("footer_links"),
|
||||
"flow_authentication": "default-authentication-flow",
|
||||
"flow_invalidation": "default-invalidation-flow",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.4}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -44,7 +44,7 @@ services:
|
||||
- "0.0.0.0:9000:9000"
|
||||
- "0.0.0.0:9443:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.4}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
networks:
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
package constants
|
||||
|
||||
const VERSION = "2021.6.2"
|
||||
const VERSION = "2021.6.4"
|
||||
|
||||
@ -10,15 +10,19 @@ import (
|
||||
|
||||
func (ws *WebServer) configureStatic() {
|
||||
statRouter := ws.lh.NewRoute().Subrouter()
|
||||
// Media files, always local
|
||||
fs := http.FileServer(http.Dir(config.G.Paths.Media))
|
||||
if config.G.Debug || config.G.Web.LoadLocalFiles {
|
||||
ws.log.Debug("Using local static files")
|
||||
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist"))))
|
||||
ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik"))))
|
||||
statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist"))))
|
||||
statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik"))))
|
||||
statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs))
|
||||
} else {
|
||||
statRouter.Use(ws.staticHeaderMiddleware)
|
||||
ws.log.Debug("Using packaged static files with aggressive caching")
|
||||
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist))))
|
||||
ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik))))
|
||||
statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist))))
|
||||
statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik))))
|
||||
statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs))
|
||||
}
|
||||
ws.lh.Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header()["Content-Type"] = []string{"text/plain"}
|
||||
@ -30,8 +34,6 @@ func (ws *WebServer) configureStatic() {
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(staticWeb.SecurityTxt)
|
||||
})
|
||||
// Media files, always local
|
||||
ws.lh.PathPrefix("/media").Handler(http.StripPrefix("/media", http.FileServer(http.Dir(config.G.Paths.Media))))
|
||||
}
|
||||
|
||||
func (ws *WebServer) staticHeaderMiddleware(h http.Handler) http.Handler {
|
||||
|
||||
@ -98,20 +98,15 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||
},
|
||||
}
|
||||
|
||||
if *u.IsActive {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}})
|
||||
} else {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}})
|
||||
}
|
||||
|
||||
if u.IsSuperuser {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"inactive"}})
|
||||
} else {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"active"}})
|
||||
}
|
||||
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)})
|
||||
|
||||
// Old fields for backwards compatibility
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{BoolToString(*u.IsActive)}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{BoolToString(u.IsSuperuser)}})
|
||||
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/active", Values: []string{BoolToString(*u.IsActive)}})
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/superuser", Values: []string{BoolToString(u.IsSuperuser)}})
|
||||
|
||||
attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...)
|
||||
|
||||
dn := fmt.Sprintf("cn=%s,%s", u.Username, pi.UserDN)
|
||||
|
||||
@ -7,6 +7,13 @@ import (
|
||||
"goauthentik.io/outpost/api"
|
||||
)
|
||||
|
||||
func BoolToString(in bool) string {
|
||||
if in {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
|
||||
attrList := []*ldap.EntryAttribute{}
|
||||
a := attrs.(*map[string]interface{})
|
||||
@ -17,6 +24,8 @@ func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
|
||||
entry.Values = t
|
||||
case string:
|
||||
entry.Values = []string{t}
|
||||
case bool:
|
||||
entry.Values = []string{BoolToString(t)}
|
||||
}
|
||||
attrList = append(attrList, entry)
|
||||
}
|
||||
|
||||
@ -29,9 +29,10 @@ func (s *Server) bundleProviders(providers []api.ProxyOutpostConfig) []*provider
|
||||
log.WithError(err).Warning("Failed to parse URL, skipping provider")
|
||||
}
|
||||
bundles[idx] = &providerBundle{
|
||||
s: s,
|
||||
Host: externalHost.Host,
|
||||
log: log.WithField("logger", "authentik.outpost.proxy-bundle").WithField("provider", provider.Name),
|
||||
s: s,
|
||||
Host: externalHost.Host,
|
||||
log: log.WithField("logger", "authentik.outpost.proxy-bundle").WithField("provider", provider.Name),
|
||||
endSessionUrl: provider.OidcConfiguration.EndSessionEndpoint,
|
||||
}
|
||||
bundles[idx].Build(provider)
|
||||
}
|
||||
|
||||
@ -25,6 +25,8 @@ type providerBundle struct {
|
||||
proxy *OAuthProxy
|
||||
Host string
|
||||
|
||||
endSessionUrl string
|
||||
|
||||
cert *tls.Certificate
|
||||
|
||||
log *log.Entry
|
||||
@ -58,6 +60,8 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.
|
||||
providerOpts.RedeemURL = provider.OidcConfiguration.TokenEndpoint
|
||||
providerOpts.OIDCJwksURL = provider.OidcConfiguration.JwksUri
|
||||
providerOpts.ProfileURL = provider.OidcConfiguration.UserinfoEndpoint
|
||||
providerOpts.ValidateURL = provider.OidcConfiguration.UserinfoEndpoint
|
||||
providerOpts.AcrValues = "goauthentik.io/providers/oauth2/default"
|
||||
|
||||
if *provider.SkipPathRegex != "" {
|
||||
skipRegexes := strings.Split(*provider.SkipPathRegex, "\n")
|
||||
@ -153,6 +157,7 @@ func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
|
||||
oauthproxy.BasicAuthPasswordAttribute = *provider.BasicAuthPasswordAttribute
|
||||
}
|
||||
|
||||
oauthproxy.endSessionEndpoint = pb.endSessionUrl
|
||||
oauthproxy.ExternalHost = pb.Host
|
||||
|
||||
pb.proxy = oauthproxy
|
||||
|
||||
@ -65,31 +65,33 @@ type OAuthProxy struct {
|
||||
AuthOnlyPath string
|
||||
UserInfoPath string
|
||||
|
||||
endSessionEndpoint string
|
||||
mode api.ProxyMode
|
||||
redirectURL *url.URL // the url to receive requests at
|
||||
whitelistDomains []string
|
||||
provider providers.Provider
|
||||
sessionStore sessionsapi.SessionStore
|
||||
ProxyPrefix string
|
||||
serveMux http.Handler
|
||||
SetXAuthRequest bool
|
||||
SetBasicAuth bool
|
||||
PassUserHeaders bool
|
||||
BasicAuthUserAttribute string
|
||||
BasicAuthPasswordAttribute string
|
||||
ExternalHost string
|
||||
PassAccessToken bool
|
||||
SetAuthorization bool
|
||||
PassAuthorization bool
|
||||
PreferEmailToUser bool
|
||||
skipAuthRegex []string
|
||||
skipAuthPreflight bool
|
||||
skipAuthStripHeaders bool
|
||||
mainJwtBearerVerifier *oidc.IDTokenVerifier
|
||||
extraJwtBearerVerifiers []*oidc.IDTokenVerifier
|
||||
compiledRegex []*regexp.Regexp
|
||||
templates *template.Template
|
||||
realClientIPParser ipapi.RealClientIPParser
|
||||
|
||||
redirectURL *url.URL // the url to receive requests at
|
||||
whitelistDomains []string
|
||||
provider providers.Provider
|
||||
sessionStore sessionsapi.SessionStore
|
||||
ProxyPrefix string
|
||||
serveMux http.Handler
|
||||
SetXAuthRequest bool
|
||||
SetBasicAuth bool
|
||||
PassUserHeaders bool
|
||||
PassAccessToken bool
|
||||
SetAuthorization bool
|
||||
PassAuthorization bool
|
||||
PreferEmailToUser bool
|
||||
skipAuthRegex []string
|
||||
skipAuthPreflight bool
|
||||
skipAuthStripHeaders bool
|
||||
mainJwtBearerVerifier *oidc.IDTokenVerifier
|
||||
extraJwtBearerVerifiers []*oidc.IDTokenVerifier
|
||||
compiledRegex []*regexp.Regexp
|
||||
templates *template.Template
|
||||
realClientIPParser ipapi.RealClientIPParser
|
||||
|
||||
sessionChain alice.Chain
|
||||
|
||||
@ -285,19 +287,13 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
// SignOut sends a response to clear the authentication cookie
|
||||
func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
|
||||
redirect, err := p.GetRedirect(req)
|
||||
if err != nil {
|
||||
p.logger.Errorf("Error obtaining redirect: %v", err)
|
||||
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
|
||||
return
|
||||
}
|
||||
err = p.ClearSessionCookie(rw, req)
|
||||
err := p.ClearSessionCookie(rw, req)
|
||||
if err != nil {
|
||||
p.logger.Errorf("Error clearing session cookie: %v", err)
|
||||
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
|
||||
return
|
||||
}
|
||||
http.Redirect(rw, req, redirect, http.StatusFound)
|
||||
http.Redirect(rw, req, p.endSessionEndpoint, http.StatusFound)
|
||||
}
|
||||
|
||||
// AuthenticateOnly checks whether the user is currently logged in
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
const VERSION = "2021.6.2"
|
||||
const VERSION = "2021.6.4"
|
||||
|
||||
func BUILD() string {
|
||||
build := os.Getenv("GIT_BUILD_HASH")
|
||||
|
||||
292
schema.yml
292
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2021.6.1
|
||||
version: 2021.6.4
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@beryju.org
|
||||
@ -3096,7 +3096,11 @@ paths:
|
||||
$ref: '#/components/schemas/Link'
|
||||
description: ''
|
||||
'404':
|
||||
description: No recovery flow found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Link'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
@ -3572,6 +3576,37 @@ paths:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
post:
|
||||
operationId: events_events_create
|
||||
description: Event Read-Only Viewset
|
||||
tags:
|
||||
- events
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EventRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EventRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EventRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
- cookieAuth: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Event'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
/api/v2beta/events/events/{event_uuid}/:
|
||||
get:
|
||||
operationId: events_events_retrieve
|
||||
@ -3600,6 +3635,106 @@ paths:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
put:
|
||||
operationId: events_events_update
|
||||
description: Event Read-Only Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: event_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Event.
|
||||
required: true
|
||||
tags:
|
||||
- events
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EventRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EventRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EventRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
- cookieAuth: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Event'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
patch:
|
||||
operationId: events_events_partial_update
|
||||
description: Event Read-Only Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: event_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Event.
|
||||
required: true
|
||||
tags:
|
||||
- events
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedEventRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedEventRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedEventRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
- cookieAuth: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Event'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
delete:
|
||||
operationId: events_events_destroy
|
||||
description: Event Read-Only Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: event_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Event.
|
||||
required: true
|
||||
tags:
|
||||
- events
|
||||
security:
|
||||
- authentik: []
|
||||
- cookieAuth: []
|
||||
responses:
|
||||
'204':
|
||||
description: No response body
|
||||
'400':
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
/api/v2beta/events/events/actions/:
|
||||
get:
|
||||
operationId: events_events_actions_list
|
||||
@ -4441,6 +4576,18 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: invalid_response_action
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- restart
|
||||
- restart_with_context
|
||||
- retry
|
||||
description: Configure how the flow executor should handle an invalid response
|
||||
to a challenge. RETRY returns the error message and a similar challenge
|
||||
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
|
||||
restarts the flow while keeping the current context.
|
||||
- in: query
|
||||
name: order
|
||||
schema:
|
||||
@ -18494,7 +18641,10 @@ components:
|
||||
title: Kp uuid
|
||||
name:
|
||||
type: string
|
||||
fingerprint:
|
||||
fingerprint_sha256:
|
||||
type: string
|
||||
readOnly: true
|
||||
fingerprint_sha1:
|
||||
type: string
|
||||
readOnly: true
|
||||
cert_expiry:
|
||||
@ -18517,7 +18667,8 @@ components:
|
||||
- cert_expiry
|
||||
- cert_subject
|
||||
- certificate_download_url
|
||||
- fingerprint
|
||||
- fingerprint_sha1
|
||||
- fingerprint_sha256
|
||||
- name
|
||||
- pk
|
||||
- private_key_available
|
||||
@ -18759,6 +18910,12 @@ components:
|
||||
name: Documentation
|
||||
- href: https://goauthentik.io/
|
||||
name: authentik Website
|
||||
flow_authentication:
|
||||
type: string
|
||||
flow_invalidation:
|
||||
type: string
|
||||
flow_recovery:
|
||||
type: string
|
||||
flow_unenrollment:
|
||||
type: string
|
||||
required:
|
||||
@ -19242,7 +19399,7 @@ components:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
action:
|
||||
type: string
|
||||
$ref: '#/components/schemas/EventActions'
|
||||
app:
|
||||
type: string
|
||||
context:
|
||||
@ -19266,6 +19423,34 @@ components:
|
||||
- app
|
||||
- created
|
||||
- pk
|
||||
EventActions:
|
||||
enum:
|
||||
- login
|
||||
- login_failed
|
||||
- logout
|
||||
- user_write
|
||||
- suspicious_request
|
||||
- password_set
|
||||
- secret_view
|
||||
- invitation_used
|
||||
- authorize_application
|
||||
- source_linked
|
||||
- impersonation_started
|
||||
- impersonation_ended
|
||||
- policy_execution
|
||||
- policy_exception
|
||||
- property_mapping_exception
|
||||
- system_task_execution
|
||||
- system_task_exception
|
||||
- system_exception
|
||||
- configuration_error
|
||||
- model_created
|
||||
- model_updated
|
||||
- model_deleted
|
||||
- email_sent
|
||||
- update_available
|
||||
- custom_
|
||||
type: string
|
||||
EventMatcherPolicy:
|
||||
type: object
|
||||
description: Event Matcher Policy Serializer
|
||||
@ -19296,7 +19481,7 @@ components:
|
||||
readOnly: true
|
||||
action:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/EventMatcherPolicyActionEnum'
|
||||
- $ref: '#/components/schemas/EventActions'
|
||||
description: Match created events with this action type. When left empty,
|
||||
all action types will be matched.
|
||||
client_ip:
|
||||
@ -19314,34 +19499,6 @@ components:
|
||||
- pk
|
||||
- verbose_name
|
||||
- verbose_name_plural
|
||||
EventMatcherPolicyActionEnum:
|
||||
enum:
|
||||
- login
|
||||
- login_failed
|
||||
- logout
|
||||
- user_write
|
||||
- suspicious_request
|
||||
- password_set
|
||||
- secret_view
|
||||
- invitation_used
|
||||
- authorize_application
|
||||
- source_linked
|
||||
- impersonation_started
|
||||
- impersonation_ended
|
||||
- policy_execution
|
||||
- policy_exception
|
||||
- property_mapping_exception
|
||||
- system_task_execution
|
||||
- system_task_exception
|
||||
- system_exception
|
||||
- configuration_error
|
||||
- model_created
|
||||
- model_updated
|
||||
- model_deleted
|
||||
- email_sent
|
||||
- update_available
|
||||
- custom_
|
||||
type: string
|
||||
EventMatcherPolicyRequest:
|
||||
type: object
|
||||
description: Event Matcher Policy Serializer
|
||||
@ -19355,7 +19512,7 @@ components:
|
||||
will be logged. By default, only execution errors are logged.
|
||||
action:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/EventMatcherPolicyActionEnum'
|
||||
- $ref: '#/components/schemas/EventActions'
|
||||
description: Match created events with this action type. When left empty,
|
||||
all action types will be matched.
|
||||
client_ip:
|
||||
@ -19375,7 +19532,7 @@ components:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
action:
|
||||
type: string
|
||||
$ref: '#/components/schemas/EventActions'
|
||||
app:
|
||||
type: string
|
||||
context:
|
||||
@ -19673,6 +19830,13 @@ components:
|
||||
minimum: -2147483648
|
||||
policy_engine_mode:
|
||||
$ref: '#/components/schemas/PolicyEngineMode'
|
||||
invalid_response_action:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/InvalidResponseActionEnum'
|
||||
description: Configure how the flow executor should handle an invalid response
|
||||
to a challenge. RETRY returns the error message and a similar challenge
|
||||
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
|
||||
restarts the flow while keeping the current context.
|
||||
required:
|
||||
- order
|
||||
- pk
|
||||
@ -19703,6 +19867,13 @@ components:
|
||||
minimum: -2147483648
|
||||
policy_engine_mode:
|
||||
$ref: '#/components/schemas/PolicyEngineMode'
|
||||
invalid_response_action:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/InvalidResponseActionEnum'
|
||||
description: Configure how the flow executor should handle an invalid response
|
||||
to a challenge. RETRY returns the error message and a similar challenge
|
||||
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
|
||||
restarts the flow while keeping the current context.
|
||||
required:
|
||||
- order
|
||||
- stage
|
||||
@ -20048,6 +20219,12 @@ components:
|
||||
- api
|
||||
- recovery
|
||||
type: string
|
||||
InvalidResponseActionEnum:
|
||||
enum:
|
||||
- retry
|
||||
- restart
|
||||
- restart_with_context
|
||||
type: string
|
||||
Invitation:
|
||||
type: object
|
||||
description: Invitation Serializer
|
||||
@ -24429,7 +24606,7 @@ components:
|
||||
will be logged. By default, only execution errors are logged.
|
||||
action:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/EventMatcherPolicyActionEnum'
|
||||
- $ref: '#/components/schemas/EventActions'
|
||||
description: Match created events with this action type. When left empty,
|
||||
all action types will be matched.
|
||||
client_ip:
|
||||
@ -24441,6 +24618,29 @@ components:
|
||||
- $ref: '#/components/schemas/AppEnum'
|
||||
description: Match events created by selected application. When left empty,
|
||||
all applications are matched.
|
||||
PatchedEventRequest:
|
||||
type: object
|
||||
description: Event Serializer
|
||||
properties:
|
||||
user:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
action:
|
||||
$ref: '#/components/schemas/EventActions'
|
||||
app:
|
||||
type: string
|
||||
context:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
client_ip:
|
||||
type: string
|
||||
nullable: true
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
tenant:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
PatchedExpressionPolicyRequest:
|
||||
type: object
|
||||
description: Group Membership Policy Serializer
|
||||
@ -24502,6 +24702,13 @@ components:
|
||||
minimum: -2147483648
|
||||
policy_engine_mode:
|
||||
$ref: '#/components/schemas/PolicyEngineMode'
|
||||
invalid_response_action:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/InvalidResponseActionEnum'
|
||||
description: Configure how the flow executor should handle an invalid response
|
||||
to a challenge. RETRY returns the error message and a similar challenge
|
||||
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
|
||||
restarts the flow while keeping the current context.
|
||||
PatchedGroupRequest:
|
||||
type: object
|
||||
description: Group Serializer
|
||||
@ -25579,6 +25786,9 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowRequest'
|
||||
create_users_as_inactive:
|
||||
type: boolean
|
||||
description: When set, newly created users are inactive and cannot login.
|
||||
PatchedWebAuthnDeviceRequest:
|
||||
type: object
|
||||
description: Serializer for WebAuthn authenticator devices
|
||||
@ -26481,6 +26691,8 @@ components:
|
||||
id_token:
|
||||
type: string
|
||||
readOnly: true
|
||||
revoked:
|
||||
type: boolean
|
||||
required:
|
||||
- id_token
|
||||
- is_expired
|
||||
@ -28073,6 +28285,9 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Flow'
|
||||
create_users_as_inactive:
|
||||
type: boolean
|
||||
description: When set, newly created users are inactive and cannot login.
|
||||
required:
|
||||
- component
|
||||
- name
|
||||
@ -28089,6 +28304,9 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowRequest'
|
||||
create_users_as_inactive:
|
||||
type: boolean
|
||||
description: When set, newly created users are inactive and cannot login.
|
||||
required:
|
||||
- name
|
||||
ValidationError:
|
||||
|
||||
232
tests/e2e/test_provider_ldap.py
Normal file
232
tests/e2e/test_provider_ldap.py
Normal file
@ -0,0 +1,232 @@
|
||||
"""LDAP and Outpost e2e tests"""
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from docker.client import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from ldap3 import (
|
||||
ALL,
|
||||
ALL_ATTRIBUTES,
|
||||
ALL_OPERATIONAL_ATTRIBUTES,
|
||||
SUBTREE,
|
||||
Connection,
|
||||
Server,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPInsufficientAccessRightsResult
|
||||
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.outposts.models import Outpost, OutpostType
|
||||
from authentik.providers.ldap.models import LDAPProvider
|
||||
from tests.e2e.utils import (
|
||||
USER,
|
||||
SeleniumTestCase,
|
||||
apply_migration,
|
||||
object_manager,
|
||||
retry,
|
||||
)
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderLDAP(SeleniumTestCase):
|
||||
"""LDAP and Outpost e2e tests"""
|
||||
|
||||
ldap_container: Container
|
||||
|
||||
def tearDown(self) -> None:
|
||||
super().tearDown()
|
||||
self.output_container_logs(self.ldap_container)
|
||||
self.ldap_container.kill()
|
||||
|
||||
def start_ldap(self, outpost: Outpost) -> Container:
|
||||
"""Start ldap container based on outpost created"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="beryju.org/authentik/outpost-ldap:gh-master",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
environment={
|
||||
"AUTHENTIK_HOST": self.live_server_url,
|
||||
"AUTHENTIK_TOKEN": outpost.token.key,
|
||||
},
|
||||
)
|
||||
return container
|
||||
|
||||
def _prepare(self) -> User:
|
||||
"""prepare user, provider, app and container"""
|
||||
# set additionalHeaders to test later
|
||||
user = USER()
|
||||
user.attributes["extraAttribute"] = "bar"
|
||||
user.save()
|
||||
|
||||
ldap: LDAPProvider = LDAPProvider.objects.create(
|
||||
name="ldap_provider",
|
||||
authorization_flow=Flow.objects.get(slug="default-authentication-flow"),
|
||||
search_group=Group.objects.first(),
|
||||
)
|
||||
# we need to create an application to actually access the ldap
|
||||
Application.objects.create(name="ldap", slug="ldap", provider=ldap)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name="ldap_outpost",
|
||||
type=OutpostType.LDAP,
|
||||
)
|
||||
outpost.providers.add(ldap)
|
||||
outpost.save()
|
||||
user = outpost.user
|
||||
|
||||
self.ldap_container = self.start_ldap(outpost)
|
||||
|
||||
# Wait until outpost healthcheck succeeds
|
||||
healthcheck_retries = 0
|
||||
while healthcheck_retries < 50:
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen:
|
||||
break
|
||||
healthcheck_retries += 1
|
||||
sleep(0.5)
|
||||
return user
|
||||
|
||||
@retry()
|
||||
@apply_migration("authentik_core", "0003_default_user")
|
||||
@apply_migration("authentik_flows", "0008_default_flows")
|
||||
@object_manager
|
||||
def test_ldap_bind_success(self):
|
||||
"""Test simple bind"""
|
||||
self._prepare()
|
||||
server = Server("ldap://localhost:3389", get_info=ALL)
|
||||
_connection = Connection(
|
||||
server,
|
||||
raise_exceptions=True,
|
||||
user=f"cn={USER().username},ou=users,DC=ldap,DC=goauthentik,DC=io",
|
||||
password=USER().username,
|
||||
)
|
||||
_connection.bind()
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user={
|
||||
"pk": USER().pk,
|
||||
"email": USER().email,
|
||||
"username": USER().username,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_migration("authentik_core", "0003_default_user")
|
||||
@apply_migration("authentik_flows", "0008_default_flows")
|
||||
@object_manager
|
||||
def test_ldap_bind_fail(self):
|
||||
"""Test simple bind (failed)"""
|
||||
self._prepare()
|
||||
server = Server("ldap://localhost:3389", get_info=ALL)
|
||||
_connection = Connection(
|
||||
server,
|
||||
raise_exceptions=True,
|
||||
user=f"cn={USER().username},ou=users,DC=ldap,DC=goauthentik,DC=io",
|
||||
password=USER().username + "fqwerwqer",
|
||||
)
|
||||
with self.assertRaises(LDAPInsufficientAccessRightsResult):
|
||||
_connection.bind()
|
||||
anon = get_anonymous_user()
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN_FAILED,
|
||||
user={"pk": anon.pk, "email": anon.email, "username": anon.username},
|
||||
)
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_migration("authentik_core", "0003_default_user")
|
||||
@apply_migration("authentik_core", "0009_group_is_superuser")
|
||||
@apply_migration("authentik_flows", "0008_default_flows")
|
||||
@object_manager
|
||||
def test_ldap_bind_search(self):
|
||||
"""Test simple bind + search"""
|
||||
outpost_user = self._prepare()
|
||||
server = Server("ldap://localhost:3389", get_info=ALL)
|
||||
_connection = Connection(
|
||||
server,
|
||||
raise_exceptions=True,
|
||||
user=f"cn={USER().username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
password=USER().username,
|
||||
)
|
||||
_connection.bind()
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user={
|
||||
"pk": USER().pk,
|
||||
"email": USER().email,
|
||||
"username": USER().username,
|
||||
},
|
||||
)
|
||||
)
|
||||
_connection.search(
|
||||
"ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
"(objectClass=user)",
|
||||
search_scope=SUBTREE,
|
||||
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
|
||||
)
|
||||
response = _connection.response
|
||||
# Remove raw_attributes to make checking easier
|
||||
for obj in response:
|
||||
del obj["raw_attributes"]
|
||||
del obj["raw_dn"]
|
||||
self.assertCountEqual(
|
||||
response,
|
||||
[
|
||||
{
|
||||
"dn": f"cn={outpost_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
"attributes": {
|
||||
"cn": [outpost_user.username],
|
||||
"uid": [outpost_user.uid],
|
||||
"name": [""],
|
||||
"displayName": [""],
|
||||
"mail": [""],
|
||||
"objectClass": [
|
||||
"user",
|
||||
"organizationalPerson",
|
||||
"goauthentik.io/ldap/user",
|
||||
],
|
||||
"memberOf": [],
|
||||
"accountStatus": ["true"],
|
||||
"superuser": ["false"],
|
||||
"goauthentik.io/ldap/active": ["true"],
|
||||
"goauthentik.io/ldap/superuser": ["false"],
|
||||
"goauthentik.io/user/override-ips": ["true"],
|
||||
"goauthentik.io/user/service-account": ["true"],
|
||||
},
|
||||
"type": "searchResEntry",
|
||||
},
|
||||
{
|
||||
"dn": f"cn={USER().username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
"attributes": {
|
||||
"cn": [USER().username],
|
||||
"uid": [USER().uid],
|
||||
"name": [USER().name],
|
||||
"displayName": [USER().name],
|
||||
"mail": [USER().email],
|
||||
"objectClass": [
|
||||
"user",
|
||||
"organizationalPerson",
|
||||
"goauthentik.io/ldap/user",
|
||||
],
|
||||
"memberOf": [
|
||||
"cn=authentik Admins,ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
||||
],
|
||||
"accountStatus": ["true"],
|
||||
"superuser": ["true"],
|
||||
"goauthentik.io/ldap/active": ["true"],
|
||||
"goauthentik.io/ldap/superuser": ["true"],
|
||||
"extraAttribute": ["bar"],
|
||||
},
|
||||
"type": "searchResEntry",
|
||||
},
|
||||
],
|
||||
)
|
||||
@ -119,6 +119,13 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
self.assertIn("X-Forwarded-Preferred-Username: akadmin", full_body_text)
|
||||
self.assertIn("X-Foo: bar", full_body_text)
|
||||
|
||||
self.driver.get("http://localhost:4180/akprox/sign_out")
|
||||
sleep(2)
|
||||
full_body_text = self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl"
|
||||
).text
|
||||
self.assertIn("You've logged out of proxy.", full_body_text)
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||
|
||||
@ -62,11 +62,14 @@ class OutpostDockerTests(TestCase):
|
||||
)
|
||||
authentication_kp = CertificateKeyPair.objects.create(
|
||||
name="docker-authentication",
|
||||
# pylint: disable=consider-using-with
|
||||
certificate_data=open(f"{self.ssl_folder}/client/cert.pem").read(),
|
||||
# pylint: disable=consider-using-with
|
||||
key_data=open(f"{self.ssl_folder}/client/key.pem").read(),
|
||||
)
|
||||
verification_kp = CertificateKeyPair.objects.create(
|
||||
name="docker-verification",
|
||||
# pylint: disable=consider-using-with
|
||||
certificate_data=open(f"{self.ssl_folder}/client/ca.pem").read(),
|
||||
)
|
||||
self.service_connection = DockerServiceConnection.objects.create(
|
||||
|
||||
@ -62,11 +62,14 @@ class TestProxyDocker(TestCase):
|
||||
)
|
||||
authentication_kp = CertificateKeyPair.objects.create(
|
||||
name="docker-authentication",
|
||||
# pylint: disable=consider-using-with
|
||||
certificate_data=open(f"{self.ssl_folder}/client/cert.pem").read(),
|
||||
# pylint: disable=consider-using-with
|
||||
key_data=open(f"{self.ssl_folder}/client/key.pem").read(),
|
||||
)
|
||||
verification_kp = CertificateKeyPair.objects.create(
|
||||
name="docker-verification",
|
||||
# pylint: disable=consider-using-with
|
||||
certificate_data=open(f"{self.ssl_folder}/client/ca.pem").read(),
|
||||
)
|
||||
self.service_connection = DockerServiceConnection.objects.create(
|
||||
|
||||
436
web/package-lock.json
generated
436
web/package-lock.json
generated
@ -18,28 +18,28 @@
|
||||
"@lingui/cli": "^3.10.2",
|
||||
"@lingui/core": "^3.10.4",
|
||||
"@lingui/macro": "^3.10.2",
|
||||
"@patternfly/patternfly": "^4.108.2",
|
||||
"@patternfly/patternfly": "^4.115.2",
|
||||
"@polymer/iron-form": "^3.0.1",
|
||||
"@polymer/paper-input": "^3.2.1",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@sentry/browser": "^6.7.2",
|
||||
"@sentry/tracing": "^6.7.2",
|
||||
"@types/chart.js": "^2.9.32",
|
||||
"@types/codemirror": "5.60.0",
|
||||
"@sentry/browser": "^6.8.0",
|
||||
"@sentry/tracing": "^6.8.0",
|
||||
"@types/chart.js": "^2.9.33",
|
||||
"@types/codemirror": "5.60.1",
|
||||
"@types/grecaptcha": "^3.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||
"@typescript-eslint/parser": "^4.28.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
||||
"@typescript-eslint/parser": "^4.28.1",
|
||||
"@webcomponents/webcomponentsjs": "^2.5.0",
|
||||
"authentik-api": "file:api",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^3.3.2",
|
||||
"chart.js": "^3.4.1",
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"codemirror": "^5.62.0",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint": "^7.30.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.2",
|
||||
"eslint-plugin-lit": "^1.5.1",
|
||||
@ -48,7 +48,7 @@
|
||||
"lit-html": "^1.4.1",
|
||||
"moment": "^2.29.1",
|
||||
"rapidoc": "^9.0.0",
|
||||
"rollup": "^2.52.2",
|
||||
"rollup": "^2.52.7",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-cssimport": "^1.0.2",
|
||||
@ -58,15 +58,16 @@
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"ts-lit-plugin": "^1.2.1",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "^4.3.4",
|
||||
"typescript": "^4.3.5",
|
||||
"webcomponent-qr-code": "^1.0.5",
|
||||
"yaml": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"devDependencies": {}
|
||||
},
|
||||
"api": {
|
||||
"name": "authentik-api",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^3.6"
|
||||
}
|
||||
},
|
||||
@ -74,6 +75,7 @@
|
||||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -1739,6 +1741,24 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||
"integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
|
||||
"dependencies": {
|
||||
"@humanwhocodes/object-schema": "^1.2.0",
|
||||
"debug": "^4.1.1",
|
||||
"minimatch": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/object-schema": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz",
|
||||
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w=="
|
||||
},
|
||||
"node_modules/@jest/types": {
|
||||
"version": "26.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
|
||||
@ -2120,9 +2140,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@patternfly/patternfly": {
|
||||
"version": "4.108.2",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.108.2.tgz",
|
||||
"integrity": "sha512-z0VB+1CXcH+eoClYQABwapX5FURSvm1nPr6asLWwg/Z4Wuxs0RjZpC6Gb+KRm8nGQwSAcMKZY1jLfPqVnznQnw=="
|
||||
"version": "4.115.2",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.115.2.tgz",
|
||||
"integrity": "sha512-7hbJ4pRmj+rlXclD2F/UwceO6fS+9flGsgHc4eUc7NyTN2GXl6PLcqrjE2CtiKEPV90+KwsGQGJXZj8bz9HweA=="
|
||||
},
|
||||
"node_modules/@polymer/font-roboto": {
|
||||
"version": "3.0.2",
|
||||
@ -2314,13 +2334,13 @@
|
||||
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.7.2.tgz",
|
||||
"integrity": "sha512-Lv0Ne1QcesyGAhVcQDfQa3hDPR/MhPSDTMg3xFi+LxqztchVc4w/ynzR0wCZFb8KIHpTj5SpJHfxpDhXYMtS9g==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.8.0.tgz",
|
||||
"integrity": "sha512-nxa71csHlG5sMHUxI4e4xxuCWtbCv/QbBfMsYw7ncJSfCKG3yNlCVh8NJ7NS0rZW/MJUT6S6+r93zw0HetNDOA==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "6.7.2",
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/utils": "6.7.2",
|
||||
"@sentry/core": "6.8.0",
|
||||
"@sentry/types": "6.8.0",
|
||||
"@sentry/utils": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2333,14 +2353,14 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.7.2.tgz",
|
||||
"integrity": "sha512-NTZqwN5nR94yrXmSfekoPs1mIFuKvf8esdIW/DadwSKWAdLJwQTJY9xK/8PQv+SEzd7wiitPAx+mCw2By1xiNQ==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.8.0.tgz",
|
||||
"integrity": "sha512-vJzWt/znEB+JqVwtwfjkRrAYRN+ep+l070Ti8GhJnvwU4IDtVlV3T/jVNrj6rl6UChcczaJQMxVxtG5x0crlAA==",
|
||||
"dependencies": {
|
||||
"@sentry/hub": "6.7.2",
|
||||
"@sentry/minimal": "6.7.2",
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/utils": "6.7.2",
|
||||
"@sentry/hub": "6.8.0",
|
||||
"@sentry/minimal": "6.8.0",
|
||||
"@sentry/types": "6.8.0",
|
||||
"@sentry/utils": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2353,12 +2373,12 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/hub": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.7.2.tgz",
|
||||
"integrity": "sha512-05qVW6ymChJsXag4+fYCQokW3AcABIgcqrVYZUBf6GMU/Gbz5SJqpV7y1+njwWvnPZydMncP9LaDVpMKbE7UYQ==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.8.0.tgz",
|
||||
"integrity": "sha512-hFrI2Ss1fTov7CH64FJpigqRxH7YvSnGeqxT9Jc1BL7nzW/vgCK+Oh2mOZbosTcrzoDv+lE8ViOnSN3w/fo+rg==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/utils": "6.7.2",
|
||||
"@sentry/types": "6.8.0",
|
||||
"@sentry/utils": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2371,12 +2391,12 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/minimal": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.7.2.tgz",
|
||||
"integrity": "sha512-jkpwFv2GFHoVl5vnK+9/Q+Ea8eVdbJ3hn3/Dqq9MOLFnVK7ED6MhdHKLT79puGSFj+85OuhM5m2Q44mIhyS5mw==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.8.0.tgz",
|
||||
"integrity": "sha512-MRxUKXiiYwKjp8mOQMpTpEuIby1Jh3zRTU0cmGZtfsZ38BC1JOle8xlwC4FdtOH+VvjSYnPBMya5lgNHNPUJDQ==",
|
||||
"dependencies": {
|
||||
"@sentry/hub": "6.7.2",
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/hub": "6.8.0",
|
||||
"@sentry/types": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2389,14 +2409,14 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/tracing": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.7.2.tgz",
|
||||
"integrity": "sha512-juKlI7FICKONWJFJxDxerj0A+8mNRhmtrdR+OXFqOkqSAy/QXlSFZcA/j//O19k2CfwK1BrvoMcQ/4gnffUOVg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.8.0.tgz",
|
||||
"integrity": "sha512-3gDkQnmOuOjHz5rY7BOatLEUksANU3efR8wuBa2ujsPQvoLSLFuyZpRjPPsxuUHQOqAYIbSNAoDloXECvQeHjw==",
|
||||
"dependencies": {
|
||||
"@sentry/hub": "6.7.2",
|
||||
"@sentry/minimal": "6.7.2",
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/utils": "6.7.2",
|
||||
"@sentry/hub": "6.8.0",
|
||||
"@sentry/minimal": "6.8.0",
|
||||
"@sentry/types": "6.8.0",
|
||||
"@sentry/utils": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2409,19 +2429,19 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.7.2.tgz",
|
||||
"integrity": "sha512-h21Go/PfstUN+ZV6SbwRSZVg9GXRJWdLfHoO5PSVb3TVEMckuxk8tAE57/u+UZDwX8wu+Xyon2TgsKpiWKxqUg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.8.0.tgz",
|
||||
"integrity": "sha512-PbSxqlh6Fd5thNU5f8EVYBVvX+G7XdPA+ThNb2QvSK8yv3rIf0McHTyF6sIebgJ38OYN7ZFK7vvhC/RgSAfYTA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/utils": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.7.2.tgz",
|
||||
"integrity": "sha512-9COL7aaBbe61Hp5BlArtXZ1o/cxli1NGONLPrVT4fMyeQFmLonhUiy77NdsW19XnvhvaA+2IoV5dg3dnFiF/og==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.8.0.tgz",
|
||||
"integrity": "sha512-OYlI2JNrcWKMdvYbWNdQwR4QBVv2V0y5wK0U6f53nArv6RsyO5TzwRu5rMVSIZofUUqjoE5hl27jqnR+vpUrsA==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/types": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2434,9 +2454,9 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@types/chart.js": {
|
||||
"version": "2.9.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz",
|
||||
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==",
|
||||
"version": "2.9.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz",
|
||||
"integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==",
|
||||
"dependencies": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
@ -2451,9 +2471,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.0.tgz",
|
||||
"integrity": "sha512-xgzXZyCzedLRNC67/Nn8rpBtTFnAsX2C+Q/LGoH6zgcpF/LqdNHJMHEOhqT1bwUcSp6kQdOIuKzRbeW9DYhEhg==",
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.1.tgz",
|
||||
"integrity": "sha512-yV14LQ5VvghnW0uSuCw2bEfZC6NvxHQEckl2w3dEk5l0yPGzQh14dCaWvG5KD/2l3cgFSifR+6nIUD7LDLdUTg==",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
@ -2579,12 +2599,12 @@
|
||||
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz",
|
||||
"integrity": "sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz",
|
||||
"integrity": "sha512-9yfcNpDaNGQ6/LQOX/KhUFTR1sCKH+PBr234k6hI9XJ0VP5UqGxap0AnNwBnWFk1MNyWBylJH9ZkzBXC+5akZQ==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/experimental-utils": "4.28.0",
|
||||
"@typescript-eslint/scope-manager": "4.28.0",
|
||||
"@typescript-eslint/experimental-utils": "4.28.1",
|
||||
"@typescript-eslint/scope-manager": "4.28.1",
|
||||
"debug": "^4.3.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"regexpp": "^3.1.0",
|
||||
@ -2609,14 +2629,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/experimental-utils": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz",
|
||||
"integrity": "sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.1.tgz",
|
||||
"integrity": "sha512-n8/ggadrZ+uyrfrSEchx3jgODdmcx7MzVM2sI3cTpI/YlfSm0+9HEUaWw3aQn2urL2KYlWYMDgn45iLfjDYB+Q==",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.7",
|
||||
"@typescript-eslint/scope-manager": "4.28.0",
|
||||
"@typescript-eslint/types": "4.28.0",
|
||||
"@typescript-eslint/typescript-estree": "4.28.0",
|
||||
"@typescript-eslint/scope-manager": "4.28.1",
|
||||
"@typescript-eslint/types": "4.28.1",
|
||||
"@typescript-eslint/typescript-estree": "4.28.1",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0"
|
||||
},
|
||||
@ -2649,13 +2669,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.0.tgz",
|
||||
"integrity": "sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.1.tgz",
|
||||
"integrity": "sha512-UjrMsgnhQIIK82hXGaD+MCN8IfORS1CbMdu7VlZbYa8LCZtbZjJA26De4IPQB7XYZbL8gJ99KWNj0l6WD0guJg==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "4.28.0",
|
||||
"@typescript-eslint/types": "4.28.0",
|
||||
"@typescript-eslint/typescript-estree": "4.28.0",
|
||||
"@typescript-eslint/scope-manager": "4.28.1",
|
||||
"@typescript-eslint/types": "4.28.1",
|
||||
"@typescript-eslint/typescript-estree": "4.28.1",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -2675,12 +2695,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz",
|
||||
"integrity": "sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.1.tgz",
|
||||
"integrity": "sha512-o95bvGKfss6705x7jFGDyS7trAORTy57lwJ+VsYwil/lOUxKQ9tA7Suuq+ciMhJc/1qPwB3XE2DKh9wubW8YYA==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "4.28.0",
|
||||
"@typescript-eslint/visitor-keys": "4.28.0"
|
||||
"@typescript-eslint/types": "4.28.1",
|
||||
"@typescript-eslint/visitor-keys": "4.28.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
|
||||
@ -2691,9 +2711,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.0.tgz",
|
||||
"integrity": "sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.1.tgz",
|
||||
"integrity": "sha512-4z+knEihcyX7blAGi7O3Fm3O6YRCP+r56NJFMNGsmtdw+NCdpG5SgNz427LS9nQkRVTswZLhz484hakQwB8RRg==",
|
||||
"engines": {
|
||||
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
|
||||
},
|
||||
@ -2703,12 +2723,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz",
|
||||
"integrity": "sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.1.tgz",
|
||||
"integrity": "sha512-GhKxmC4sHXxHGJv8e8egAZeTZ6HI4mLU6S7FUzvFOtsk7ZIDN1ksA9r9DyOgNqowA9yAtZXV0Uiap61bIO81FQ==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "4.28.0",
|
||||
"@typescript-eslint/visitor-keys": "4.28.0",
|
||||
"@typescript-eslint/types": "4.28.1",
|
||||
"@typescript-eslint/visitor-keys": "4.28.1",
|
||||
"debug": "^4.3.1",
|
||||
"globby": "^11.0.3",
|
||||
"is-glob": "^4.0.1",
|
||||
@ -2748,11 +2768,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz",
|
||||
"integrity": "sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.1.tgz",
|
||||
"integrity": "sha512-K4HMrdFqr9PFquPu178SaSb92CaWe2yErXyPumc8cYWxFmhgJsNY9eSePmO05j0JhBvf2Cdhptd6E6Yv9HVHcg==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "4.28.0",
|
||||
"@typescript-eslint/types": "4.28.1",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -3316,9 +3336,9 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.3.2.tgz",
|
||||
"integrity": "sha512-H0hSO7xqTIrwxoACqnSoNromEMfXvfuVnrbuSt2TuXfBDDofbnto4zuZlRtRvC73/b37q3wGAWZyUU41QPvNbA=="
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
|
||||
"integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
|
||||
},
|
||||
"node_modules/chartjs-adapter-moment": {
|
||||
"version": "1.0.0",
|
||||
@ -3861,12 +3881,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz",
|
||||
"integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==",
|
||||
"version": "7.30.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
|
||||
"integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.12.11",
|
||||
"@eslint/eslintrc": "^0.4.2",
|
||||
"@humanwhocodes/config-array": "^0.5.0",
|
||||
"ajv": "^6.10.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
@ -6770,9 +6791,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "2.52.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.2.tgz",
|
||||
"integrity": "sha512-4RlFC3k2BIHlUsJ9mGd8OO+9Lm2eDF5P7+6DNQOp5sx+7N/1tFM01kELfbxlMX3MxT6owvLB1ln4S3QvvQlbUA==",
|
||||
"version": "2.52.7",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.7.tgz",
|
||||
"integrity": "sha512-55cSH4CCU6MaPr9TAOyrIC+7qFCHscL7tkNsm1MBfIJRRqRbCEY0mmeFn4Wg8FKsHtEH8r389Fz38r/o+kgXLg==",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
@ -7604,9 +7625,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
|
||||
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==",
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
||||
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -9191,6 +9212,21 @@
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
|
||||
"integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w=="
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||
"integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
|
||||
"requires": {
|
||||
"@humanwhocodes/object-schema": "^1.2.0",
|
||||
"debug": "^4.1.1",
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"@humanwhocodes/object-schema": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz",
|
||||
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w=="
|
||||
},
|
||||
"@jest/types": {
|
||||
"version": "26.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
|
||||
@ -9482,9 +9518,9 @@
|
||||
}
|
||||
},
|
||||
"@patternfly/patternfly": {
|
||||
"version": "4.108.2",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.108.2.tgz",
|
||||
"integrity": "sha512-z0VB+1CXcH+eoClYQABwapX5FURSvm1nPr6asLWwg/Z4Wuxs0RjZpC6Gb+KRm8nGQwSAcMKZY1jLfPqVnznQnw=="
|
||||
"version": "4.115.2",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.115.2.tgz",
|
||||
"integrity": "sha512-7hbJ4pRmj+rlXclD2F/UwceO6fS+9flGsgHc4eUc7NyTN2GXl6PLcqrjE2CtiKEPV90+KwsGQGJXZj8bz9HweA=="
|
||||
},
|
||||
"@polymer/font-roboto": {
|
||||
"version": "3.0.2",
|
||||
@ -9669,13 +9705,13 @@
|
||||
}
|
||||
},
|
||||
"@sentry/browser": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.7.2.tgz",
|
||||
"integrity": "sha512-Lv0Ne1QcesyGAhVcQDfQa3hDPR/MhPSDTMg3xFi+LxqztchVc4w/ynzR0wCZFb8KIHpTj5SpJHfxpDhXYMtS9g==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.8.0.tgz",
|
||||
"integrity": "sha512-nxa71csHlG5sMHUxI4e4xxuCWtbCv/QbBfMsYw7ncJSfCKG3yNlCVh8NJ7NS0rZW/MJUT6S6+r93zw0HetNDOA==",
|
||||
"requires": {
|
||||
"@sentry/core": "6.7.2",
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/utils": "6.7.2",
|
||||
"@sentry/core": "6.8.0",
|
||||
"@sentry/types": "6.8.0",
|
||||
"@sentry/utils": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9687,14 +9723,14 @@
|
||||
}
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.7.2.tgz",
|
||||
"integrity": "sha512-NTZqwN5nR94yrXmSfekoPs1mIFuKvf8esdIW/DadwSKWAdLJwQTJY9xK/8PQv+SEzd7wiitPAx+mCw2By1xiNQ==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.8.0.tgz",
|
||||
"integrity": "sha512-vJzWt/znEB+JqVwtwfjkRrAYRN+ep+l070Ti8GhJnvwU4IDtVlV3T/jVNrj6rl6UChcczaJQMxVxtG5x0crlAA==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.7.2",
|
||||
"@sentry/minimal": "6.7.2",
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/utils": "6.7.2",
|
||||
"@sentry/hub": "6.8.0",
|
||||
"@sentry/minimal": "6.8.0",
|
||||
"@sentry/types": "6.8.0",
|
||||
"@sentry/utils": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9706,12 +9742,12 @@
|
||||
}
|
||||
},
|
||||
"@sentry/hub": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.7.2.tgz",
|
||||
"integrity": "sha512-05qVW6ymChJsXag4+fYCQokW3AcABIgcqrVYZUBf6GMU/Gbz5SJqpV7y1+njwWvnPZydMncP9LaDVpMKbE7UYQ==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.8.0.tgz",
|
||||
"integrity": "sha512-hFrI2Ss1fTov7CH64FJpigqRxH7YvSnGeqxT9Jc1BL7nzW/vgCK+Oh2mOZbosTcrzoDv+lE8ViOnSN3w/fo+rg==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/utils": "6.7.2",
|
||||
"@sentry/types": "6.8.0",
|
||||
"@sentry/utils": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9723,12 +9759,12 @@
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.7.2.tgz",
|
||||
"integrity": "sha512-jkpwFv2GFHoVl5vnK+9/Q+Ea8eVdbJ3hn3/Dqq9MOLFnVK7ED6MhdHKLT79puGSFj+85OuhM5m2Q44mIhyS5mw==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.8.0.tgz",
|
||||
"integrity": "sha512-MRxUKXiiYwKjp8mOQMpTpEuIby1Jh3zRTU0cmGZtfsZ38BC1JOle8xlwC4FdtOH+VvjSYnPBMya5lgNHNPUJDQ==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.7.2",
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/hub": "6.8.0",
|
||||
"@sentry/types": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9740,14 +9776,14 @@
|
||||
}
|
||||
},
|
||||
"@sentry/tracing": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.7.2.tgz",
|
||||
"integrity": "sha512-juKlI7FICKONWJFJxDxerj0A+8mNRhmtrdR+OXFqOkqSAy/QXlSFZcA/j//O19k2CfwK1BrvoMcQ/4gnffUOVg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.8.0.tgz",
|
||||
"integrity": "sha512-3gDkQnmOuOjHz5rY7BOatLEUksANU3efR8wuBa2ujsPQvoLSLFuyZpRjPPsxuUHQOqAYIbSNAoDloXECvQeHjw==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.7.2",
|
||||
"@sentry/minimal": "6.7.2",
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/utils": "6.7.2",
|
||||
"@sentry/hub": "6.8.0",
|
||||
"@sentry/minimal": "6.8.0",
|
||||
"@sentry/types": "6.8.0",
|
||||
"@sentry/utils": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9759,16 +9795,16 @@
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.7.2.tgz",
|
||||
"integrity": "sha512-h21Go/PfstUN+ZV6SbwRSZVg9GXRJWdLfHoO5PSVb3TVEMckuxk8tAE57/u+UZDwX8wu+Xyon2TgsKpiWKxqUg=="
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.8.0.tgz",
|
||||
"integrity": "sha512-PbSxqlh6Fd5thNU5f8EVYBVvX+G7XdPA+ThNb2QvSK8yv3rIf0McHTyF6sIebgJ38OYN7ZFK7vvhC/RgSAfYTA=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.7.2.tgz",
|
||||
"integrity": "sha512-9COL7aaBbe61Hp5BlArtXZ1o/cxli1NGONLPrVT4fMyeQFmLonhUiy77NdsW19XnvhvaA+2IoV5dg3dnFiF/og==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.8.0.tgz",
|
||||
"integrity": "sha512-OYlI2JNrcWKMdvYbWNdQwR4QBVv2V0y5wK0U6f53nArv6RsyO5TzwRu5rMVSIZofUUqjoE5hl27jqnR+vpUrsA==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.7.2",
|
||||
"@sentry/types": "6.8.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9780,9 +9816,9 @@
|
||||
}
|
||||
},
|
||||
"@types/chart.js": {
|
||||
"version": "2.9.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz",
|
||||
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==",
|
||||
"version": "2.9.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz",
|
||||
"integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==",
|
||||
"requires": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
@ -9797,9 +9833,9 @@
|
||||
}
|
||||
},
|
||||
"@types/codemirror": {
|
||||
"version": "5.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.0.tgz",
|
||||
"integrity": "sha512-xgzXZyCzedLRNC67/Nn8rpBtTFnAsX2C+Q/LGoH6zgcpF/LqdNHJMHEOhqT1bwUcSp6kQdOIuKzRbeW9DYhEhg==",
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.1.tgz",
|
||||
"integrity": "sha512-yV14LQ5VvghnW0uSuCw2bEfZC6NvxHQEckl2w3dEk5l0yPGzQh14dCaWvG5KD/2l3cgFSifR+6nIUD7LDLdUTg==",
|
||||
"requires": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
@ -9925,12 +9961,12 @@
|
||||
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA=="
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz",
|
||||
"integrity": "sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz",
|
||||
"integrity": "sha512-9yfcNpDaNGQ6/LQOX/KhUFTR1sCKH+PBr234k6hI9XJ0VP5UqGxap0AnNwBnWFk1MNyWBylJH9ZkzBXC+5akZQ==",
|
||||
"requires": {
|
||||
"@typescript-eslint/experimental-utils": "4.28.0",
|
||||
"@typescript-eslint/scope-manager": "4.28.0",
|
||||
"@typescript-eslint/experimental-utils": "4.28.1",
|
||||
"@typescript-eslint/scope-manager": "4.28.1",
|
||||
"debug": "^4.3.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"regexpp": "^3.1.0",
|
||||
@ -9939,14 +9975,14 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz",
|
||||
"integrity": "sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.1.tgz",
|
||||
"integrity": "sha512-n8/ggadrZ+uyrfrSEchx3jgODdmcx7MzVM2sI3cTpI/YlfSm0+9HEUaWw3aQn2urL2KYlWYMDgn45iLfjDYB+Q==",
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.7",
|
||||
"@typescript-eslint/scope-manager": "4.28.0",
|
||||
"@typescript-eslint/types": "4.28.0",
|
||||
"@typescript-eslint/typescript-estree": "4.28.0",
|
||||
"@typescript-eslint/scope-manager": "4.28.1",
|
||||
"@typescript-eslint/types": "4.28.1",
|
||||
"@typescript-eslint/typescript-estree": "4.28.1",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0"
|
||||
},
|
||||
@ -9962,37 +9998,37 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.0.tgz",
|
||||
"integrity": "sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.1.tgz",
|
||||
"integrity": "sha512-UjrMsgnhQIIK82hXGaD+MCN8IfORS1CbMdu7VlZbYa8LCZtbZjJA26De4IPQB7XYZbL8gJ99KWNj0l6WD0guJg==",
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "4.28.0",
|
||||
"@typescript-eslint/types": "4.28.0",
|
||||
"@typescript-eslint/typescript-estree": "4.28.0",
|
||||
"@typescript-eslint/scope-manager": "4.28.1",
|
||||
"@typescript-eslint/types": "4.28.1",
|
||||
"@typescript-eslint/typescript-estree": "4.28.1",
|
||||
"debug": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz",
|
||||
"integrity": "sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.1.tgz",
|
||||
"integrity": "sha512-o95bvGKfss6705x7jFGDyS7trAORTy57lwJ+VsYwil/lOUxKQ9tA7Suuq+ciMhJc/1qPwB3XE2DKh9wubW8YYA==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.28.0",
|
||||
"@typescript-eslint/visitor-keys": "4.28.0"
|
||||
"@typescript-eslint/types": "4.28.1",
|
||||
"@typescript-eslint/visitor-keys": "4.28.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.0.tgz",
|
||||
"integrity": "sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA=="
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.1.tgz",
|
||||
"integrity": "sha512-4z+knEihcyX7blAGi7O3Fm3O6YRCP+r56NJFMNGsmtdw+NCdpG5SgNz427LS9nQkRVTswZLhz484hakQwB8RRg=="
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz",
|
||||
"integrity": "sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.1.tgz",
|
||||
"integrity": "sha512-GhKxmC4sHXxHGJv8e8egAZeTZ6HI4mLU6S7FUzvFOtsk7ZIDN1ksA9r9DyOgNqowA9yAtZXV0Uiap61bIO81FQ==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.28.0",
|
||||
"@typescript-eslint/visitor-keys": "4.28.0",
|
||||
"@typescript-eslint/types": "4.28.1",
|
||||
"@typescript-eslint/visitor-keys": "4.28.1",
|
||||
"debug": "^4.3.1",
|
||||
"globby": "^11.0.3",
|
||||
"is-glob": "^4.0.1",
|
||||
@ -10016,11 +10052,11 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz",
|
||||
"integrity": "sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.1.tgz",
|
||||
"integrity": "sha512-K4HMrdFqr9PFquPu178SaSb92CaWe2yErXyPumc8cYWxFmhgJsNY9eSePmO05j0JhBvf2Cdhptd6E6Yv9HVHcg==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.28.0",
|
||||
"@typescript-eslint/types": "4.28.1",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@ -10170,7 +10206,8 @@
|
||||
"typescript": {
|
||||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w=="
|
||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -10461,9 +10498,9 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.3.2.tgz",
|
||||
"integrity": "sha512-H0hSO7xqTIrwxoACqnSoNromEMfXvfuVnrbuSt2TuXfBDDofbnto4zuZlRtRvC73/b37q3wGAWZyUU41QPvNbA=="
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
|
||||
"integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
|
||||
},
|
||||
"chartjs-adapter-moment": {
|
||||
"version": "1.0.0",
|
||||
@ -10899,12 +10936,13 @@
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
},
|
||||
"eslint": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz",
|
||||
"integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==",
|
||||
"version": "7.30.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
|
||||
"integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.12.11",
|
||||
"@eslint/eslintrc": "^0.4.2",
|
||||
"@humanwhocodes/config-array": "^0.5.0",
|
||||
"ajv": "^6.10.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
@ -13200,9 +13238,9 @@
|
||||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.52.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.2.tgz",
|
||||
"integrity": "sha512-4RlFC3k2BIHlUsJ9mGd8OO+9Lm2eDF5P7+6DNQOp5sx+7N/1tFM01kELfbxlMX3MxT6owvLB1ln4S3QvvQlbUA==",
|
||||
"version": "2.52.7",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.7.tgz",
|
||||
"integrity": "sha512-55cSH4CCU6MaPr9TAOyrIC+7qFCHscL7tkNsm1MBfIJRRqRbCEY0mmeFn4Wg8FKsHtEH8r389Fz38r/o+kgXLg==",
|
||||
"requires": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
@ -13896,9 +13934,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
|
||||
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew=="
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
||||
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.13.0",
|
||||
|
||||
@ -47,28 +47,28 @@
|
||||
"@lingui/cli": "^3.10.2",
|
||||
"@lingui/core": "^3.10.4",
|
||||
"@lingui/macro": "^3.10.2",
|
||||
"@patternfly/patternfly": "^4.108.2",
|
||||
"@patternfly/patternfly": "^4.115.2",
|
||||
"@polymer/iron-form": "^3.0.1",
|
||||
"@polymer/paper-input": "^3.2.1",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@sentry/browser": "^6.7.2",
|
||||
"@sentry/tracing": "^6.7.2",
|
||||
"@types/chart.js": "^2.9.32",
|
||||
"@types/codemirror": "5.60.0",
|
||||
"@sentry/browser": "^6.8.0",
|
||||
"@sentry/tracing": "^6.8.0",
|
||||
"@types/chart.js": "^2.9.33",
|
||||
"@types/codemirror": "5.60.1",
|
||||
"@types/grecaptcha": "^3.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||
"@typescript-eslint/parser": "^4.28.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
||||
"@typescript-eslint/parser": "^4.28.1",
|
||||
"@webcomponents/webcomponentsjs": "^2.5.0",
|
||||
"authentik-api": "file:api",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^3.3.2",
|
||||
"chart.js": "^3.4.1",
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"codemirror": "^5.62.0",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint": "^7.30.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.2",
|
||||
"eslint-plugin-lit": "^1.5.1",
|
||||
@ -77,7 +77,7 @@
|
||||
"lit-html": "^1.4.1",
|
||||
"moment": "^2.29.1",
|
||||
"rapidoc": "^9.0.0",
|
||||
"rollup": "^2.52.2",
|
||||
"rollup": "^2.52.7",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-cssimport": "^1.0.2",
|
||||
@ -87,7 +87,7 @@
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"ts-lit-plugin": "^1.2.1",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "^4.3.4",
|
||||
"typescript": "^4.3.5",
|
||||
"webcomponent-qr-code": "^1.0.5",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
|
||||
@ -7,7 +7,16 @@ export class LoggingMiddleware implements Middleware {
|
||||
|
||||
post(context: ResponseContext): Promise<Response | void> {
|
||||
tenant().then(tenant => {
|
||||
console.debug(`authentik/api[${tenant.matchedDomain}]: ${context.response.status} ${context.init.method} ${context.url}`);
|
||||
let msg = `authentik/api[${tenant.matchedDomain}]: `;
|
||||
msg += `${context.response.status} ${context.init.method} ${context.url}`;
|
||||
if (context.response.status >= 400) {
|
||||
context.response.text().then(t => {
|
||||
msg += ` => ${t}`;
|
||||
console.debug(msg);
|
||||
});
|
||||
} else {
|
||||
console.debug(msg);
|
||||
}
|
||||
});
|
||||
return Promise.resolve(context.response);
|
||||
}
|
||||
|
||||
@ -139,6 +139,7 @@ body {
|
||||
/* Card */
|
||||
.pf-c-card {
|
||||
--pf-c-card--BackgroundColor: var(--ak-dark-background-light);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-card__title,
|
||||
.pf-c-card__body {
|
||||
|
||||
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2021.6.2";
|
||||
export const VERSION = "2021.6.4";
|
||||
export const PAGE_SIZE = 20;
|
||||
export const EVENT_REFRESH = "ak-refresh";
|
||||
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user