mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 14:42:22 +02:00
Compare commits
78 Commits
remote_deb
...
api--set-A
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3d9165e3d | ||
|
|
cd0440c40c | ||
|
|
138feeb116 | ||
|
|
38ae472f6c | ||
|
|
922e9b6de2 | ||
|
|
7d0656c6fa | ||
|
|
0bbe415b5b | ||
|
|
e52c1b2bdc | ||
|
|
5064167f28 | ||
|
|
bca0f51b53 | ||
|
|
67c197e5a5 | ||
|
|
32b17da699 | ||
|
|
c75eed630a | ||
|
|
9f17d6df96 | ||
|
|
13c8ad5c56 | ||
|
|
28209c03e2 | ||
|
|
f47cf08d8a | ||
|
|
d69433b314 | ||
|
|
849a6053ad | ||
|
|
abdbe0269f | ||
|
|
55384c384a | ||
|
|
06fd68f076 | ||
|
|
d35ab99b2d | ||
|
|
a3b0180049 | ||
|
|
88a545f4fb | ||
|
|
ba62507fc2 | ||
|
|
82fc2e2c80 | ||
|
|
80b3739640 | ||
|
|
1258e1eada | ||
|
|
96ed17e760 | ||
|
|
4b17468b6e | ||
|
|
c834681251 | ||
|
|
9edd7cfbda | ||
|
|
4851179522 | ||
|
|
685f920de2 | ||
|
|
3b4d51b0c5 | ||
|
|
a1098d00b7 | ||
|
|
0d4984b964 | ||
|
|
38330df1f9 | ||
|
|
8b03c36d5a | ||
|
|
07a53a101c | ||
|
|
a3db2ce6a3 | ||
|
|
5487cdb874 | ||
|
|
2d5160d09b | ||
|
|
973fe0bd65 | ||
|
|
58b5e605de | ||
|
|
626e23b87a | ||
|
|
3559beba9c | ||
|
|
0b6d3a2850 | ||
|
|
56ca192391 | ||
|
|
6df62aaa2a | ||
|
|
ca344a64c4 | ||
|
|
a0cdd81f71 | ||
|
|
8eff4c7e0b | ||
|
|
d241a0e8f1 | ||
|
|
ebfc01fcda | ||
|
|
4b0e8a411b | ||
|
|
9bf6595fc6 | ||
|
|
5c07e845d2 | ||
|
|
4f76232e7c | ||
|
|
846f8a7e30 | ||
|
|
fa1c3490c3 | ||
|
|
a35edf7d0f | ||
|
|
9d4d5b7133 | ||
|
|
8d91a76bc9 | ||
|
|
6910428a93 | ||
|
|
cb181d388a | ||
|
|
aad4b6f925 | ||
|
|
821b74d7c1 | ||
|
|
8963d29ab4 | ||
|
|
699360064e | ||
|
|
3f94f830fc | ||
|
|
aaba353a9e | ||
|
|
abdff1c877 | ||
|
|
16fd8183b0 | ||
|
|
d3eaa3a4d9 | ||
|
|
02aba83017 | ||
|
|
e78c43e9d9 |
4
.github/actions/setup/action.yml
vendored
4
.github/actions/setup/action.yml
vendored
@@ -64,7 +64,7 @@ runs:
|
|||||||
rustflags: ""
|
rustflags: ""
|
||||||
- name: Setup rust dependencies
|
- name: Setup rust dependencies
|
||||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||||
uses: taiki-e/install-action@cf525cb33f51aca27cd6fa02034117ab963ff9f1 # v2
|
uses: taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
||||||
- name: Setup node (web)
|
- name: Setup node (web)
|
||||||
@@ -104,7 +104,7 @@ runs:
|
|||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
run: |
|
run: |
|
||||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||||
docker compose -f .github/actions/setup/compose.yml up -d
|
docker compose -f .github/actions/setup/compose.yml up -d --wait
|
||||||
cd web && npm ci
|
cd web && npm ci
|
||||||
- name: Generate config
|
- name: Generate config
|
||||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||||
|
|||||||
6
.github/actions/setup/compose.yml
vendored
6
.github/actions/setup/compose.yml
vendored
@@ -8,8 +8,14 @@ services:
|
|||||||
POSTGRES_USER: authentik
|
POSTGRES_USER: authentik
|
||||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||||
POSTGRES_DB: authentik
|
POSTGRES_DB: authentik
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB} -h 127.0.0.1"]
|
||||||
|
interval: 1s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 60
|
||||||
restart: always
|
restart: always
|
||||||
s3:
|
s3:
|
||||||
container_name: s3
|
container_name: s3
|
||||||
|
|||||||
2
.github/workflows/_reusable-docker-build.yml
vendored
2
.github/workflows/_reusable-docker-build.yml
vendored
@@ -90,7 +90,7 @@ jobs:
|
|||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- uses: int128/docker-manifest-create-action@7df7f9e221d927eaadf87db231ddf728047308a4 # v2
|
- uses: int128/docker-manifest-create-action@fa55f72001a6c74b0f4997dca65c70d334905180 # v2
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
tags: ${{ matrix.tag }}
|
tags: ${{ matrix.tag }}
|
||||||
|
|||||||
16
.github/workflows/ci-main.yml
vendored
16
.github/workflows/ci-main.yml
vendored
@@ -282,10 +282,18 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
job:
|
job:
|
||||||
- name: basic
|
- name: oidc_basic
|
||||||
glob: tests/openid_conformance/test_basic.py
|
glob: tests/openid_conformance/test_oidc_basic.py
|
||||||
- name: implicit
|
- name: oidc_implicit
|
||||||
glob: tests/openid_conformance/test_implicit.py
|
glob: tests/openid_conformance/test_oidc_implicit.py
|
||||||
|
- name: oidc_rp-initiated
|
||||||
|
glob: tests/openid_conformance/test_oidc_rp_initiated.py
|
||||||
|
- name: oidc_frontchannel
|
||||||
|
glob: tests/openid_conformance/test_oidc_frontchannel.py
|
||||||
|
- name: oidc_backchannel
|
||||||
|
glob: tests/openid_conformance/test_oidc_backchannel.py
|
||||||
|
- name: ssf_transmitter
|
||||||
|
glob: tests/openid_conformance/test_ssf_transmitter.py
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -229,6 +229,11 @@ source_docs/
|
|||||||
|
|
||||||
### Golang ###
|
### Golang ###
|
||||||
/vendor/
|
/vendor/
|
||||||
|
server
|
||||||
|
proxy
|
||||||
|
ldap
|
||||||
|
rac
|
||||||
|
radius
|
||||||
|
|
||||||
### Docker ###
|
### Docker ###
|
||||||
tests/openid_conformance/exports/*.zip
|
tests/openid_conformance/exports/*.zip
|
||||||
|
|||||||
108
Cargo.lock
generated
108
Cargo.lock
generated
@@ -17,18 +17,6 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ahash"
|
|
||||||
version = "0.8.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"once_cell",
|
|
||||||
"version_check",
|
|
||||||
"zerocopy",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -203,6 +191,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"which",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1014,6 +1003,17 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "evmap"
|
||||||
|
version = "11.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8"
|
||||||
|
dependencies = [
|
||||||
|
"hashbag",
|
||||||
|
"left-right",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eyre"
|
name = "eyre"
|
||||||
version = "0.6.12"
|
version = "0.6.12"
|
||||||
@@ -1230,6 +1230,21 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generator"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"rustversion",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -1311,6 +1326,12 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbag"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@@ -1868,6 +1889,17 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "left-right"
|
||||||
|
version = "0.11.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
"loom",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.183"
|
version = "0.2.183"
|
||||||
@@ -1939,6 +1971,19 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "loom"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"generator",
|
||||||
|
"scoped-tls",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-slab"
|
name = "lru-slab"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -1978,21 +2023,22 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "metrics"
|
name = "metrics"
|
||||||
version = "0.24.3"
|
version = "0.24.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8"
|
checksum = "ff56c2e7dce6bd462e3b8919986a617027481b1dcc703175b58cf9dd98a2f071"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
|
"rapidhash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "metrics-exporter-prometheus"
|
name = "metrics-exporter-prometheus"
|
||||||
version = "0.18.1"
|
version = "0.18.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda"
|
checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"evmap",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"metrics",
|
"metrics",
|
||||||
"metrics-util",
|
"metrics-util",
|
||||||
@@ -2813,6 +2859,15 @@ dependencies = [
|
|||||||
"rand_core 0.9.5",
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rapidhash"
|
||||||
|
version = "4.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-cpuid"
|
name = "raw-cpuid"
|
||||||
version = "11.6.0"
|
version = "11.6.0"
|
||||||
@@ -2871,9 +2926,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.2"
|
version = "0.13.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -3105,6 +3160,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scoped-tls"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -4515,6 +4576,15 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "which"
|
||||||
|
version = "8.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
|||||||
@@ -43,15 +43,15 @@ hyper-unix-socket = "= 0.6.1"
|
|||||||
hyper-util = "= 0.1.20"
|
hyper-util = "= 0.1.20"
|
||||||
ipnet = { version = "= 2.12.0", features = ["serde"] }
|
ipnet = { version = "= 2.12.0", features = ["serde"] }
|
||||||
json-subscriber = "= 0.2.8"
|
json-subscriber = "= 0.2.8"
|
||||||
metrics = "= 0.24.3"
|
metrics = "= 0.24.5"
|
||||||
metrics-exporter-prometheus = { version = "= 0.18.1", default-features = false }
|
metrics-exporter-prometheus = { version = "= 0.18.3", default-features = false }
|
||||||
nix = { version = "= 0.31.2", features = ["hostname", "signal"] }
|
nix = { version = "= 0.31.2", features = ["hostname", "signal"] }
|
||||||
notify = "= 8.2.0"
|
notify = "= 8.2.0"
|
||||||
pin-project-lite = "= 0.2.17"
|
pin-project-lite = "= 0.2.17"
|
||||||
pyo3 = "= 0.28.3"
|
pyo3 = "= 0.28.3"
|
||||||
pyo3-build-config = "= 0.28.3"
|
pyo3-build-config = "= 0.28.3"
|
||||||
regex = "= 1.12.3"
|
regex = "= 1.12.3"
|
||||||
reqwest = { version = "= 0.13.2", features = [
|
reqwest = { version = "= 0.13.3", features = [
|
||||||
"form",
|
"form",
|
||||||
"json",
|
"json",
|
||||||
"multipart",
|
"multipart",
|
||||||
@@ -113,6 +113,7 @@ tracing-subscriber = { version = "= 0.3.23", features = [
|
|||||||
] }
|
] }
|
||||||
url = "= 2.5.8"
|
url = "= 2.5.8"
|
||||||
uuid = { version = "= 1.23.1", features = ["serde", "v4"] }
|
uuid = { version = "= 1.23.1", features = ["serde", "v4"] }
|
||||||
|
which = "= 8.0.2"
|
||||||
|
|
||||||
ak-axum = { package = "authentik-axum", version = "2026.5.0-rc1", path = "./packages/ak-axum" }
|
ak-axum = { package = "authentik-axum", version = "2026.5.0-rc1", path = "./packages/ak-axum" }
|
||||||
ak-client = { package = "authentik-client", version = "2026.5.0-rc1", path = "./packages/client-rust" }
|
ak-client = { package = "authentik-client", version = "2026.5.0-rc1", path = "./packages/client-rust" }
|
||||||
@@ -282,6 +283,7 @@ sqlx = { workspace = true, optional = true }
|
|||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
which.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
11
Makefile
11
Makefile
@@ -109,14 +109,11 @@ i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that requir
|
|||||||
aws-cfn:
|
aws-cfn:
|
||||||
cd lifecycle/aws && npm i && $(UV) run npm run aws-cfn
|
cd lifecycle/aws && npm i && $(UV) run npm run aws-cfn
|
||||||
|
|
||||||
run-server: ## Run the main authentik server process
|
run: ## Run the main authentik server and worker processes
|
||||||
$(UV) run ak server
|
$(UV) run ak allinone
|
||||||
|
|
||||||
run-worker: ## Run the main authentik worker process
|
run-watch: ## Run the authentik server and worker, with auto reloading
|
||||||
$(UV) run ak worker
|
watchexec --on-busy-update=restart --stop-signal=SIGINT --exts py,rs,go --no-meta --notify -- $(UV) run ak allinone
|
||||||
|
|
||||||
run-worker-watch: ## Run the authentik worker, with auto reloading
|
|
||||||
watchexec --on-busy-update=restart --stop-signal=SIGINT --exts py,rs --no-meta --notify -- $(UV) run ak worker
|
|
||||||
|
|
||||||
core-i18n-extract:
|
core-i18n-extract:
|
||||||
$(UV) run ak makemessages \
|
$(UV) run ak makemessages \
|
||||||
|
|||||||
@@ -1,31 +1,73 @@
|
|||||||
"""authentik API Modelviewset tests"""
|
"""authentik API Modelviewset tests"""
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
|
from authentik.admin.api.version_history import VersionHistoryViewSet
|
||||||
from authentik.api.v3.urls import router
|
from authentik.api.v3.urls import router
|
||||||
|
from authentik.core.tests.utils import RequestFactory, create_test_admin_user
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.tenants.api.domains import DomainViewSet
|
||||||
|
from authentik.tenants.api.tenants import TenantViewSet
|
||||||
|
from authentik.tenants.utils import get_current_tenant
|
||||||
|
|
||||||
|
|
||||||
class TestModelViewSets(TestCase):
|
class TestModelViewSets(TestCase):
|
||||||
"""Test Viewset"""
|
"""Test Viewset"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = create_test_admin_user()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
def viewset_tester_factory(test_viewset: type[ModelViewSet]) -> Callable:
|
|
||||||
|
def viewset_tester_factory(test_viewset: type[ModelViewSet], full=True) -> dict[str, Callable]:
|
||||||
"""Test Viewset"""
|
"""Test Viewset"""
|
||||||
|
|
||||||
def tester(self: TestModelViewSets):
|
def test_attrs(self: TestModelViewSets) -> None:
|
||||||
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
|
"""Test attributes we require on all viewsets"""
|
||||||
self.assertIsNotNone(getattr(test_viewset, "ordering", None))
|
self.assertIsNotNone(getattr(test_viewset, "ordering", None))
|
||||||
|
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
|
||||||
filterset_class = getattr(test_viewset, "filterset_class", None)
|
filterset_class = getattr(test_viewset, "filterset_class", None)
|
||||||
if not filterset_class:
|
if not filterset_class:
|
||||||
self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None))
|
self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None))
|
||||||
|
|
||||||
return tester
|
def test_ordering(self: TestModelViewSets) -> None:
|
||||||
|
"""Test that all ordering fields are correct"""
|
||||||
|
view = test_viewset.as_view({"get": "list"})
|
||||||
|
for ordering_field in test_viewset.ordering:
|
||||||
|
with self.subTest(ordering_field):
|
||||||
|
req = self.factory.get(
|
||||||
|
f"/?{urlencode({'ordering': ordering_field}, doseq=True)}", user=self.user
|
||||||
|
)
|
||||||
|
req.tenant = get_current_tenant()
|
||||||
|
res = view(req)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
def test_search(self: TestModelViewSets) -> None:
|
||||||
|
"""Test that search fields are correct"""
|
||||||
|
view = test_viewset.as_view({"get": "list"})
|
||||||
|
req = self.factory.get(
|
||||||
|
f"/?{urlencode({'search': generate_id()}, doseq=True)}", user=self.user
|
||||||
|
)
|
||||||
|
req.tenant = get_current_tenant()
|
||||||
|
res = view(req)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
cases = {
|
||||||
|
"attrs": test_attrs,
|
||||||
|
}
|
||||||
|
if full:
|
||||||
|
cases["ordering"] = test_ordering
|
||||||
|
cases["search"] = test_search
|
||||||
|
return cases
|
||||||
|
|
||||||
|
|
||||||
for _, viewset, _ in router.registry:
|
for _, viewset, _ in router.registry:
|
||||||
if not issubclass(viewset, ModelViewSet | ReadOnlyModelViewSet):
|
if not issubclass(viewset, ModelViewSet | ReadOnlyModelViewSet):
|
||||||
continue
|
continue
|
||||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}", viewset_tester_factory(viewset))
|
full = viewset not in [VersionHistoryViewSet, DomainViewSet, TenantViewSet]
|
||||||
|
for test, case in viewset_tester_factory(viewset, full=full).items():
|
||||||
|
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}_{test}", case)
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class BlueprintInstanceSerializer(ModelSerializer):
|
|||||||
|
|
||||||
def check_blueprint_perms(blueprint: Blueprint, user: User, explicit_action: str | None = None):
|
def check_blueprint_perms(blueprint: Blueprint, user: User, explicit_action: str | None = None):
|
||||||
"""Check for individual permissions for each model in a blueprint"""
|
"""Check for individual permissions for each model in a blueprint"""
|
||||||
for entry in blueprint.entries:
|
for entry in blueprint.iter_entries():
|
||||||
full_model = entry.get_model(blueprint)
|
full_model = entry.get_model(blueprint)
|
||||||
app, __, model = full_model.partition(".")
|
app, __, model = full_model.partition(".")
|
||||||
perms = [
|
perms = [
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
"""Test blueprints v1"""
|
"""Test blueprints v1"""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
|
from authentik.enterprise.license import LicenseKey
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.tests.utils import load_fixture
|
from authentik.lib.tests.utils import load_fixture
|
||||||
@@ -42,3 +45,45 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
|
|||||||
# Ensure objects do not exist
|
# Ensure objects do not exist
|
||||||
self.assertFalse(Flow.objects.filter(slug=flow_slug1))
|
self.assertFalse(Flow.objects.filter(slug=flow_slug1))
|
||||||
self.assertFalse(Flow.objects.filter(slug=flow_slug2))
|
self.assertFalse(Flow.objects.filter(slug=flow_slug2))
|
||||||
|
|
||||||
|
def test_enterprise_license_context_unlicensed(self):
|
||||||
|
"""Test enterprise license context defaults to a false boolean when unlicensed."""
|
||||||
|
license_key = LicenseKey("test", 0, "Test license", 0, 0)
|
||||||
|
|
||||||
|
with patch("authentik.enterprise.license.LicenseKey.get_total", return_value=license_key):
|
||||||
|
importer = Importer.from_string("""
|
||||||
|
version: 1
|
||||||
|
entries:
|
||||||
|
- identifiers:
|
||||||
|
name: enterprise-test
|
||||||
|
slug: enterprise-test
|
||||||
|
model: authentik_flows.flow
|
||||||
|
conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
designation: stage_configuration
|
||||||
|
title: foo
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.assertIs(importer.blueprint.context["goauthentik.io/enterprise/licensed"], False)
|
||||||
|
|
||||||
|
def test_enterprise_license_context_licensed(self):
|
||||||
|
"""Test enterprise license context defaults to a true boolean when licensed."""
|
||||||
|
license_key = LicenseKey("test", 253402300799, "Test license", 1000, 1000)
|
||||||
|
|
||||||
|
with patch("authentik.enterprise.license.LicenseKey.get_total", return_value=license_key):
|
||||||
|
importer = Importer.from_string("""
|
||||||
|
version: 1
|
||||||
|
entries:
|
||||||
|
- identifiers:
|
||||||
|
name: enterprise-test
|
||||||
|
slug: enterprise-test
|
||||||
|
model: authentik_flows.flow
|
||||||
|
conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
designation: stage_configuration
|
||||||
|
title: foo
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.assertIs(importer.blueprint.context["goauthentik.io/enterprise/licensed"], True)
|
||||||
|
|||||||
@@ -146,9 +146,7 @@ class Importer:
|
|||||||
try:
|
try:
|
||||||
from authentik.enterprise.license import LicenseKey
|
from authentik.enterprise.license import LicenseKey
|
||||||
|
|
||||||
context["goauthentik.io/enterprise/licensed"] = (
|
context["goauthentik.io/enterprise/licensed"] = LicenseKey.get_total().status().is_valid
|
||||||
LicenseKey.get_total().status().is_valid,
|
|
||||||
)
|
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
pass
|
pass
|
||||||
return context
|
return context
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class BrandSerializer(ModelSerializer):
|
|||||||
"flow_unenrollment",
|
"flow_unenrollment",
|
||||||
"flow_user_settings",
|
"flow_user_settings",
|
||||||
"flow_device_code",
|
"flow_device_code",
|
||||||
|
"flow_lockdown",
|
||||||
"default_application",
|
"default_application",
|
||||||
"web_certificate",
|
"web_certificate",
|
||||||
"client_certificates",
|
"client_certificates",
|
||||||
@@ -117,6 +118,7 @@ class CurrentBrandSerializer(PassiveSerializer):
|
|||||||
flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False)
|
flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False)
|
||||||
flow_user_settings = CharField(source="flow_user_settings.slug", required=False)
|
flow_user_settings = CharField(source="flow_user_settings.slug", required=False)
|
||||||
flow_device_code = CharField(source="flow_device_code.slug", required=False)
|
flow_device_code = CharField(source="flow_device_code.slug", required=False)
|
||||||
|
flow_lockdown = CharField(source="flow_lockdown.slug", required=False)
|
||||||
|
|
||||||
default_locale = CharField(read_only=True)
|
default_locale = CharField(read_only=True)
|
||||||
flags = SerializerMethodField()
|
flags = SerializerMethodField()
|
||||||
@@ -154,6 +156,7 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"flow_unenrollment",
|
"flow_unenrollment",
|
||||||
"flow_user_settings",
|
"flow_user_settings",
|
||||||
"flow_device_code",
|
"flow_device_code",
|
||||||
|
"flow_lockdown",
|
||||||
"web_certificate",
|
"web_certificate",
|
||||||
"client_certificates",
|
"client_certificates",
|
||||||
]
|
]
|
||||||
|
|||||||
25
authentik/brands/migrations/0012_brand_flow_lockdown.py
Normal file
25
authentik/brands/migrations/0012_brand_flow_lockdown.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.2.12 on 2026-03-14 02:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_brands", "0011_alter_brand_branding_default_flow_background_and_more"),
|
||||||
|
("authentik_flows", "0031_alter_flow_layout"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="brand",
|
||||||
|
name="flow_lockdown",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="brand_lockdown",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -58,6 +58,9 @@ class Brand(SerializerModel):
|
|||||||
flow_device_code = models.ForeignKey(
|
flow_device_code = models.ForeignKey(
|
||||||
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code"
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code"
|
||||||
)
|
)
|
||||||
|
flow_lockdown = models.ForeignKey(
|
||||||
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_lockdown"
|
||||||
|
)
|
||||||
|
|
||||||
default_application = models.ForeignKey(
|
default_application = models.ForeignKey(
|
||||||
"authentik_core.Application",
|
"authentik_core.Application",
|
||||||
|
|||||||
@@ -20,11 +20,16 @@ class TestBrands(APITestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.default_flags = {}
|
|
||||||
for flag in Flag.available(visibility="public"):
|
|
||||||
self.default_flags[flag().key] = flag.get()
|
|
||||||
Brand.objects.all().delete()
|
Brand.objects.all().delete()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_flags(self) -> dict[str, object]:
|
||||||
|
"""Get current public flags.
|
||||||
|
|
||||||
|
Some tests define temporary Flag subclasses, so this can't be cached in setUp.
|
||||||
|
"""
|
||||||
|
return {flag().key: flag.get() for flag in Flag.available(visibility="public")}
|
||||||
|
|
||||||
def test_current_brand(self):
|
def test_current_brand(self):
|
||||||
"""Test Current brand API"""
|
"""Test Current brand API"""
|
||||||
brand = create_test_brand()
|
brand = create_test_brand()
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ class ApplicationEntitlementViewSet(UsedByMixin, ModelViewSet):
|
|||||||
search_fields = [
|
search_fields = [
|
||||||
"pbm_uuid",
|
"pbm_uuid",
|
||||||
"name",
|
"name",
|
||||||
"app",
|
"app__name",
|
||||||
|
"app__slug",
|
||||||
"attributes",
|
"attributes",
|
||||||
]
|
]
|
||||||
filterset_fields = [
|
filterset_fields = [
|
||||||
|
|||||||
@@ -32,19 +32,19 @@ from authentik.rbac.decorators import permission_required
|
|||||||
class UserAgentDeviceDict(TypedDict):
|
class UserAgentDeviceDict(TypedDict):
|
||||||
"""User agent device"""
|
"""User agent device"""
|
||||||
|
|
||||||
brand: str
|
brand: str | None = None
|
||||||
family: str
|
family: str
|
||||||
model: str
|
model: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class UserAgentOSDict(TypedDict):
|
class UserAgentOSDict(TypedDict):
|
||||||
"""User agent os"""
|
"""User agent os"""
|
||||||
|
|
||||||
family: str
|
family: str
|
||||||
major: str
|
major: str | None = None
|
||||||
minor: str
|
minor: str | None = None
|
||||||
patch: str
|
patch: str | None = None
|
||||||
patch_minor: str
|
patch_minor: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class UserAgentBrowserDict(TypedDict):
|
class UserAgentBrowserDict(TypedDict):
|
||||||
|
|||||||
@@ -563,6 +563,9 @@ class UsersFilter(FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class UserViewSet(
|
class UserViewSet(
|
||||||
|
ConditionalInheritance(
|
||||||
|
"authentik.enterprise.stages.account_lockdown.api.UserAccountLockdownMixin"
|
||||||
|
),
|
||||||
ConditionalInheritance("authentik.enterprise.reports.api.reports.ExportMixin"),
|
ConditionalInheritance("authentik.enterprise.reports.api.reports.ExportMixin"),
|
||||||
UsedByMixin,
|
UsedByMixin,
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""authentik core signals"""
|
"""authentik core signals"""
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
from django.contrib.auth.signals import user_logged_in
|
from django.contrib.auth.signals import user_logged_in
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@@ -59,7 +58,7 @@ def user_logged_in_session(sender, request: HttpRequest, user: User, **_):
|
|||||||
layer = get_channel_layer()
|
layer = get_channel_layer()
|
||||||
device_cookie = request.COOKIES.get("authentik_device")
|
device_cookie = request.COOKIES.get("authentik_device")
|
||||||
if device_cookie:
|
if device_cookie:
|
||||||
async_to_sync(layer.group_send)(
|
layer.group_send_blocking(
|
||||||
build_device_group(device_cookie),
|
build_device_group(device_cookie),
|
||||||
{"type": "event.session.authenticated"},
|
{"type": "event.session.authenticated"},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<style data-id="static-styles">
|
<style data-id="static-styles">
|
||||||
:root {
|
:root {
|
||||||
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url }}");
|
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url|iriencode|safe }}");
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.db.models import BooleanField as ModelBooleanField
|
from django.db.models import Exists, OuterRef, Q, Subquery
|
||||||
from django.db.models import Case, Q, Value, When
|
|
||||||
from django_filters.rest_framework import BooleanFilter, FilterSet
|
from django_filters.rest_framework import BooleanFilter, FilterSet
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
@@ -14,7 +13,7 @@ from rest_framework.viewsets import GenericViewSet
|
|||||||
from authentik.core.api.utils import ModelSerializer
|
from authentik.core.api.utils import ModelSerializer
|
||||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||||
from authentik.enterprise.lifecycle.api.reviews import ReviewSerializer
|
from authentik.enterprise.lifecycle.api.reviews import ReviewSerializer
|
||||||
from authentik.enterprise.lifecycle.models import LifecycleIteration, ReviewState
|
from authentik.enterprise.lifecycle.models import LifecycleIteration, LifecycleRule, ReviewState
|
||||||
from authentik.enterprise.lifecycle.utils import (
|
from authentik.enterprise.lifecycle.utils import (
|
||||||
ContentTypeField,
|
ContentTypeField,
|
||||||
ReviewerGroupSerializer,
|
ReviewerGroupSerializer,
|
||||||
@@ -26,20 +25,25 @@ from authentik.enterprise.lifecycle.utils import (
|
|||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedRuleSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
||||||
|
reviewer_groups = ReviewerGroupSerializer(many=True, read_only=True)
|
||||||
|
min_reviewers = IntegerField(read_only=True)
|
||||||
|
reviewers = ReviewerUserSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = LifecycleRule
|
||||||
|
fields = ["id", "name", "reviewer_groups", "min_reviewers", "reviewers"]
|
||||||
|
|
||||||
|
|
||||||
class LifecycleIterationSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
class LifecycleIterationSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
||||||
content_type = ContentTypeField()
|
content_type = ContentTypeField()
|
||||||
object_verbose = SerializerMethodField()
|
object_verbose = SerializerMethodField()
|
||||||
|
rule = RelatedRuleSerializer(read_only=True)
|
||||||
object_admin_url = SerializerMethodField(read_only=True)
|
object_admin_url = SerializerMethodField(read_only=True)
|
||||||
grace_period_end = SerializerMethodField(read_only=True)
|
grace_period_end = SerializerMethodField(read_only=True)
|
||||||
reviews = ReviewSerializer(many=True, read_only=True, source="review_set.all")
|
reviews = ReviewSerializer(many=True, read_only=True, source="review_set.all")
|
||||||
user_can_review = SerializerMethodField(read_only=True)
|
user_can_review = SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
reviewer_groups = ReviewerGroupSerializer(
|
|
||||||
many=True, read_only=True, source="rule.reviewer_groups"
|
|
||||||
)
|
|
||||||
min_reviewers = IntegerField(read_only=True, source="rule.min_reviewers")
|
|
||||||
reviewers = ReviewerUserSerializer(many=True, read_only=True, source="rule.reviewers")
|
|
||||||
|
|
||||||
next_review_date = SerializerMethodField(read_only=True)
|
next_review_date = SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -55,10 +59,8 @@ class LifecycleIterationSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
|||||||
"grace_period_end",
|
"grace_period_end",
|
||||||
"next_review_date",
|
"next_review_date",
|
||||||
"reviews",
|
"reviews",
|
||||||
|
"rule",
|
||||||
"user_can_review",
|
"user_can_review",
|
||||||
"reviewer_groups",
|
|
||||||
"min_reviewers",
|
|
||||||
"reviewers",
|
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
@@ -88,43 +90,55 @@ class IterationViewSet(EnterpriseRequiredMixin, CreateModelMixin, GenericViewSet
|
|||||||
queryset = LifecycleIteration.objects.all()
|
queryset = LifecycleIteration.objects.all()
|
||||||
serializer_class = LifecycleIterationSerializer
|
serializer_class = LifecycleIterationSerializer
|
||||||
ordering = ["-opened_on"]
|
ordering = ["-opened_on"]
|
||||||
ordering_fields = ["state", "content_type__model", "opened_on", "grace_period_end"]
|
ordering_fields = [
|
||||||
|
"state",
|
||||||
|
"content_type__model",
|
||||||
|
"rule__name",
|
||||||
|
"opened_on",
|
||||||
|
"grace_period_end",
|
||||||
|
]
|
||||||
filterset_class = LifecycleIterationFilterSet
|
filterset_class = LifecycleIterationFilterSet
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
return self.queryset.annotate(
|
return self.queryset.annotate(
|
||||||
user_is_reviewer=Case(
|
user_is_reviewer=Exists(
|
||||||
When(
|
LifecycleRule.objects.filter(
|
||||||
Q(rule__reviewers=user)
|
pk=OuterRef("rule_id"),
|
||||||
| Q(rule__reviewer_groups__in=user.groups.all().with_ancestors()),
|
).filter(
|
||||||
then=Value(True),
|
Q(reviewers=user) | Q(reviewer_groups__in=user.groups.all().with_ancestors())
|
||||||
),
|
)
|
||||||
default=Value(False),
|
|
||||||
output_field=ModelBooleanField(),
|
|
||||||
)
|
)
|
||||||
).distinct()
|
)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
operation_id="lifecycle_iterations_list_latest",
|
||||||
|
responses={200: LifecycleIterationSerializer(many=True)},
|
||||||
|
)
|
||||||
@action(
|
@action(
|
||||||
detail=False,
|
detail=False,
|
||||||
|
pagination_class=None,
|
||||||
methods=["get"],
|
methods=["get"],
|
||||||
url_path=r"latest/(?P<content_type>[^/]+)/(?P<object_id>[^/]+)",
|
url_path=r"latest/(?P<content_type>[^/]+)/(?P<object_id>[^/]+)",
|
||||||
)
|
)
|
||||||
def latest_iteration(self, request: Request, content_type: str, object_id: str) -> Response:
|
def latest_iterations(self, request: Request, content_type: str, object_id: str) -> Response:
|
||||||
ct = parse_content_type(content_type)
|
ct = parse_content_type(content_type)
|
||||||
try:
|
latest_ids_subquery = (
|
||||||
obj = (
|
LifecycleIteration.objects.filter(
|
||||||
self.get_queryset()
|
rule=OuterRef("rule"),
|
||||||
.filter(
|
content_type__app_label=ct["app_label"],
|
||||||
content_type__app_label=ct["app_label"],
|
content_type__model=ct["model"],
|
||||||
content_type__model=ct["model"],
|
object_id=object_id,
|
||||||
object_id=object_id,
|
|
||||||
)
|
|
||||||
.latest("opened_on")
|
|
||||||
)
|
)
|
||||||
except LifecycleIteration.DoesNotExist:
|
.order_by("-opened_on")
|
||||||
return Response(status=404)
|
.values("id")[:1]
|
||||||
serializer = self.get_serializer(obj)
|
)
|
||||||
|
latest_per_rule = LifecycleIteration.objects.filter(
|
||||||
|
content_type__app_label=ct["app_label"],
|
||||||
|
content_type__model=ct["model"],
|
||||||
|
object_id=object_id,
|
||||||
|
).filter(id=Subquery(latest_ids_subquery))
|
||||||
|
serializer = self.get_serializer(latest_per_rule, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
|||||||
@@ -84,23 +84,6 @@ class LifecycleRuleSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
|||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{"grace_period": _("Grace period must be shorter than the interval.")}
|
{"grace_period": _("Grace period must be shorter than the interval.")}
|
||||||
)
|
)
|
||||||
if "content_type" in attrs or "object_id" in attrs:
|
|
||||||
content_type = attrs.get("content_type", getattr(self.instance, "content_type", None))
|
|
||||||
object_id = attrs.get("object_id", getattr(self.instance, "object_id", None))
|
|
||||||
if content_type is not None and object_id is None:
|
|
||||||
existing = LifecycleRule.objects.filter(
|
|
||||||
content_type=content_type, object_id__isnull=True
|
|
||||||
)
|
|
||||||
if self.instance:
|
|
||||||
existing = existing.exclude(pk=self.instance.pk)
|
|
||||||
if existing.exists():
|
|
||||||
raise ValidationError(
|
|
||||||
{
|
|
||||||
"content_type": _(
|
|
||||||
"Only one type-wide rule for each object type is allowed."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.2.11 on 2026-03-05 11:27
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_lifecycle", "0002_alter_lifecycleiteration_opened_on"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="lifecyclerule",
|
||||||
|
name="uniq_lifecycle_rule_ct_null_object",
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="lifecyclerule",
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -56,14 +56,6 @@ class LifecycleRule(SerializerModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [models.Index(fields=["content_type"])]
|
indexes = [models.Index(fields=["content_type"])]
|
||||||
unique_together = [["content_type", "object_id"]]
|
|
||||||
constraints = [
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=["content_type"],
|
|
||||||
condition=Q(object_id__isnull=True),
|
|
||||||
name="uniq_lifecycle_rule_ct_null_object",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[BaseSerializer]:
|
def serializer(self) -> type[BaseSerializer]:
|
||||||
@@ -82,12 +74,6 @@ class LifecycleRule(SerializerModel):
|
|||||||
qs = self.content_type.get_all_objects_for_this_type()
|
qs = self.content_type.get_all_objects_for_this_type()
|
||||||
if self.object_id:
|
if self.object_id:
|
||||||
qs = qs.filter(pk=self.object_id)
|
qs = qs.filter(pk=self.object_id)
|
||||||
else:
|
|
||||||
qs = qs.exclude(
|
|
||||||
pk__in=LifecycleRule.objects.filter(
|
|
||||||
content_type=self.content_type, object_id__isnull=False
|
|
||||||
).values_list(Cast("object_id", output_field=self._get_pk_field()), flat=True)
|
|
||||||
)
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def _get_stale_iterations(self) -> QuerySet[LifecycleIteration]:
|
def _get_stale_iterations(self) -> QuerySet[LifecycleIteration]:
|
||||||
@@ -107,8 +93,7 @@ class LifecycleRule(SerializerModel):
|
|||||||
|
|
||||||
def _get_newly_due_objects(self) -> QuerySet:
|
def _get_newly_due_objects(self) -> QuerySet:
|
||||||
recent_iteration_ids = LifecycleIteration.objects.filter(
|
recent_iteration_ids = LifecycleIteration.objects.filter(
|
||||||
content_type=self.content_type,
|
rule=self,
|
||||||
object_id__isnull=False,
|
|
||||||
opened_on__gte=start_of_day(
|
opened_on__gte=start_of_day(
|
||||||
timezone.now() + timedelta(days=1) - timedelta_from_string(self.interval)
|
timezone.now() + timedelta(days=1) - timedelta_from_string(self.interval)
|
||||||
),
|
),
|
||||||
@@ -214,9 +199,15 @@ class LifecycleIteration(SerializerModel, ManagedModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
|
if (self.content_type.app_label, self.content_type.model) == ("authentik_core", "group"):
|
||||||
|
object_label = self.object.name
|
||||||
|
elif (self.content_type.app_label, self.content_type.model) == ("authentik_rbac", "role"):
|
||||||
|
object_label = self.object.name
|
||||||
|
else:
|
||||||
|
object_label = str(self.object)
|
||||||
event = Event.new(
|
event = Event.new(
|
||||||
EventAction.REVIEW_INITIATED,
|
EventAction.REVIEW_INITIATED,
|
||||||
message=_(f"Access review is due for {self.content_type.name} {str(self.object)}"),
|
message=_(f"Access review is due for {self.content_type.name.lower()} {object_label}"),
|
||||||
**self._get_event_args(),
|
**self._get_event_args(),
|
||||||
)
|
)
|
||||||
event.save()
|
event.save()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.db.models.signals import post_save, pre_delete
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from authentik.enterprise.lifecycle.models import LifecycleRule, ReviewState
|
from authentik.enterprise.lifecycle.models import LifecycleRule, ReviewState
|
||||||
|
from authentik.tasks.schedules.models import Schedule
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=LifecycleRule)
|
@receiver(post_save, sender=LifecycleRule)
|
||||||
@@ -11,7 +12,9 @@ def post_rule_save(sender, instance: LifecycleRule, created: bool, **_):
|
|||||||
|
|
||||||
apply_lifecycle_rule.send_with_options(
|
apply_lifecycle_rule.send_with_options(
|
||||||
args=(instance.id,),
|
args=(instance.id,),
|
||||||
rel_obj=instance,
|
rel_obj=Schedule.objects.get(
|
||||||
|
actor_name="authentik.enterprise.lifecycle.tasks.apply_lifecycle_rules"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ from dramatiq import actor
|
|||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.enterprise.lifecycle.models import LifecycleRule
|
from authentik.enterprise.lifecycle.models import LifecycleRule
|
||||||
from authentik.events.models import Event, Notification, NotificationTransport
|
from authentik.events.models import Event, Notification, NotificationTransport
|
||||||
|
from authentik.tasks.schedules.models import Schedule
|
||||||
|
|
||||||
|
|
||||||
@actor(description=_("Dispatch tasks to validate lifecycle rules."))
|
@actor(description=_("Dispatch tasks to apply lifecycle rules."))
|
||||||
def apply_lifecycle_rules():
|
def apply_lifecycle_rules():
|
||||||
for rule in LifecycleRule.objects.all():
|
for rule in LifecycleRule.objects.all():
|
||||||
apply_lifecycle_rule.send_with_options(
|
apply_lifecycle_rule.send_with_options(
|
||||||
args=(rule.id,),
|
args=(rule.id,),
|
||||||
rel_obj=rule,
|
rel_obj=Schedule.objects.get(
|
||||||
|
actor_name="authentik.enterprise.lifecycle.tasks.apply_lifecycle_rules"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
@@ -19,6 +20,11 @@ class TestLifecycleRuleAPI(APITestCase):
|
|||||||
self.content_type = ContentType.objects.get_for_model(Application)
|
self.content_type = ContentType.objects.get_for_model(Application)
|
||||||
self.reviewer_group = Group.objects.create(name=generate_id())
|
self.reviewer_group = Group.objects.create(name=generate_id())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
config = apps.get_app_config("authentik_tasks_schedules")
|
||||||
|
config._on_startup_callback(None)
|
||||||
|
|
||||||
def test_list_rules(self):
|
def test_list_rules(self):
|
||||||
rule = LifecycleRule.objects.create(
|
rule = LifecycleRule.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
@@ -190,6 +196,11 @@ class TestIterationAPI(APITestCase):
|
|||||||
self.reviewer_group = Group.objects.create(name=generate_id())
|
self.reviewer_group = Group.objects.create(name=generate_id())
|
||||||
self.reviewer_group.users.add(self.user)
|
self.reviewer_group.users.add(self.user)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
config = apps.get_app_config("authentik_tasks_schedules")
|
||||||
|
config._on_startup_callback(None)
|
||||||
|
|
||||||
def test_open_iterations(self):
|
def test_open_iterations(self):
|
||||||
rule = LifecycleRule.objects.create(
|
rule = LifecycleRule.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
@@ -231,7 +242,7 @@ class TestIterationAPI(APITestCase):
|
|||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_api:lifecycleiteration-latest-iteration",
|
"authentik_api:lifecycleiteration-latest-iterations",
|
||||||
kwargs={
|
kwargs={
|
||||||
"content_type": f"{self.content_type.app_label}.{self.content_type.model}",
|
"content_type": f"{self.content_type.app_label}.{self.content_type.model}",
|
||||||
"object_id": str(self.app.pk),
|
"object_id": str(self.app.pk),
|
||||||
@@ -239,19 +250,20 @@ class TestIterationAPI(APITestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.data["object_id"], str(self.app.pk))
|
self.assertEqual(len(response.data), 1)
|
||||||
|
self.assertEqual(response.data[0]["object_id"], str(self.app.pk))
|
||||||
|
|
||||||
def test_latest_iteration_not_found(self):
|
def test_latest_iteration_not_found(self):
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_api:lifecycleiteration-latest-iteration",
|
"authentik_api:lifecycleiteration-latest-iterations",
|
||||||
kwargs={
|
kwargs={
|
||||||
"content_type": f"{self.content_type.app_label}.{self.content_type.model}",
|
"content_type": f"{self.content_type.app_label}.{self.content_type.model}",
|
||||||
"object_id": "00000000-0000-0000-0000-000000000000",
|
"object_id": "00000000-0000-0000-0000-000000000000",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.data, [])
|
||||||
|
|
||||||
def test_iteration_includes_user_can_review(self):
|
def test_iteration_includes_user_can_review(self):
|
||||||
rule = LifecycleRule.objects.create(
|
rule = LifecycleRule.objects.create(
|
||||||
@@ -279,6 +291,11 @@ class TestReviewAPI(APITestCase):
|
|||||||
self.reviewer_group = Group.objects.create(name=generate_id())
|
self.reviewer_group = Group.objects.create(name=generate_id())
|
||||||
self.reviewer_group.users.add(self.user)
|
self.reviewer_group.users.add(self.user)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
config = apps.get_app_config("authentik_tasks_schedules")
|
||||||
|
config._on_startup_callback(None)
|
||||||
|
|
||||||
def test_create_review(self):
|
def test_create_review(self):
|
||||||
rule = LifecycleRule.objects.create(
|
rule = LifecycleRule.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import datetime as dt
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import RequestFactory, TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -29,6 +30,11 @@ class TestLifecycleModels(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
config = apps.get_app_config("authentik_tasks_schedules")
|
||||||
|
config._on_startup_callback(None)
|
||||||
|
|
||||||
def _get_request(self):
|
def _get_request(self):
|
||||||
return self.factory.get("/")
|
return self.factory.get("/")
|
||||||
|
|
||||||
@@ -438,31 +444,6 @@ class TestLifecycleModels(TestCase):
|
|||||||
self.assertIn(app_one, objects)
|
self.assertIn(app_one, objects)
|
||||||
self.assertIn(app_two, objects)
|
self.assertIn(app_two, objects)
|
||||||
|
|
||||||
def test_rule_type_excludes_objects_with_specific_rules(self):
|
|
||||||
app_with_rule = Application.objects.create(name=generate_id(), slug=generate_id())
|
|
||||||
app_without_rule = Application.objects.create(name=generate_id(), slug=generate_id())
|
|
||||||
content_type = ContentType.objects.get_for_model(Application)
|
|
||||||
|
|
||||||
# Create a specific rule for app_with_rule
|
|
||||||
LifecycleRule.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
content_type=content_type,
|
|
||||||
object_id=str(app_with_rule.pk),
|
|
||||||
interval="days=30",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a type-level rule
|
|
||||||
type_rule = LifecycleRule.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
content_type=content_type,
|
|
||||||
object_id=None,
|
|
||||||
interval="days=60",
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = list(type_rule.get_objects())
|
|
||||||
self.assertNotIn(app_with_rule, objects)
|
|
||||||
self.assertIn(app_without_rule, objects)
|
|
||||||
|
|
||||||
def test_rule_type_apply_creates_iterations_for_all_objects(self):
|
def test_rule_type_apply_creates_iterations_for_all_objects(self):
|
||||||
app_one = Application.objects.create(name=generate_id(), slug=generate_id())
|
app_one = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||||
app_two = Application.objects.create(name=generate_id(), slug=generate_id())
|
app_two = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||||
@@ -669,6 +650,73 @@ class TestLifecycleModels(TestCase):
|
|||||||
self.assertIn(explicit_reviewer, reviewers)
|
self.assertIn(explicit_reviewer, reviewers)
|
||||||
self.assertIn(group_member, reviewers)
|
self.assertIn(group_member, reviewers)
|
||||||
|
|
||||||
|
def test_multiple_rules_same_object_create_separate_iterations(self):
|
||||||
|
"""Two rules targeting the same object each create their own iteration."""
|
||||||
|
obj = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
|
||||||
|
rule_one = self._create_rule_for_object(obj, interval="days=30", grace_period="days=10")
|
||||||
|
rule_two = self._create_rule_for_object(obj, interval="days=60", grace_period="days=20")
|
||||||
|
|
||||||
|
iterations = LifecycleIteration.objects.filter(
|
||||||
|
content_type=content_type, object_id=str(obj.pk)
|
||||||
|
)
|
||||||
|
self.assertEqual(iterations.count(), 2)
|
||||||
|
|
||||||
|
iter_one = iterations.get(rule=rule_one)
|
||||||
|
iter_two = iterations.get(rule=rule_two)
|
||||||
|
self.assertEqual(iter_one.state, ReviewState.PENDING)
|
||||||
|
self.assertEqual(iter_two.state, ReviewState.PENDING)
|
||||||
|
self.assertNotEqual(iter_one.pk, iter_two.pk)
|
||||||
|
|
||||||
|
def test_multiple_rules_same_object_reviewed_independently(self):
|
||||||
|
"""Reviewing one rule's iteration does not affect the other rule's iteration."""
|
||||||
|
obj = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
|
||||||
|
reviewer = create_test_user()
|
||||||
|
|
||||||
|
rule_one = self._create_rule_for_object(obj, min_reviewers=1)
|
||||||
|
rule_two = self._create_rule_for_object(obj, min_reviewers=1)
|
||||||
|
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
group.users.add(reviewer)
|
||||||
|
rule_one.reviewer_groups.add(group)
|
||||||
|
rule_two.reviewer_groups.add(group)
|
||||||
|
|
||||||
|
iter_one = LifecycleIteration.objects.get(
|
||||||
|
content_type=content_type, object_id=str(obj.pk), rule=rule_one
|
||||||
|
)
|
||||||
|
iter_two = LifecycleIteration.objects.get(
|
||||||
|
content_type=content_type, object_id=str(obj.pk), rule=rule_two
|
||||||
|
)
|
||||||
|
|
||||||
|
request = self._get_request()
|
||||||
|
|
||||||
|
# Review only rule_one's iteration
|
||||||
|
Review.objects.create(iteration=iter_one, reviewer=reviewer)
|
||||||
|
iter_one.on_review(request)
|
||||||
|
|
||||||
|
iter_one.refresh_from_db()
|
||||||
|
iter_two.refresh_from_db()
|
||||||
|
self.assertEqual(iter_one.state, ReviewState.REVIEWED)
|
||||||
|
self.assertEqual(iter_two.state, ReviewState.PENDING)
|
||||||
|
|
||||||
|
def test_type_rule_and_object_rule_both_create_iterations(self):
|
||||||
|
"""A type-level rule and an object-level rule both create iterations for the same object."""
|
||||||
|
obj = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
|
||||||
|
object_rule = self._create_rule_for_object(obj, interval="days=30")
|
||||||
|
type_rule = self._create_rule_for_type(Application, interval="days=60")
|
||||||
|
|
||||||
|
iterations = LifecycleIteration.objects.filter(
|
||||||
|
content_type=content_type, object_id=str(obj.pk)
|
||||||
|
)
|
||||||
|
self.assertEqual(iterations.count(), 2)
|
||||||
|
self.assertTrue(iterations.filter(rule=object_rule).exists())
|
||||||
|
self.assertTrue(iterations.filter(rule=type_rule).exists())
|
||||||
|
|
||||||
|
|
||||||
class TestLifecycleDateBoundaries(TestCase):
|
class TestLifecycleDateBoundaries(TestCase):
|
||||||
"""Verify that start_of_day normalization ensures correct overdue/due
|
"""Verify that start_of_day normalization ensures correct overdue/due
|
||||||
@@ -679,6 +727,11 @@ class TestLifecycleDateBoundaries(TestCase):
|
|||||||
ensures that the boundary is always at midnight, so millisecond variations
|
ensures that the boundary is always at midnight, so millisecond variations
|
||||||
in task execution time do not affect results."""
|
in task execution time do not affect results."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
config = apps.get_app_config("authentik_tasks_schedules")
|
||||||
|
config._on_startup_callback(None)
|
||||||
|
|
||||||
def _create_rule_and_iteration(self, grace_period="days=1", interval="days=365"):
|
def _create_rule_and_iteration(self, grace_period="days=1", interval="days=365"):
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||||
content_type = ContentType.objects.get_for_model(Application)
|
content_type = ContentType.objects.get_for_model(Application)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Generated by Django 5.2.12 on 2026-04-04 16:58
|
# Generated by Django 5.2.12 on 2026-04-04 16:58
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -40,4 +41,109 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="stream",
|
||||||
|
name="events_requested",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
|
||||||
|
"Caep Session Revoked",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/token-claims-change",
|
||||||
|
"Caep Token Claims Change",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
|
||||||
|
"Caep Credential Change",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/assurance-level-change",
|
||||||
|
"Caep Assurance Level Change",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/device-compliance-change",
|
||||||
|
"Caep Device Compliance Change",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/session-established",
|
||||||
|
"Caep Session Established",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/session-presented",
|
||||||
|
"Caep Session Presented",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/risk-level-change",
|
||||||
|
"Caep Risk Level Change",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/ssf/event-type/verification",
|
||||||
|
"Set Verification",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
default=list,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="stream",
|
||||||
|
name="status",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("enabled", "Enabled"),
|
||||||
|
("paused", "Paused"),
|
||||||
|
("disabled", "Disabled"),
|
||||||
|
("disabled_deleted", "Disabled Deleted"),
|
||||||
|
],
|
||||||
|
default="enabled",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="streamevent",
|
||||||
|
name="type",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
|
||||||
|
"Caep Session Revoked",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/token-claims-change",
|
||||||
|
"Caep Token Claims Change",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
|
||||||
|
"Caep Credential Change",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/assurance-level-change",
|
||||||
|
"Caep Assurance Level Change",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/device-compliance-change",
|
||||||
|
"Caep Device Compliance Change",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/session-established",
|
||||||
|
"Caep Session Established",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/session-presented",
|
||||||
|
"Caep Session Presented",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/risk-level-change",
|
||||||
|
"Caep Risk Level Change",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://schemas.openid.net/secevent/ssf/event-type/verification",
|
||||||
|
"Set Verification",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -24,8 +24,31 @@ class EventTypes(models.TextChoices):
|
|||||||
"""SSF Event types supported by authentik"""
|
"""SSF Event types supported by authentik"""
|
||||||
|
|
||||||
CAEP_SESSION_REVOKED = "https://schemas.openid.net/secevent/caep/event-type/session-revoked"
|
CAEP_SESSION_REVOKED = "https://schemas.openid.net/secevent/caep/event-type/session-revoked"
|
||||||
|
"""https://openid.net/specs/openid-caep-1_0-final.html#section-3.1"""
|
||||||
|
CAEP_TOKEN_CLAIMS_CHANGE = (
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/token-claims-change"
|
||||||
|
)
|
||||||
|
"""https://openid.net/specs/openid-caep-1_0-final.html#section-3.2"""
|
||||||
CAEP_CREDENTIAL_CHANGE = "https://schemas.openid.net/secevent/caep/event-type/credential-change"
|
CAEP_CREDENTIAL_CHANGE = "https://schemas.openid.net/secevent/caep/event-type/credential-change"
|
||||||
|
"""https://openid.net/specs/openid-caep-1_0-final.html#section-3.3"""
|
||||||
|
CAEP_ASSURANCE_LEVEL_CHANGE = (
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/assurance-level-change"
|
||||||
|
)
|
||||||
|
"""https://openid.net/specs/openid-caep-1_0-final.html#section-3.4"""
|
||||||
|
CAEP_DEVICE_COMPLIANCE_CHANGE = (
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/device-compliance-change"
|
||||||
|
)
|
||||||
|
"""https://openid.net/specs/openid-caep-1_0-final.html#section-3.5"""
|
||||||
|
CAEP_SESSION_ESTABLISHED = (
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/session-established"
|
||||||
|
)
|
||||||
|
"""https://openid.net/specs/openid-caep-1_0-final.html#section-3.6"""
|
||||||
|
CAEP_SESSION_PRESENTED = "https://schemas.openid.net/secevent/caep/event-type/session-presented"
|
||||||
|
"""https://openid.net/specs/openid-caep-1_0-final.html#section-3.7"""
|
||||||
|
CAEP_RISK_LEVEL_CHANGE = "https://schemas.openid.net/secevent/caep/event-type/risk-level-change"
|
||||||
|
"""https://openid.net/specs/openid-caep-1_0-final.html#section-3.8"""
|
||||||
SET_VERIFICATION = "https://schemas.openid.net/secevent/ssf/event-type/verification"
|
SET_VERIFICATION = "https://schemas.openid.net/secevent/ssf/event-type/verification"
|
||||||
|
"""https://openid.net/specs/openid-sharedsignals-framework-1_0.html#section-8.1.4.1"""
|
||||||
|
|
||||||
|
|
||||||
class DeliveryMethods(models.TextChoices):
|
class DeliveryMethods(models.TextChoices):
|
||||||
@@ -46,10 +69,12 @@ class SSFEventStatus(models.TextChoices):
|
|||||||
|
|
||||||
|
|
||||||
class StreamStatus(models.TextChoices):
|
class StreamStatus(models.TextChoices):
|
||||||
|
"""SSF Stream status"""
|
||||||
|
|
||||||
ENABLED = "enabled"
|
ENABLED = "enabled"
|
||||||
PAUSED = "paused"
|
PAUSED = "paused"
|
||||||
DISABLED = "disabled"
|
DISABLED = "disabled"
|
||||||
|
DISABLED_DELETED = "disabled_deleted"
|
||||||
|
|
||||||
|
|
||||||
class SSFProvider(TasksModel, BackchannelProvider):
|
class SSFProvider(TasksModel, BackchannelProvider):
|
||||||
|
|||||||
@@ -108,13 +108,13 @@ def send_ssf_event(stream_uuid: UUID, event_data: dict[str, Any]):
|
|||||||
event.save()
|
event.save()
|
||||||
self.info("Event successfully sent", status=response.status_code)
|
self.info("Event successfully sent", status=response.status_code)
|
||||||
# Cleanup, if we were the last pending message for this stream and it has been deleted
|
# Cleanup, if we were the last pending message for this stream and it has been deleted
|
||||||
# (status=StreamStatus.DISABLED), then we can delete the stream
|
# (status=StreamStatus.DISABLED_DELETED), then we can delete the stream
|
||||||
if (
|
if (
|
||||||
not StreamEvent.objects.filter(
|
not StreamEvent.objects.filter(
|
||||||
stream=stream,
|
stream=stream,
|
||||||
status__in=[SSFEventStatus.PENDING_FAILED, SSFEventStatus.PENDING_NEW],
|
status__in=[SSFEventStatus.PENDING_FAILED, SSFEventStatus.PENDING_NEW],
|
||||||
).exists()
|
).exists()
|
||||||
and stream.status == StreamStatus.DISABLED
|
and stream.status == StreamStatus.DISABLED_DELETED
|
||||||
):
|
):
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
"Deleting inactive stream as all pending messages were sent.", stream=stream
|
"Deleting inactive stream as all pending messages were sent.", stream=stream
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class TestSSFAuth(APITestCase):
|
|||||||
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
|
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
event.payload["events"],
|
event.payload["events"],
|
||||||
{"https://schemas.openid.net/secevent/ssf/event-type/verification": {"state": None}},
|
{"https://schemas.openid.net/secevent/ssf/event-type/verification": {}},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_stream_add_oidc(self):
|
def test_stream_add_oidc(self):
|
||||||
@@ -115,7 +115,7 @@ class TestSSFAuth(APITestCase):
|
|||||||
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
|
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
event.payload["events"],
|
event.payload["events"],
|
||||||
{"https://schemas.openid.net/secevent/ssf/event-type/verification": {"state": None}},
|
{"https://schemas.openid.net/secevent/ssf/event-type/verification": {}},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_token_invalid(self):
|
def test_token_invalid(self):
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class TestStream(APITestCase):
|
|||||||
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
|
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
event.payload["events"],
|
event.payload["events"],
|
||||||
{"https://schemas.openid.net/secevent/ssf/event-type/verification": {"state": None}},
|
{"https://schemas.openid.net/secevent/ssf/event-type/verification": {}},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_stream_add_poll(self):
|
def test_stream_add_poll(self):
|
||||||
@@ -96,7 +96,7 @@ class TestStream(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(res.status_code, 204)
|
self.assertEqual(res.status_code, 204)
|
||||||
stream.refresh_from_db()
|
stream.refresh_from_db()
|
||||||
self.assertEqual(stream.status, StreamStatus.DISABLED)
|
self.assertEqual(stream.status, StreamStatus.DISABLED_DELETED)
|
||||||
|
|
||||||
def test_stream_get(self):
|
def test_stream_get(self):
|
||||||
"""get stream"""
|
"""get stream"""
|
||||||
@@ -225,3 +225,26 @@ class TestStream(APITestCase):
|
|||||||
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
|
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
|
||||||
)
|
)
|
||||||
self.assertEqual(res.status_code, 404)
|
self.assertEqual(res.status_code, 404)
|
||||||
|
|
||||||
|
def test_stream_status_update(self):
|
||||||
|
stream = Stream.objects.create(provider=self.provider)
|
||||||
|
res = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"authentik_providers_ssf:stream-status",
|
||||||
|
kwargs={"application_slug": self.application.slug},
|
||||||
|
),
|
||||||
|
data={
|
||||||
|
"stream_id": str(stream.pk),
|
||||||
|
"status": StreamStatus.DISABLED,
|
||||||
|
},
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
stream.refresh_from_db()
|
||||||
|
self.assertJSONEqual(
|
||||||
|
res.content,
|
||||||
|
{
|
||||||
|
"stream_id": str(stream.pk),
|
||||||
|
"status": str(stream.status),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class TestTasks(APITestCase):
|
|||||||
)
|
)
|
||||||
event_data = stream.prepare_event_payload(
|
event_data = stream.prepare_event_payload(
|
||||||
EventTypes.SET_VERIFICATION,
|
EventTypes.SET_VERIFICATION,
|
||||||
{"state": None},
|
{},
|
||||||
sub_id={"format": "opaque", "id": str(stream.uuid)},
|
sub_id={"format": "opaque", "id": str(stream.uuid)},
|
||||||
)
|
)
|
||||||
with Mocker() as mocker:
|
with Mocker() as mocker:
|
||||||
@@ -46,7 +46,7 @@ class TestTasks(APITestCase):
|
|||||||
)
|
)
|
||||||
jwt = decode_complete(mocker.request_history[0].body, options={"verify_signature": False})
|
jwt = decode_complete(mocker.request_history[0].body, options={"verify_signature": False})
|
||||||
self.assertEqual(jwt["header"]["typ"], "secevent+jwt")
|
self.assertEqual(jwt["header"]["typ"], "secevent+jwt")
|
||||||
self.assertIsNone(jwt["payload"]["events"][EventTypes.SET_VERIFICATION]["state"])
|
self.assertEqual(jwt["payload"]["events"][EventTypes.SET_VERIFICATION], {})
|
||||||
|
|
||||||
def test_push_auth(self):
|
def test_push_auth(self):
|
||||||
auth = generate_id()
|
auth = generate_id()
|
||||||
@@ -58,7 +58,7 @@ class TestTasks(APITestCase):
|
|||||||
)
|
)
|
||||||
event_data = stream.prepare_event_payload(
|
event_data = stream.prepare_event_payload(
|
||||||
EventTypes.SET_VERIFICATION,
|
EventTypes.SET_VERIFICATION,
|
||||||
{"state": None},
|
{},
|
||||||
sub_id={"format": "opaque", "id": str(stream.uuid)},
|
sub_id={"format": "opaque", "id": str(stream.uuid)},
|
||||||
)
|
)
|
||||||
with Mocker() as mocker:
|
with Mocker() as mocker:
|
||||||
@@ -72,7 +72,7 @@ class TestTasks(APITestCase):
|
|||||||
)
|
)
|
||||||
jwt = decode_complete(mocker.request_history[0].body, options={"verify_signature": False})
|
jwt = decode_complete(mocker.request_history[0].body, options={"verify_signature": False})
|
||||||
self.assertEqual(jwt["header"]["typ"], "secevent+jwt")
|
self.assertEqual(jwt["header"]["typ"], "secevent+jwt")
|
||||||
self.assertIsNone(jwt["payload"]["events"][EventTypes.SET_VERIFICATION]["state"])
|
self.assertEqual(jwt["payload"]["events"][EventTypes.SET_VERIFICATION], {})
|
||||||
|
|
||||||
def test_push_stream_disable(self):
|
def test_push_stream_disable(self):
|
||||||
auth = generate_id()
|
auth = generate_id()
|
||||||
@@ -81,11 +81,11 @@ class TestTasks(APITestCase):
|
|||||||
delivery_method=DeliveryMethods.RFC_PUSH,
|
delivery_method=DeliveryMethods.RFC_PUSH,
|
||||||
endpoint_url="http://localhost/ssf-push",
|
endpoint_url="http://localhost/ssf-push",
|
||||||
authorization_header=auth,
|
authorization_header=auth,
|
||||||
status=StreamStatus.DISABLED,
|
status=StreamStatus.DISABLED_DELETED,
|
||||||
)
|
)
|
||||||
event_data = stream.prepare_event_payload(
|
event_data = stream.prepare_event_payload(
|
||||||
EventTypes.SET_VERIFICATION,
|
EventTypes.SET_VERIFICATION,
|
||||||
{"state": None},
|
{},
|
||||||
sub_id={"format": "opaque", "id": str(stream.uuid)},
|
sub_id={"format": "opaque", "id": str(stream.uuid)},
|
||||||
)
|
)
|
||||||
with Mocker() as mocker:
|
with Mocker() as mocker:
|
||||||
@@ -95,7 +95,7 @@ class TestTasks(APITestCase):
|
|||||||
).get_result(block=True, timeout=1)
|
).get_result(block=True, timeout=1)
|
||||||
jwt = decode_complete(mocker.request_history[0].body, options={"verify_signature": False})
|
jwt = decode_complete(mocker.request_history[0].body, options={"verify_signature": False})
|
||||||
self.assertEqual(jwt["header"]["typ"], "secevent+jwt")
|
self.assertEqual(jwt["header"]["typ"], "secevent+jwt")
|
||||||
self.assertIsNone(jwt["payload"]["events"][EventTypes.SET_VERIFICATION]["state"])
|
self.assertEqual(jwt["payload"]["events"][EventTypes.SET_VERIFICATION], {})
|
||||||
self.assertFalse(Stream.objects.filter(pk=stream.pk).exists())
|
self.assertFalse(Stream.objects.filter(pk=stream.pk).exists())
|
||||||
|
|
||||||
def test_push_error(self):
|
def test_push_error(self):
|
||||||
@@ -106,7 +106,7 @@ class TestTasks(APITestCase):
|
|||||||
)
|
)
|
||||||
event_data = stream.prepare_event_payload(
|
event_data = stream.prepare_event_payload(
|
||||||
EventTypes.SET_VERIFICATION,
|
EventTypes.SET_VERIFICATION,
|
||||||
{"state": None},
|
{},
|
||||||
sub_id={"format": "opaque", "id": str(stream.uuid)},
|
sub_id={"format": "opaque", "id": str(stream.uuid)},
|
||||||
)
|
)
|
||||||
with Mocker() as mocker:
|
with Mocker() as mocker:
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ class SSFView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class SSFStreamView(SSFView):
|
class SSFStreamView(SSFView):
|
||||||
def get_object(self, any_status=False) -> Stream:
|
def get_object(self) -> Stream:
|
||||||
streams = Stream.objects.filter(provider=self.provider)
|
streams = Stream.objects.filter(provider=self.provider).exclude(
|
||||||
if not any_status:
|
status=StreamStatus.DISABLED_DELETED
|
||||||
streams = streams.filter(status__in=[StreamStatus.ENABLED, StreamStatus.PAUSED])
|
)
|
||||||
if "stream_id" in self.request.query_params:
|
if "stream_id" in self.request.query_params:
|
||||||
streams = streams.filter(pk=self.request.query_params["stream_id"])
|
streams = streams.filter(pk=self.request.query_params["stream_id"])
|
||||||
if "stream_id" in self.request.data:
|
if "stream_id" in self.request.data:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import Http404, HttpRequest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||||
from rest_framework.fields import CharField, ChoiceField, ListField, SerializerMethodField
|
from rest_framework.fields import CharField, ChoiceField, ListField, SerializerMethodField
|
||||||
@@ -106,7 +106,11 @@ class StreamResponseSerializer(PassiveSerializer):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_events_supported(self, instance: Stream) -> list[str]:
|
def get_events_supported(self, instance: Stream) -> list[str]:
|
||||||
return [x.value for x in EventTypes]
|
return [
|
||||||
|
EventTypes.CAEP_SESSION_REVOKED,
|
||||||
|
EventTypes.CAEP_CREDENTIAL_CHANGE,
|
||||||
|
EventTypes.SET_VERIFICATION,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class StreamView(SSFStreamView):
|
class StreamView(SSFStreamView):
|
||||||
@@ -128,10 +132,9 @@ class StreamView(SSFStreamView):
|
|||||||
LOGGER.info("Sending verification event", stream=instance)
|
LOGGER.info("Sending verification event", stream=instance)
|
||||||
send_ssf_events(
|
send_ssf_events(
|
||||||
EventTypes.SET_VERIFICATION,
|
EventTypes.SET_VERIFICATION,
|
||||||
{
|
{},
|
||||||
"state": None,
|
|
||||||
},
|
|
||||||
stream_filter={"pk": instance.uuid},
|
stream_filter={"pk": instance.uuid},
|
||||||
|
request=request,
|
||||||
sub_id={"format": "opaque", "id": str(instance.uuid)},
|
sub_id={"format": "opaque", "id": str(instance.uuid)},
|
||||||
)
|
)
|
||||||
response = StreamResponseSerializer(instance=instance, context={"request": request}).data
|
response = StreamResponseSerializer(instance=instance, context={"request": request}).data
|
||||||
@@ -159,7 +162,9 @@ class StreamView(SSFStreamView):
|
|||||||
|
|
||||||
def delete(self, request: Request, *args, **kwargs) -> Response:
|
def delete(self, request: Request, *args, **kwargs) -> Response:
|
||||||
stream = self.get_object()
|
stream = self.get_object()
|
||||||
stream.status = StreamStatus.DISABLED
|
if stream.status == StreamStatus.DISABLED_DELETED:
|
||||||
|
raise Http404
|
||||||
|
stream.status = StreamStatus.DISABLED_DELETED
|
||||||
stream.save()
|
stream.save()
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
@@ -175,6 +180,7 @@ class StreamVerifyView(SSFStreamView):
|
|||||||
"state": state,
|
"state": state,
|
||||||
},
|
},
|
||||||
stream_filter={"pk": stream.uuid},
|
stream_filter={"pk": stream.uuid},
|
||||||
|
request=request,
|
||||||
sub_id={"format": "opaque", "id": str(stream.uuid)},
|
sub_id={"format": "opaque", "id": str(stream.uuid)},
|
||||||
)
|
)
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
@@ -182,8 +188,25 @@ class StreamVerifyView(SSFStreamView):
|
|||||||
|
|
||||||
class StreamStatusView(SSFStreamView):
|
class StreamStatusView(SSFStreamView):
|
||||||
|
|
||||||
|
class StreamStatusSerializer(PassiveSerializer):
|
||||||
|
stream_id = CharField()
|
||||||
|
status = ChoiceField(choices=StreamStatus.choices)
|
||||||
|
|
||||||
def get(self, request: Request, *args, **kwargs):
|
def get(self, request: Request, *args, **kwargs):
|
||||||
stream = self.get_object(any_status=True)
|
stream = self.get_object()
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"stream_id": str(stream.pk),
|
||||||
|
"status": str(stream.status),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def post(self, request: Request, *args, **kwargs):
|
||||||
|
stream = self.get_object()
|
||||||
|
serializer = self.StreamStatusSerializer(stream, data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
stream.status = serializer.validated_data["status"]
|
||||||
|
stream.save()
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"stream_id": str(stream.pk),
|
"stream_id": str(stream.pk),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ TENANT_APPS = [
|
|||||||
"authentik.enterprise.providers.ssf",
|
"authentik.enterprise.providers.ssf",
|
||||||
"authentik.enterprise.providers.ws_federation",
|
"authentik.enterprise.providers.ws_federation",
|
||||||
"authentik.enterprise.reports",
|
"authentik.enterprise.reports",
|
||||||
|
"authentik.enterprise.stages.account_lockdown",
|
||||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||||
"authentik.enterprise.stages.mtls",
|
"authentik.enterprise.stages.mtls",
|
||||||
"authentik.enterprise.stages.source",
|
"authentik.enterprise.stages.source",
|
||||||
|
|||||||
141
authentik/enterprise/stages/account_lockdown/api.py
Normal file
141
authentik/enterprise/stages/account_lockdown/api.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
"""Account Lockdown Stage API Views"""
|
||||||
|
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from drf_spectacular.utils import OpenApiExample, OpenApiResponse, extend_schema
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.serializers import PrimaryKeyRelatedField
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.api.validation import validate
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
from authentik.core.api.utils import LinkSerializer, PassiveSerializer
|
||||||
|
from authentik.core.models import (
|
||||||
|
User,
|
||||||
|
)
|
||||||
|
from authentik.enterprise.api import EnterpriseRequiredMixin, enterprise_action
|
||||||
|
from authentik.enterprise.stages.account_lockdown.models import AccountLockdownStage
|
||||||
|
from authentik.enterprise.stages.account_lockdown.stage import (
|
||||||
|
can_lock_user,
|
||||||
|
get_lockdown_target_users,
|
||||||
|
)
|
||||||
|
from authentik.flows.api.stages import StageSerializer
|
||||||
|
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLockdownStageSerializer(EnterpriseRequiredMixin, StageSerializer):
|
||||||
|
"""AccountLockdownStage Serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AccountLockdownStage
|
||||||
|
fields = StageSerializer.Meta.fields + [
|
||||||
|
"deactivate_user",
|
||||||
|
"set_unusable_password",
|
||||||
|
"delete_sessions",
|
||||||
|
"revoke_tokens",
|
||||||
|
"self_service_completion_flow",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLockdownStageViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
"""AccountLockdownStage Viewset"""
|
||||||
|
|
||||||
|
queryset = AccountLockdownStage.objects.all()
|
||||||
|
serializer_class = AccountLockdownStageSerializer
|
||||||
|
filterset_fields = "__all__"
|
||||||
|
ordering = ["name"]
|
||||||
|
search_fields = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
class UserAccountLockdownSerializer(PassiveSerializer):
|
||||||
|
"""Choose the target account before starting the lockdown flow."""
|
||||||
|
|
||||||
|
user = PrimaryKeyRelatedField(
|
||||||
|
queryset=get_lockdown_target_users(),
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
help_text=_("User to lock. If omitted, locks the current user (self-service)."),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAccountLockdownMixin:
|
||||||
|
"""Enterprise account-lockdown API actions for UserViewSet."""
|
||||||
|
|
||||||
|
def _create_lockdown_flow_url(self, request: Request, user: User) -> str:
|
||||||
|
"""Create a flow URL for account lockdown.
|
||||||
|
|
||||||
|
The request body selects the target before the flow starts. The API
|
||||||
|
pre-plans the lockdown flow with the target as the pending user, so the
|
||||||
|
account lockdown stage can use the normal flow context.
|
||||||
|
"""
|
||||||
|
flow = request._request.brand.flow_lockdown
|
||||||
|
if flow is None:
|
||||||
|
raise ValidationError({"non_field_errors": [_("No lockdown flow configured.")]})
|
||||||
|
planner = FlowPlanner(flow)
|
||||||
|
planner.use_cache = False
|
||||||
|
try:
|
||||||
|
plan = planner.plan(request._request, {PLAN_CONTEXT_PENDING_USER: user})
|
||||||
|
except EmptyFlowException, FlowNonApplicableException:
|
||||||
|
raise ValidationError(
|
||||||
|
{"non_field_errors": [_("Lockdown flow is not applicable.")]}
|
||||||
|
) from None
|
||||||
|
return plan.to_redirect(request._request, flow).url
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
description=_("Choose the target account, then return a flow link."),
|
||||||
|
request=UserAccountLockdownSerializer,
|
||||||
|
responses={
|
||||||
|
"200": OpenApiResponse(
|
||||||
|
response=LinkSerializer,
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
"Lockdown flow URL",
|
||||||
|
value={
|
||||||
|
"link": "https://example.invalid/if/flow/default-account-lockdown/",
|
||||||
|
},
|
||||||
|
response_only=True,
|
||||||
|
status_codes=["200"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"400": OpenApiResponse(
|
||||||
|
description=_("No lockdown flow configured or the flow is not applicable")
|
||||||
|
),
|
||||||
|
"403": OpenApiResponse(
|
||||||
|
description=_("Permission denied (when targeting another user)")
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(
|
||||||
|
detail=False,
|
||||||
|
methods=["POST"],
|
||||||
|
permission_classes=[IsAuthenticated],
|
||||||
|
url_path="account_lockdown",
|
||||||
|
)
|
||||||
|
@validate(UserAccountLockdownSerializer)
|
||||||
|
@enterprise_action
|
||||||
|
def account_lockdown(self, request: Request, body: UserAccountLockdownSerializer) -> Response:
|
||||||
|
"""Trigger account lockdown for a user.
|
||||||
|
|
||||||
|
If no user is specified, locks the current user (self-service).
|
||||||
|
When targeting another user, admin permissions are required.
|
||||||
|
|
||||||
|
Returns a flow link for the frontend to follow. The flow is pre-planned
|
||||||
|
with the target user as pending user for the lockdown stage.
|
||||||
|
"""
|
||||||
|
user = body.validated_data.get("user") or request.user
|
||||||
|
|
||||||
|
if not can_lock_user(request.user, user):
|
||||||
|
LOGGER.debug("Permission denied for account lockdown", user=request.user)
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
flow_url = self._create_lockdown_flow_url(request, user)
|
||||||
|
LOGGER.debug("Returning lockdown flow URL", flow_url=flow_url, user=user.username)
|
||||||
|
return Response({"link": flow_url})
|
||||||
12
authentik/enterprise/stages/account_lockdown/apps.py
Normal file
12
authentik/enterprise/stages/account_lockdown/apps.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"""authentik account lockdown stage app config"""
|
||||||
|
|
||||||
|
from authentik.enterprise.apps import EnterpriseConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AuthentikEnterpriseStageAccountLockdownConfig(EnterpriseConfig):
|
||||||
|
"""authentik account lockdown stage config"""
|
||||||
|
|
||||||
|
name = "authentik.enterprise.stages.account_lockdown"
|
||||||
|
label = "authentik_stages_account_lockdown"
|
||||||
|
verbose_name = "authentik Enterprise.Stages.Account Lockdown"
|
||||||
|
default = True
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Generated by Django 5.2.13 on 2026-04-19 21:56
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_flows", "0031_alter_flow_layout"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="AccountLockdownStage",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"stage_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="authentik_flows.stage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deactivate_user",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Deactivate the user account (set is_active to False)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"set_unusable_password",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True, help_text="Set an unusable password for the user"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"delete_sessions",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True, help_text="Delete all active sessions for the user"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"revoke_tokens",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Revoke all tokens for the user (API, app password, recovery, verification, OAuth)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"self_service_completion_flow",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Flow to redirect users to after self-service lockdown. This flow should not require authentication since the user's session is deleted.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="account_lockdown_stages",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Account Lockdown Stage",
|
||||||
|
"verbose_name_plural": "Account Lockdown Stages",
|
||||||
|
},
|
||||||
|
bases=("authentik_flows.stage",),
|
||||||
|
),
|
||||||
|
]
|
||||||
62
authentik/enterprise/stages/account_lockdown/models.py
Normal file
62
authentik/enterprise/stages/account_lockdown/models.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"""Account lockdown stage models"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views import View
|
||||||
|
from rest_framework.serializers import BaseSerializer
|
||||||
|
|
||||||
|
from authentik.flows.models import Stage
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLockdownStage(Stage):
|
||||||
|
"""Lock down a target user account."""
|
||||||
|
|
||||||
|
deactivate_user = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text=_("Deactivate the user account (set is_active to False)"),
|
||||||
|
)
|
||||||
|
set_unusable_password = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text=_("Set an unusable password for the user"),
|
||||||
|
)
|
||||||
|
delete_sessions = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text=_("Delete all active sessions for the user"),
|
||||||
|
)
|
||||||
|
revoke_tokens = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text=_(
|
||||||
|
"Revoke all tokens for the user (API, app password, recovery, verification, OAuth)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self_service_completion_flow = models.ForeignKey(
|
||||||
|
"authentik_flows.Flow",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="account_lockdown_stages",
|
||||||
|
help_text=_(
|
||||||
|
"Flow to redirect users to after self-service lockdown. "
|
||||||
|
"This flow should not require authentication since the user's session is deleted."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serializer(self) -> type[BaseSerializer]:
|
||||||
|
from authentik.enterprise.stages.account_lockdown.api import AccountLockdownStageSerializer
|
||||||
|
|
||||||
|
return AccountLockdownStageSerializer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def view(self) -> type[View]:
|
||||||
|
from authentik.enterprise.stages.account_lockdown.stage import AccountLockdownStageView
|
||||||
|
|
||||||
|
return AccountLockdownStageView
|
||||||
|
|
||||||
|
@property
|
||||||
|
def component(self) -> str:
|
||||||
|
return "ak-stage-account-lockdown-form"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Account Lockdown Stage")
|
||||||
|
verbose_name_plural = _("Account Lockdown Stages")
|
||||||
345
authentik/enterprise/stages/account_lockdown/stage.py
Normal file
345
authentik/enterprise/stages/account_lockdown/stage.py
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
"""Account lockdown stage logic"""
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
|
from django.db.models import Model, QuerySet
|
||||||
|
from django.db.models.query_utils import Q
|
||||||
|
from django.db.transaction import atomic
|
||||||
|
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from dramatiq.actor import Actor
|
||||||
|
from dramatiq.composition import group
|
||||||
|
from dramatiq.results.errors import ResultTimeout
|
||||||
|
|
||||||
|
from authentik.core.models import (
|
||||||
|
AuthenticatedSession,
|
||||||
|
ExpiringModel,
|
||||||
|
Session,
|
||||||
|
Token,
|
||||||
|
User,
|
||||||
|
UserTypes,
|
||||||
|
)
|
||||||
|
from authentik.enterprise.stages.account_lockdown.models import AccountLockdownStage
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.flows.stage import StageView
|
||||||
|
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
|
||||||
|
from authentik.lib.sync.outgoing.signals import sync_outgoing_inhibit_dispatch
|
||||||
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
|
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
|
|
||||||
|
PLAN_CONTEXT_LOCKDOWN_REASON = "lockdown_reason"
|
||||||
|
LOCKDOWN_EVENT_ACTION_ID = "account_lockdown"
|
||||||
|
|
||||||
|
TARGET_REQUIRED_MESSAGE = _("No target user specified for account lockdown")
|
||||||
|
PERMISSION_DENIED_MESSAGE = _("You do not have permission to lock down this account.")
|
||||||
|
ACCOUNT_LOCKDOWN_FAILED_MESSAGE = _("Account lockdown failed for this account.")
|
||||||
|
SELF_SERVICE_COMPLETION_FLOW_REQUIRED_MESSAGE = _(
|
||||||
|
"Self-service account lockdown requires a completion flow."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_lockdown_target_users() -> QuerySet[User]:
|
||||||
|
"""Return users that can be targeted by account lockdown."""
|
||||||
|
return User.objects.exclude_anonymous().exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_model_field(model: type[Model], field_name: str):
|
||||||
|
"""Get a model field by name, if present."""
|
||||||
|
try:
|
||||||
|
return model._meta.get_field(field_name)
|
||||||
|
except FieldDoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_user_field(model: type[Model]) -> bool:
|
||||||
|
"""Check if a model has a direct user foreign key."""
|
||||||
|
field = _get_model_field(model, "user")
|
||||||
|
return bool(field and getattr(field, "remote_field", None) and field.remote_field.model is User)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_authenticated_session_field(model: type[Model]) -> bool:
|
||||||
|
"""Check if a model is linked to an authenticated session."""
|
||||||
|
field = _get_model_field(model, "session")
|
||||||
|
return bool(
|
||||||
|
field
|
||||||
|
and getattr(field, "remote_field", None)
|
||||||
|
and field.remote_field.model is AuthenticatedSession
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_provider_field(model: type[Model]) -> bool:
|
||||||
|
"""Check if a model is linked to a provider."""
|
||||||
|
return _get_model_field(model, "provider") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def get_lockdown_token_models() -> tuple[type[Model], ...]:
|
||||||
|
"""Return token, grant, and provider session models removed by account lockdown."""
|
||||||
|
token_models: list[type[Model]] = []
|
||||||
|
for model in apps.get_models():
|
||||||
|
if model._meta.abstract or not issubclass(model, ExpiringModel):
|
||||||
|
continue
|
||||||
|
if model is Token:
|
||||||
|
token_models.append(model)
|
||||||
|
elif _has_user_field(model) and (
|
||||||
|
_has_provider_field(model) or _has_authenticated_session_field(model)
|
||||||
|
):
|
||||||
|
token_models.append(model)
|
||||||
|
elif _has_authenticated_session_field(model):
|
||||||
|
token_models.append(model)
|
||||||
|
return tuple(token_models)
|
||||||
|
|
||||||
|
|
||||||
|
def get_lockdown_token_queryset(model: type[Model], user: User) -> QuerySet:
|
||||||
|
"""Return account lockdown artifacts for a model and user."""
|
||||||
|
manager = model.objects.including_expired()
|
||||||
|
if _has_user_field(model):
|
||||||
|
return manager.filter(user=user)
|
||||||
|
return manager.filter(session__user=user)
|
||||||
|
|
||||||
|
|
||||||
|
def can_lock_user(actor, user: User) -> bool:
|
||||||
|
"""Check whether the actor may lock the target user."""
|
||||||
|
if not actor.is_authenticated:
|
||||||
|
return False
|
||||||
|
if user.pk == actor.pk:
|
||||||
|
return True
|
||||||
|
return actor.has_perm("authentik_core.change_user", user)
|
||||||
|
|
||||||
|
|
||||||
|
def get_outgoing_sync_tasks() -> tuple[tuple[type[OutgoingSyncProvider], Actor], ...]:
|
||||||
|
"""Return outgoing sync provider types and their direct sync tasks."""
|
||||||
|
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
|
||||||
|
from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync_direct
|
||||||
|
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
|
||||||
|
from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync_direct
|
||||||
|
from authentik.providers.scim.models import SCIMProvider
|
||||||
|
from authentik.providers.scim.tasks import scim_sync_direct
|
||||||
|
|
||||||
|
return (
|
||||||
|
(SCIMProvider, scim_sync_direct),
|
||||||
|
(GoogleWorkspaceProvider, google_workspace_sync_direct),
|
||||||
|
(MicrosoftEntraProvider, microsoft_entra_sync_direct),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLockdownStageView(StageView):
|
||||||
|
"""Execute account lockdown actions on the target user."""
|
||||||
|
|
||||||
|
def is_self_service(self, request: HttpRequest, user: User) -> bool:
|
||||||
|
"""Check whether the currently authenticated user is locking their own account."""
|
||||||
|
return request.user.is_authenticated and user.pk == request.user.pk
|
||||||
|
|
||||||
|
def get_reason(self) -> str:
|
||||||
|
"""Get the lockdown reason from the plan context.
|
||||||
|
|
||||||
|
Priority:
|
||||||
|
1. prompt_data[PLAN_CONTEXT_LOCKDOWN_REASON]
|
||||||
|
2. PLAN_CONTEXT_LOCKDOWN_REASON (explicitly set)
|
||||||
|
3. Empty string as fallback
|
||||||
|
"""
|
||||||
|
prompt_data = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {})
|
||||||
|
if PLAN_CONTEXT_LOCKDOWN_REASON in prompt_data:
|
||||||
|
return prompt_data[PLAN_CONTEXT_LOCKDOWN_REASON]
|
||||||
|
return self.executor.plan.context.get(PLAN_CONTEXT_LOCKDOWN_REASON, "")
|
||||||
|
|
||||||
|
def _apply_lockdown_actions(self, stage: AccountLockdownStage, user: User) -> None:
|
||||||
|
"""Apply the configured account changes to the target user."""
|
||||||
|
if stage.deactivate_user:
|
||||||
|
user.is_active = False
|
||||||
|
if stage.set_unusable_password:
|
||||||
|
user.set_unusable_password()
|
||||||
|
if stage.deactivate_user:
|
||||||
|
with sync_outgoing_inhibit_dispatch():
|
||||||
|
user.save()
|
||||||
|
return
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
def _sync_deactivated_user_to_outgoing_providers(self, user: User) -> None:
|
||||||
|
"""Synchronize a deactivated user to outgoing sync providers."""
|
||||||
|
messages = []
|
||||||
|
wait_timeout = 0
|
||||||
|
model = class_to_path(User)
|
||||||
|
provider_filter = Q(backchannel_application__isnull=False) | Q(application__isnull=False)
|
||||||
|
|
||||||
|
for provider_model, task_sync_direct in get_outgoing_sync_tasks():
|
||||||
|
for provider in provider_model.objects.filter(provider_filter):
|
||||||
|
time_limit = int(
|
||||||
|
timedelta_from_string(provider.sync_page_timeout).total_seconds() * 1000
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
task_sync_direct.message_with_options(
|
||||||
|
args=(model, user.pk, provider.pk),
|
||||||
|
rel_obj=provider,
|
||||||
|
time_limit=time_limit,
|
||||||
|
uid=f"{provider.name}:user:{user.pk}:direct",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
wait_timeout += time_limit
|
||||||
|
|
||||||
|
if not messages:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
group(messages).run().wait(timeout=wait_timeout)
|
||||||
|
except ResultTimeout:
|
||||||
|
self.logger.warning(
|
||||||
|
"Timed out waiting for outgoing sync tasks; tasks remain queued",
|
||||||
|
user=user.username,
|
||||||
|
timeout=wait_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_lockdown_artifact_querysets(
|
||||||
|
self, stage: AccountLockdownStage, user: User
|
||||||
|
) -> tuple[QuerySet, ...]:
|
||||||
|
"""Return the configured sessions and tokens targeted by lockdown."""
|
||||||
|
querysets: list[QuerySet] = []
|
||||||
|
if stage.delete_sessions:
|
||||||
|
querysets.append(Session.objects.filter(authenticatedsession__user=user))
|
||||||
|
if stage.revoke_tokens:
|
||||||
|
querysets.extend(
|
||||||
|
get_lockdown_token_queryset(model, user) for model in get_lockdown_token_models()
|
||||||
|
)
|
||||||
|
return tuple(querysets)
|
||||||
|
|
||||||
|
def _delete_lockdown_artifacts(self, stage: AccountLockdownStage, user: User) -> None:
|
||||||
|
"""Delete sessions and tokens selected by the lockdown configuration."""
|
||||||
|
for queryset in self._get_lockdown_artifact_querysets(stage, user):
|
||||||
|
queryset.delete()
|
||||||
|
|
||||||
|
def _has_lockdown_artifacts(self, stage: AccountLockdownStage, user: User) -> bool:
|
||||||
|
"""Check whether there are still sessions or tokens to remove."""
|
||||||
|
return any(
|
||||||
|
queryset.exists() for queryset in self._get_lockdown_artifact_querysets(stage, user)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _emit_lockdown_event(self, request: HttpRequest, user: User, reason: str) -> None:
|
||||||
|
"""Emit the audit event for a completed lockdown."""
|
||||||
|
# Emit the audit event after the transaction commits. If event creation
|
||||||
|
# fails here, dispatch() would otherwise treat the whole lockdown as
|
||||||
|
# failed even though the account changes have already been committed.
|
||||||
|
try:
|
||||||
|
Event.new(
|
||||||
|
EventAction.USER_WRITE,
|
||||||
|
action_id=LOCKDOWN_EVENT_ACTION_ID,
|
||||||
|
reason=reason,
|
||||||
|
affected_user=user.username,
|
||||||
|
).from_http(request)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
# Event emission should not make the lockdown itself fail.
|
||||||
|
self.logger.warning(
|
||||||
|
"Failed to emit account lockdown event",
|
||||||
|
user=user.username,
|
||||||
|
exc=exc,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _lockdown_user(
|
||||||
|
self,
|
||||||
|
request: HttpRequest,
|
||||||
|
stage: AccountLockdownStage,
|
||||||
|
user: User,
|
||||||
|
reason: str,
|
||||||
|
) -> None:
|
||||||
|
"""Execute lockdown actions on a single user."""
|
||||||
|
with atomic():
|
||||||
|
user = User.objects.get(pk=user.pk)
|
||||||
|
self._apply_lockdown_actions(stage, user)
|
||||||
|
self._delete_lockdown_artifacts(stage, user)
|
||||||
|
|
||||||
|
# These additional checks/deletes are done to prevent a timing attack that creates tokens
|
||||||
|
# with a compromised token that is simultaneously being deleted.
|
||||||
|
while self._has_lockdown_artifacts(stage, user):
|
||||||
|
with atomic():
|
||||||
|
self._delete_lockdown_artifacts(stage, user)
|
||||||
|
|
||||||
|
if stage.deactivate_user:
|
||||||
|
try:
|
||||||
|
self._sync_deactivated_user_to_outgoing_providers(user)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
# Local lockdown has already committed. Provider sync failures
|
||||||
|
# must not reopen access or mark the lockdown itself as failed.
|
||||||
|
self.logger.warning(
|
||||||
|
"Failed to sync account lockdown deactivation to outgoing providers",
|
||||||
|
user=user.username,
|
||||||
|
exc=exc,
|
||||||
|
)
|
||||||
|
self._emit_lockdown_event(request, user, reason)
|
||||||
|
|
||||||
|
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
"""Execute account lockdown actions."""
|
||||||
|
self.request = request
|
||||||
|
stage: AccountLockdownStage = self.executor.current_stage
|
||||||
|
|
||||||
|
pending_user = self.get_pending_user()
|
||||||
|
if not pending_user.is_authenticated:
|
||||||
|
self.logger.warning("No target user found for account lockdown")
|
||||||
|
return self.executor.stage_invalid(TARGET_REQUIRED_MESSAGE)
|
||||||
|
user = get_lockdown_target_users().filter(pk=pending_user.pk).first()
|
||||||
|
if user is None:
|
||||||
|
self.logger.warning("Target user is not eligible for account lockdown")
|
||||||
|
return self.executor.stage_invalid(TARGET_REQUIRED_MESSAGE)
|
||||||
|
if not can_lock_user(request.user, user):
|
||||||
|
self.logger.warning(
|
||||||
|
"Permission denied for account lockdown",
|
||||||
|
actor=getattr(request.user, "username", None),
|
||||||
|
target=user.username,
|
||||||
|
)
|
||||||
|
return self.executor.stage_invalid(PERMISSION_DENIED_MESSAGE)
|
||||||
|
|
||||||
|
reason = self.get_reason()
|
||||||
|
self_service = self.is_self_service(request, user)
|
||||||
|
if self_service and stage.delete_sessions and not stage.self_service_completion_flow:
|
||||||
|
self.logger.warning("No completion flow configured for self-service account lockdown")
|
||||||
|
return self.executor.stage_invalid(SELF_SERVICE_COMPLETION_FLOW_REQUIRED_MESSAGE)
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
"Executing account lockdown",
|
||||||
|
user=user.username,
|
||||||
|
reason=reason,
|
||||||
|
self_service=self_service,
|
||||||
|
deactivate_user=stage.deactivate_user,
|
||||||
|
set_unusable_password=stage.set_unusable_password,
|
||||||
|
delete_sessions=stage.delete_sessions,
|
||||||
|
revoke_tokens=stage.revoke_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._lockdown_user(request, stage, user, reason)
|
||||||
|
self.logger.info("Account lockdown completed", user=user.username)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
# Convert unexpected lockdown errors to a flow-stage failure instead
|
||||||
|
# of leaking an exception through the flow executor.
|
||||||
|
self.logger.warning("Account lockdown failed", user=user.username, exc=exc)
|
||||||
|
return self.executor.stage_invalid(ACCOUNT_LOCKDOWN_FAILED_MESSAGE)
|
||||||
|
|
||||||
|
if self_service:
|
||||||
|
if stage.delete_sessions:
|
||||||
|
return self._self_service_completion_response(request)
|
||||||
|
return self.executor.stage_ok()
|
||||||
|
|
||||||
|
return self.executor.stage_ok()
|
||||||
|
|
||||||
|
def _self_service_completion_response(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
"""Redirect to completion flow after self-service lockdown.
|
||||||
|
|
||||||
|
Since all sessions are deleted, the user cannot continue in the flow.
|
||||||
|
Redirect them to an unauthenticated completion flow that shows the
|
||||||
|
lockdown message.
|
||||||
|
|
||||||
|
We use a direct HTTP redirect instead of a challenge because the
|
||||||
|
flow executor's challenge handling may try to access the session
|
||||||
|
which we just deleted.
|
||||||
|
"""
|
||||||
|
stage: AccountLockdownStage = self.executor.current_stage
|
||||||
|
completion_flow = stage.self_service_completion_flow
|
||||||
|
if completion_flow:
|
||||||
|
# Flush the current request's session to prevent Django's session
|
||||||
|
# middleware from trying to save a deleted session
|
||||||
|
if hasattr(request, "session"):
|
||||||
|
request.session.flush()
|
||||||
|
redirect_to = reverse(
|
||||||
|
"authentik_core:if-flow",
|
||||||
|
kwargs={"flow_slug": completion_flow.slug},
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(redirect_to)
|
||||||
|
return self.executor.stage_invalid(SELF_SERVICE_COMPLETION_FLOW_REQUIRED_MESSAGE)
|
||||||
148
authentik/enterprise/stages/account_lockdown/tests/test_api.py
Normal file
148
authentik/enterprise/stages/account_lockdown/tests/test_api.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
"""Test Users Account Lockdown API"""
|
||||||
|
|
||||||
|
from json import loads
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from authentik.core.tests.utils import (
|
||||||
|
create_test_brand,
|
||||||
|
create_test_flow,
|
||||||
|
create_test_user,
|
||||||
|
)
|
||||||
|
from authentik.enterprise.stages.account_lockdown.models import AccountLockdownStage
|
||||||
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
# Patch for enterprise license check
|
||||||
|
patch_license = patch(
|
||||||
|
"authentik.enterprise.models.LicenseUsageStatus.is_valid",
|
||||||
|
MagicMock(return_value=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch_license
|
||||||
|
class AccountLockdownAPITestCase(APITestCase):
|
||||||
|
"""Shared helpers for account lockdown API tests."""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.lockdown_flow = create_test_flow(FlowDesignation.STAGE_CONFIGURATION)
|
||||||
|
self.lockdown_stage = AccountLockdownStage.objects.create(name=generate_id())
|
||||||
|
FlowStageBinding.objects.create(
|
||||||
|
target=self.lockdown_flow,
|
||||||
|
stage=self.lockdown_stage,
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
self.brand = create_test_brand()
|
||||||
|
self.brand.flow_lockdown = self.lockdown_flow
|
||||||
|
self.brand.save()
|
||||||
|
|
||||||
|
def create_user_with_email(self):
|
||||||
|
"""Create a regular user with a unique email address."""
|
||||||
|
user = create_test_user()
|
||||||
|
user.email = f"{generate_id()}@test.com"
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
def assert_redirect_targets(self, response, user):
|
||||||
|
"""Assert that a response contains a pre-planned lockdown flow link for a user."""
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
body = loads(response.content)
|
||||||
|
self.assertIn(self.lockdown_flow.slug, body["link"])
|
||||||
|
self.assertEqual(urlparse(body["link"]).query, "")
|
||||||
|
plan = self.client.session[SESSION_KEY_PLAN]
|
||||||
|
self.assertEqual(plan.context[PLAN_CONTEXT_PENDING_USER].pk, user.pk)
|
||||||
|
|
||||||
|
def assert_no_flow_configured(self, response):
|
||||||
|
"""Assert that the API reports a missing lockdown flow."""
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
body = loads(response.content)
|
||||||
|
self.assertIn("No lockdown flow configured", body["non_field_errors"][0])
|
||||||
|
|
||||||
|
|
||||||
|
@patch_license
|
||||||
|
class TestUsersAccountLockdownAPI(AccountLockdownAPITestCase):
|
||||||
|
"""Test Users Account Lockdown API"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
self.actor = create_test_user()
|
||||||
|
self.user = self.create_user_with_email()
|
||||||
|
|
||||||
|
def test_account_lockdown_with_change_user_returns_redirect(self):
|
||||||
|
"""Test that account lockdown allows users with change_user permission."""
|
||||||
|
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.user)
|
||||||
|
self.client.force_login(self.actor)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-account-lockdown"),
|
||||||
|
data={"user": self.user.pk},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_redirect_targets(response, self.user)
|
||||||
|
|
||||||
|
def test_account_lockdown_no_flow_configured(self):
|
||||||
|
"""Test account lockdown when no flow is configured"""
|
||||||
|
self.brand.flow_lockdown = None
|
||||||
|
self.brand.save()
|
||||||
|
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.user)
|
||||||
|
self.client.force_login(self.actor)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-account-lockdown"),
|
||||||
|
data={"user": self.user.pk},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_no_flow_configured(response)
|
||||||
|
|
||||||
|
def test_account_lockdown_unauthenticated(self):
|
||||||
|
"""Test account lockdown requires authentication"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-account-lockdown"),
|
||||||
|
data={"user": self.user.pk},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_account_lockdown_without_change_user_denied(self):
|
||||||
|
"""Test account lockdown denies users without change_user permission."""
|
||||||
|
self.client.force_login(self.actor)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-account-lockdown"),
|
||||||
|
data={"user": self.user.pk},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_account_lockdown_self_returns_redirect(self):
|
||||||
|
"""Test successful self-service account lockdown returns a direct redirect."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-account-lockdown"),
|
||||||
|
data={},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_redirect_targets(response, self.user)
|
||||||
|
|
||||||
|
def test_account_lockdown_self_target_without_change_user_returns_redirect(self):
|
||||||
|
"""Test self-service does not require change_user permission."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-account-lockdown"),
|
||||||
|
data={"user": self.user.pk},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_redirect_targets(response, self.user)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Tests for the packaged account-lockdown blueprint."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
|
from authentik.blueprints.models import BlueprintInstance
|
||||||
|
from authentik.blueprints.v1.importer import Importer
|
||||||
|
from authentik.blueprints.v1.tasks import blueprints_find, check_blueprint_v1_file
|
||||||
|
from authentik.enterprise.license import LicenseKey
|
||||||
|
from authentik.flows.models import Flow
|
||||||
|
|
||||||
|
BLUEPRINT_PATH = "example/flow-default-account-lockdown.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccountLockdownBlueprint(TransactionTestCase):
|
||||||
|
"""Test the packaged account-lockdown blueprint behavior."""
|
||||||
|
|
||||||
|
def test_blueprint_is_not_auto_instantiated(self):
|
||||||
|
"""Test the packaged blueprint is opt-in and skipped by discovery."""
|
||||||
|
BlueprintInstance.objects.filter(path=BLUEPRINT_PATH).delete()
|
||||||
|
blueprint = next(item for item in blueprints_find() if item.path == BLUEPRINT_PATH)
|
||||||
|
|
||||||
|
check_blueprint_v1_file(blueprint)
|
||||||
|
|
||||||
|
self.assertFalse(BlueprintInstance.objects.filter(path=BLUEPRINT_PATH).exists())
|
||||||
|
|
||||||
|
def test_blueprint_requires_licensed_context(self):
|
||||||
|
"""Test manual import only creates flows when enterprise is licensed."""
|
||||||
|
content = BlueprintInstance(path=BLUEPRINT_PATH).retrieve()
|
||||||
|
license_key = LicenseKey("test", 253402300799, "Test license", 1000, 1000)
|
||||||
|
|
||||||
|
with patch("authentik.enterprise.license.LicenseKey.get_total", return_value=license_key):
|
||||||
|
importer = Importer.from_string(content, {"goauthentik.io/enterprise/licensed": False})
|
||||||
|
valid, logs = importer.validate()
|
||||||
|
self.assertTrue(valid, logs)
|
||||||
|
self.assertTrue(importer.apply())
|
||||||
|
self.assertFalse(Flow.objects.filter(slug="default-account-lockdown").exists())
|
||||||
|
self.assertFalse(Flow.objects.filter(slug="default-account-lockdown-complete").exists())
|
||||||
|
|
||||||
|
importer = Importer.from_string(content, {"goauthentik.io/enterprise/licensed": True})
|
||||||
|
valid, logs = importer.validate()
|
||||||
|
self.assertTrue(valid, logs)
|
||||||
|
self.assertTrue(importer.apply())
|
||||||
|
self.assertTrue(Flow.objects.filter(slug="default-account-lockdown").exists())
|
||||||
|
self.assertTrue(Flow.objects.filter(slug="default-account-lockdown-complete").exists())
|
||||||
627
authentik/enterprise/stages/account_lockdown/tests/test_stage.py
Normal file
627
authentik/enterprise/stages/account_lockdown/tests/test_stage.py
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
"""Account lockdown stage tests"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import asdict
|
||||||
|
from threading import Event as ThreadEvent
|
||||||
|
from threading import Thread
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.test import TransactionTestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from dramatiq.results.errors import ResultTimeout
|
||||||
|
|
||||||
|
from authentik.core.models import AuthenticatedSession, Session, Token, TokenIntents
|
||||||
|
from authentik.core.tests.utils import (
|
||||||
|
RequestFactory,
|
||||||
|
create_test_admin_user,
|
||||||
|
create_test_cert,
|
||||||
|
create_test_flow,
|
||||||
|
create_test_user,
|
||||||
|
)
|
||||||
|
from authentik.enterprise.stages.account_lockdown.models import AccountLockdownStage
|
||||||
|
from authentik.enterprise.stages.account_lockdown.stage import (
|
||||||
|
LOCKDOWN_EVENT_ACTION_ID,
|
||||||
|
PLAN_CONTEXT_LOCKDOWN_REASON,
|
||||||
|
AccountLockdownStageView,
|
||||||
|
can_lock_user,
|
||||||
|
)
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.flows.markers import StageMarker
|
||||||
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
|
from authentik.flows.tests import FlowTestCase
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
|
from authentik.providers.oauth2.id_token import IDToken
|
||||||
|
from authentik.providers.oauth2.models import (
|
||||||
|
AccessToken,
|
||||||
|
AuthorizationCode,
|
||||||
|
DeviceToken,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
RefreshToken,
|
||||||
|
)
|
||||||
|
from authentik.providers.saml.models import SAMLProvider, SAMLSession
|
||||||
|
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
|
|
||||||
|
patch_enterprise_enabled = patch(
|
||||||
|
"authentik.enterprise.apps.AuthentikEnterpriseConfig.check_enabled",
|
||||||
|
return_value=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLockdownStageTestMixin:
|
||||||
|
"""Shared setup helpers for account lockdown stage tests."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.patch_enterprise_enabled = patch_enterprise_enabled.start()
|
||||||
|
cls.patch_event_dispatch = patch("authentik.events.tasks.event_trigger_dispatch.send")
|
||||||
|
cls.patch_event_dispatch.start()
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
cls.patch_event_dispatch.stop()
|
||||||
|
patch_enterprise_enabled.stop()
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.user = create_test_admin_user()
|
||||||
|
self.target_user = create_test_admin_user()
|
||||||
|
self.flow = create_test_flow(FlowDesignation.STAGE_CONFIGURATION)
|
||||||
|
self.stage = AccountLockdownStage.objects.create(
|
||||||
|
name="lockdown",
|
||||||
|
)
|
||||||
|
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=0)
|
||||||
|
self.request_factory = RequestFactory()
|
||||||
|
|
||||||
|
def make_stage_view(self, plan: FlowPlan):
|
||||||
|
def _stage_ok():
|
||||||
|
return HttpResponse(status=204)
|
||||||
|
|
||||||
|
def _stage_invalid(_error_message=None):
|
||||||
|
return HttpResponse(status=400)
|
||||||
|
|
||||||
|
return AccountLockdownStageView(
|
||||||
|
SimpleNamespace(
|
||||||
|
plan=plan,
|
||||||
|
current_stage=self.stage,
|
||||||
|
current_binding=self.binding,
|
||||||
|
flow=self.flow,
|
||||||
|
stage_ok=_stage_ok,
|
||||||
|
stage_invalid=_stage_invalid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_request(self, *, user=None, query=None):
|
||||||
|
return self.request_factory.post(
|
||||||
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
|
query_params=query or {},
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_lockdown_event(self):
|
||||||
|
"""Return the account-lockdown user-write event."""
|
||||||
|
return Event.objects.filter(
|
||||||
|
action=EventAction.USER_WRITE,
|
||||||
|
context__action_id=LOCKDOWN_EVENT_ACTION_ID,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccountLockdownStage(AccountLockdownStageTestMixin, FlowTestCase):
|
||||||
|
"""Account lockdown stage tests"""
|
||||||
|
|
||||||
|
def test_lockdown_no_target(self):
|
||||||
|
"""Test lockdown stage with no pending user fails"""
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
|
||||||
|
response = view.dispatch(self.make_request())
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_lockdown_with_pending_user(self):
|
||||||
|
"""Test lockdown stage with a pending target user."""
|
||||||
|
self.target_user.is_active = True
|
||||||
|
self.target_user.save()
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
plan.context[PLAN_CONTEXT_LOCKDOWN_REASON] = "Security incident"
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
request = self.make_request(user=self.user)
|
||||||
|
|
||||||
|
self.assertTrue(can_lock_user(request.user, self.target_user))
|
||||||
|
response = view.dispatch(request)
|
||||||
|
|
||||||
|
self.target_user.refresh_from_db()
|
||||||
|
self.assertFalse(self.target_user.is_active)
|
||||||
|
self.assertFalse(self.target_user.has_usable_password())
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
|
||||||
|
# Check event was created
|
||||||
|
event = self.get_lockdown_event()
|
||||||
|
self.assertIsNotNone(event)
|
||||||
|
self.assertEqual(event.context["action_id"], LOCKDOWN_EVENT_ACTION_ID)
|
||||||
|
self.assertEqual(event.context["reason"], "Security incident")
|
||||||
|
self.assertEqual(event.context["affected_user"], self.target_user.username)
|
||||||
|
|
||||||
|
def test_lockdown_with_pending_user_reason(self):
|
||||||
|
"""Test lockdown stage with a pending target and explicit reason."""
|
||||||
|
self.target_user.is_active = True
|
||||||
|
self.target_user.save()
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
plan.context[PLAN_CONTEXT_LOCKDOWN_REASON] = "Compromised account"
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
request = self.make_request(user=self.user)
|
||||||
|
|
||||||
|
self.assertTrue(can_lock_user(request.user, self.target_user))
|
||||||
|
response = view.dispatch(request)
|
||||||
|
|
||||||
|
self.target_user.refresh_from_db()
|
||||||
|
self.assertFalse(self.target_user.is_active)
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
|
||||||
|
def test_lockdown_reason_from_prompt(self):
|
||||||
|
"""Test lockdown stage reads the reason from prompt data."""
|
||||||
|
self.target_user.is_active = True
|
||||||
|
self.target_user.save()
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||||
|
PLAN_CONTEXT_LOCKDOWN_REASON: "User requested lockdown",
|
||||||
|
}
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
request = self.make_request(user=self.user)
|
||||||
|
view._lockdown_user(request, self.stage, self.target_user, view.get_reason())
|
||||||
|
|
||||||
|
event = self.get_lockdown_event()
|
||||||
|
self.assertIsNotNone(event)
|
||||||
|
self.assertEqual(event.context["reason"], "User requested lockdown")
|
||||||
|
|
||||||
|
def test_lockdown_event_failure_does_not_fail_self_service(self):
|
||||||
|
"""Test lockdown still succeeds when event emission fails."""
|
||||||
|
self.stage.delete_sessions = False
|
||||||
|
self.stage.save()
|
||||||
|
|
||||||
|
self.target_user.is_active = True
|
||||||
|
self.target_user.save()
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
request = self.make_request(user=self.target_user)
|
||||||
|
|
||||||
|
original_event_new = Event.new
|
||||||
|
|
||||||
|
def _event_new_side_effect(action, *args, **kwargs):
|
||||||
|
if (
|
||||||
|
action == EventAction.USER_WRITE
|
||||||
|
and kwargs.get("action_id") == LOCKDOWN_EVENT_ACTION_ID
|
||||||
|
):
|
||||||
|
raise RuntimeError("simulated event failure")
|
||||||
|
return original_event_new(action, *args, **kwargs)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"authentik.enterprise.stages.account_lockdown.stage.Event.new",
|
||||||
|
side_effect=_event_new_side_effect,
|
||||||
|
):
|
||||||
|
view._lockdown_user(request, self.stage, self.target_user, view.get_reason())
|
||||||
|
|
||||||
|
self.target_user.refresh_from_db()
|
||||||
|
self.assertFalse(self.target_user.is_active)
|
||||||
|
|
||||||
|
def test_dispatch_records_success_when_event_emission_fails(self):
|
||||||
|
"""Test dispatch still completes if event emission fails."""
|
||||||
|
self.stage.delete_sessions = False
|
||||||
|
self.stage.save()
|
||||||
|
|
||||||
|
self.target_user.is_active = True
|
||||||
|
self.target_user.save()
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
request = self.make_request(
|
||||||
|
user=self.target_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
original_event_new = Event.new
|
||||||
|
|
||||||
|
def _event_new_side_effect(action, *args, **kwargs):
|
||||||
|
if (
|
||||||
|
action == EventAction.USER_WRITE
|
||||||
|
and kwargs.get("action_id") == LOCKDOWN_EVENT_ACTION_ID
|
||||||
|
):
|
||||||
|
raise RuntimeError("simulated event failure")
|
||||||
|
return original_event_new(action, *args, **kwargs)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"authentik.enterprise.stages.account_lockdown.stage.Event.new",
|
||||||
|
side_effect=_event_new_side_effect,
|
||||||
|
):
|
||||||
|
response = view.dispatch(request)
|
||||||
|
|
||||||
|
self.target_user.refresh_from_db()
|
||||||
|
self.assertFalse(self.target_user.is_active)
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
|
||||||
|
def test_lockdown_self_service_redirects_to_completion_flow(self):
|
||||||
|
"""Test self-service lockdown redirects to completion flow when sessions are deleted."""
|
||||||
|
completion_flow = create_test_flow(FlowDesignation.STAGE_CONFIGURATION)
|
||||||
|
self.stage.self_service_completion_flow = completion_flow
|
||||||
|
self.stage.save()
|
||||||
|
|
||||||
|
self.target_user.is_active = True
|
||||||
|
self.target_user.save()
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
request = self.make_request(user=self.target_user)
|
||||||
|
view._lockdown_user(request, self.stage, self.target_user, view.get_reason())
|
||||||
|
response = view._self_service_completion_response(request)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(
|
||||||
|
response.url,
|
||||||
|
reverse("authentik_core:if-flow", kwargs={"flow_slug": completion_flow.slug}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_lockdown_self_service_requires_completion_flow(self):
|
||||||
|
"""Test self-service lockdown fails before deleting sessions without a completion flow."""
|
||||||
|
self.stage.self_service_completion_flow = None
|
||||||
|
self.stage.save()
|
||||||
|
|
||||||
|
self.target_user.is_active = True
|
||||||
|
self.target_user.save()
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
request = self.make_request(user=self.target_user)
|
||||||
|
|
||||||
|
response = view.dispatch(request)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.target_user.refresh_from_db()
|
||||||
|
self.assertTrue(self.target_user.is_active)
|
||||||
|
|
||||||
|
def test_lockdown_denies_other_user_without_permission(self):
|
||||||
|
"""Test lockdown stage rejects non-self requests without change_user permission."""
|
||||||
|
actor = create_test_user()
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.target_user
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
request = self.make_request(user=actor)
|
||||||
|
|
||||||
|
self.assertFalse(can_lock_user(request.user, self.target_user))
|
||||||
|
response = view.dispatch(request)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_lockdown_revokes_tokens(self):
|
||||||
|
"""Test lockdown stage revokes tokens"""
|
||||||
|
Token.objects.create(
|
||||||
|
user=self.target_user,
|
||||||
|
identifier="test-token",
|
||||||
|
intent=TokenIntents.INTENT_API,
|
||||||
|
key=generate_id(),
|
||||||
|
expiring=False,
|
||||||
|
)
|
||||||
|
self.assertEqual(Token.objects.filter(user=self.target_user).count(), 1)
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||||
|
|
||||||
|
self.assertEqual(Token.objects.filter(user=self.target_user).count(), 0)
|
||||||
|
|
||||||
|
def test_lockdown_revokes_provider_tokens(self):
|
||||||
|
"""Test lockdown stage revokes provider tokens and sessions."""
|
||||||
|
oauth_provider = OAuth2Provider.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
authorization_flow=create_test_flow(),
|
||||||
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver/callback")
|
||||||
|
],
|
||||||
|
signing_key=create_test_cert(),
|
||||||
|
)
|
||||||
|
saml_provider = SAMLProvider.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
authorization_flow=create_test_flow(),
|
||||||
|
acs_url="https://sp.example.com/acs",
|
||||||
|
issuer_override="https://idp.example.com",
|
||||||
|
)
|
||||||
|
session = Session.objects.create(
|
||||||
|
session_key=generate_id(),
|
||||||
|
expires=timezone.now() + timezone.timedelta(hours=1),
|
||||||
|
last_ip="127.0.0.1",
|
||||||
|
)
|
||||||
|
auth_session = AuthenticatedSession.objects.create(
|
||||||
|
session=session,
|
||||||
|
user=self.target_user,
|
||||||
|
)
|
||||||
|
grant_kwargs = {
|
||||||
|
"provider": oauth_provider,
|
||||||
|
"user": self.target_user,
|
||||||
|
"auth_time": timezone.now(),
|
||||||
|
"_scope": "openid profile",
|
||||||
|
"expiring": False,
|
||||||
|
}
|
||||||
|
token_kwargs = grant_kwargs | {"_id_token": json.dumps(asdict(IDToken("foo", "bar")))}
|
||||||
|
AuthorizationCode.objects.create(
|
||||||
|
code=generate_id(),
|
||||||
|
session=auth_session,
|
||||||
|
**grant_kwargs,
|
||||||
|
)
|
||||||
|
AccessToken.objects.create(
|
||||||
|
token=generate_id(),
|
||||||
|
session=auth_session,
|
||||||
|
**token_kwargs,
|
||||||
|
)
|
||||||
|
RefreshToken.objects.create(
|
||||||
|
token=generate_id(),
|
||||||
|
session=auth_session,
|
||||||
|
**token_kwargs,
|
||||||
|
)
|
||||||
|
DeviceToken.objects.create(
|
||||||
|
provider=oauth_provider,
|
||||||
|
user=self.target_user,
|
||||||
|
session=auth_session,
|
||||||
|
_scope="openid profile",
|
||||||
|
expiring=False,
|
||||||
|
)
|
||||||
|
SAMLSession.objects.create(
|
||||||
|
provider=saml_provider,
|
||||||
|
user=self.target_user,
|
||||||
|
session=auth_session,
|
||||||
|
session_index=generate_id(),
|
||||||
|
name_id=self.target_user.email,
|
||||||
|
expires=timezone.now() + timezone.timedelta(hours=1),
|
||||||
|
expiring=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||||
|
|
||||||
|
self.assertEqual(AuthorizationCode.objects.filter(user=self.target_user).count(), 0)
|
||||||
|
self.assertEqual(AccessToken.objects.filter(user=self.target_user).count(), 0)
|
||||||
|
self.assertEqual(RefreshToken.objects.filter(user=self.target_user).count(), 0)
|
||||||
|
self.assertEqual(DeviceToken.objects.filter(user=self.target_user).count(), 0)
|
||||||
|
self.assertEqual(SAMLSession.objects.filter(user=self.target_user).count(), 0)
|
||||||
|
|
||||||
|
def test_lockdown_selective_actions(self):
|
||||||
|
"""Test lockdown stage with selective actions"""
|
||||||
|
self.stage.deactivate_user = True
|
||||||
|
self.stage.set_unusable_password = False
|
||||||
|
self.stage.delete_sessions = False
|
||||||
|
self.stage.revoke_tokens = False
|
||||||
|
self.stage.save()
|
||||||
|
|
||||||
|
self.target_user.is_active = True
|
||||||
|
self.target_user.set_password("testpassword")
|
||||||
|
self.target_user.save()
|
||||||
|
|
||||||
|
Token.objects.create(
|
||||||
|
user=self.target_user,
|
||||||
|
identifier="test-token",
|
||||||
|
intent=TokenIntents.INTENT_API,
|
||||||
|
key=generate_id(),
|
||||||
|
expiring=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||||
|
|
||||||
|
self.target_user.refresh_from_db()
|
||||||
|
# User should be deactivated
|
||||||
|
self.assertFalse(self.target_user.is_active)
|
||||||
|
# Password should still be usable
|
||||||
|
self.assertTrue(self.target_user.has_usable_password())
|
||||||
|
# Token should still exist
|
||||||
|
self.assertEqual(Token.objects.filter(user=self.target_user).count(), 1)
|
||||||
|
|
||||||
|
def test_lockdown_no_actions(self):
|
||||||
|
"""Test lockdown stage with all actions disabled"""
|
||||||
|
self.stage.deactivate_user = False
|
||||||
|
self.stage.set_unusable_password = False
|
||||||
|
self.stage.delete_sessions = False
|
||||||
|
self.stage.revoke_tokens = False
|
||||||
|
self.stage.save()
|
||||||
|
|
||||||
|
self.target_user.is_active = True
|
||||||
|
self.target_user.set_password("testpassword")
|
||||||
|
self.target_user.save()
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||||
|
|
||||||
|
self.target_user.refresh_from_db()
|
||||||
|
# User should still be active
|
||||||
|
self.assertTrue(self.target_user.is_active)
|
||||||
|
# Password should still be usable
|
||||||
|
self.assertTrue(self.target_user.has_usable_password())
|
||||||
|
# Event should still be created
|
||||||
|
event = self.get_lockdown_event()
|
||||||
|
self.assertIsNotNone(event)
|
||||||
|
|
||||||
|
def test_lockdown_deactivation_inhibits_signal_dispatch_until_after_commit(self):
|
||||||
|
"""Test lockdown queues explicit outgoing syncs after the deactivation transaction."""
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"authentik.enterprise.stages.account_lockdown.stage.sync_outgoing_inhibit_dispatch"
|
||||||
|
) as inhibit,
|
||||||
|
patch.object(view, "_sync_deactivated_user_to_outgoing_providers") as sync_outgoing,
|
||||||
|
):
|
||||||
|
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||||
|
|
||||||
|
inhibit.assert_called_once()
|
||||||
|
sync_outgoing.assert_called_once()
|
||||||
|
synced_user = sync_outgoing.call_args.args[0]
|
||||||
|
self.assertEqual(synced_user.pk, self.target_user.pk)
|
||||||
|
self.assertFalse(synced_user.is_active)
|
||||||
|
|
||||||
|
def test_lockdown_waits_for_direct_outgoing_provider_syncs(self):
|
||||||
|
"""Test direct outgoing sync tasks are enqueued and waited on."""
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
provider = SimpleNamespace(name="outgoing", pk=1, sync_page_timeout="seconds=5")
|
||||||
|
task_sync_direct = MagicMock()
|
||||||
|
task_sync_direct.message_with_options.return_value = "direct-message"
|
||||||
|
provider_model = SimpleNamespace(
|
||||||
|
objects=SimpleNamespace(filter=MagicMock(return_value=[provider]))
|
||||||
|
)
|
||||||
|
task_group = MagicMock()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"authentik.enterprise.stages.account_lockdown.stage.get_outgoing_sync_tasks",
|
||||||
|
return_value=((provider_model, task_sync_direct),),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"authentik.enterprise.stages.account_lockdown.stage.group",
|
||||||
|
return_value=task_group,
|
||||||
|
) as task_group_cls,
|
||||||
|
):
|
||||||
|
view._sync_deactivated_user_to_outgoing_providers(self.target_user)
|
||||||
|
|
||||||
|
task_sync_direct.message_with_options.assert_called_once_with(
|
||||||
|
args=(class_to_path(type(self.target_user)), self.target_user.pk, provider.pk),
|
||||||
|
rel_obj=provider,
|
||||||
|
time_limit=5000,
|
||||||
|
uid=f"{provider.name}:user:{self.target_user.pk}:direct",
|
||||||
|
)
|
||||||
|
task_group_cls.assert_called_once_with(["direct-message"])
|
||||||
|
task_group.run.return_value.wait.assert_called_once_with(timeout=5000)
|
||||||
|
|
||||||
|
def test_lockdown_outgoing_provider_sync_timeout_leaves_tasks_running(self):
|
||||||
|
"""Test timeout while waiting for direct outgoing syncs does not fail lockdown."""
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
provider = SimpleNamespace(name="outgoing", pk=1, sync_page_timeout="seconds=5")
|
||||||
|
task_sync_direct = MagicMock()
|
||||||
|
task_sync_direct.message_with_options.return_value = "direct-message"
|
||||||
|
provider_model = SimpleNamespace(
|
||||||
|
objects=SimpleNamespace(filter=MagicMock(return_value=[provider]))
|
||||||
|
)
|
||||||
|
task_group = MagicMock()
|
||||||
|
task_group.run.return_value.wait.side_effect = ResultTimeout("timed out")
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"authentik.enterprise.stages.account_lockdown.stage.get_outgoing_sync_tasks",
|
||||||
|
return_value=((provider_model, task_sync_direct),),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"authentik.enterprise.stages.account_lockdown.stage.group",
|
||||||
|
return_value=task_group,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
view._sync_deactivated_user_to_outgoing_providers(self.target_user)
|
||||||
|
|
||||||
|
task_group.run.assert_called_once_with()
|
||||||
|
task_group.run.return_value.wait.assert_called_once_with(timeout=5000)
|
||||||
|
|
||||||
|
def test_lockdown_outgoing_provider_sync_failure_does_not_fail_lockdown(self):
|
||||||
|
"""Test completed local lockdown still emits an event if outgoing sync fails."""
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
view,
|
||||||
|
"_sync_deactivated_user_to_outgoing_providers",
|
||||||
|
side_effect=ValueError("sync failed"),
|
||||||
|
):
|
||||||
|
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||||
|
|
||||||
|
self.target_user.refresh_from_db()
|
||||||
|
self.assertFalse(self.target_user.is_active)
|
||||||
|
event = self.get_lockdown_event()
|
||||||
|
self.assertIsNotNone(event)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccountLockdownStageConcurrency(AccountLockdownStageTestMixin, TransactionTestCase):
|
||||||
|
"""Account lockdown concurrency tests."""
|
||||||
|
|
||||||
|
def test_lockdown_retries_when_another_transaction_recreates_a_token(self):
|
||||||
|
"""Lockdown should remove a token recreated before the retry check runs."""
|
||||||
|
Token.objects.create(
|
||||||
|
user=self.target_user,
|
||||||
|
identifier=f"initial-token-{generate_id()}",
|
||||||
|
intent=TokenIntents.INTENT_API,
|
||||||
|
key=generate_id(),
|
||||||
|
expiring=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
view = self.make_stage_view(plan)
|
||||||
|
original_has_artifacts = view._has_lockdown_artifacts
|
||||||
|
target_user = self.target_user
|
||||||
|
thread_ready = ThreadEvent()
|
||||||
|
start_create = ThreadEvent()
|
||||||
|
thread_done = ThreadEvent()
|
||||||
|
thread_errors = []
|
||||||
|
|
||||||
|
class TokenCreatorThread(Thread):
|
||||||
|
__test__ = False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
thread_ready.set()
|
||||||
|
if not start_create.wait(timeout=5):
|
||||||
|
thread_errors.append("timed out waiting to recreate token")
|
||||||
|
return
|
||||||
|
Token.objects.create(
|
||||||
|
user=target_user,
|
||||||
|
identifier=f"concurrent-token-{generate_id()}",
|
||||||
|
intent=TokenIntents.INTENT_API,
|
||||||
|
key=generate_id(),
|
||||||
|
expiring=False,
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
thread_errors.append(exc)
|
||||||
|
finally:
|
||||||
|
thread_done.set()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
def has_artifacts_after_concurrent_create(stage, user):
|
||||||
|
if not start_create.is_set():
|
||||||
|
start_create.set()
|
||||||
|
self.assertTrue(
|
||||||
|
thread_done.wait(timeout=30),
|
||||||
|
(
|
||||||
|
"Concurrent token creation did not complete "
|
||||||
|
f"before retry check: {thread_errors}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return original_has_artifacts(stage, user)
|
||||||
|
|
||||||
|
creator = TokenCreatorThread()
|
||||||
|
with patch.object(
|
||||||
|
view, "_has_lockdown_artifacts", side_effect=has_artifacts_after_concurrent_create
|
||||||
|
):
|
||||||
|
creator.start()
|
||||||
|
self.assertTrue(
|
||||||
|
thread_ready.wait(timeout=5),
|
||||||
|
"Concurrent token creation thread did not start",
|
||||||
|
)
|
||||||
|
view._lockdown_user(self.make_request(user=self.user), self.stage, self.target_user, "")
|
||||||
|
creator.join()
|
||||||
|
|
||||||
|
self.assertEqual(thread_errors, [])
|
||||||
|
self.assertEqual(Token.objects.filter(user=self.target_user).count(), 0)
|
||||||
5
authentik/enterprise/stages/account_lockdown/urls.py
Normal file
5
authentik/enterprise/stages/account_lockdown/urls.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""API URLs"""
|
||||||
|
|
||||||
|
from authentik.enterprise.stages.account_lockdown.api import AccountLockdownStageViewSet
|
||||||
|
|
||||||
|
api_urlpatterns = [("stages/account_lockdown", AccountLockdownStageViewSet)]
|
||||||
@@ -8,7 +8,6 @@ from inspect import currentframe
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -410,7 +409,7 @@ class NotificationTransport(TasksModel, SerializerModel):
|
|||||||
)
|
)
|
||||||
notification.save()
|
notification.save()
|
||||||
layer = get_channel_layer()
|
layer = get_channel_layer()
|
||||||
async_to_sync(layer.group_send)(
|
layer.group_send_blocking(
|
||||||
build_user_group(notification.user),
|
build_user_group(notification.user),
|
||||||
{
|
{
|
||||||
"type": "event.notification",
|
"type": "event.notification",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
background-image: url("{{ flow_background_url }}");
|
background-image: url("{{ flow_background_url|iriencode|safe }}");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
||||||
<style data-id="flow-css">
|
<style data-id="flow-css">
|
||||||
:root {
|
:root {
|
||||||
--ak-global--background-image: url("{{ flow_background_url }}");
|
--ak-global--background-image: url("{{ flow_background_url|iriencode|safe }}");
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
"""stage view tests"""
|
"""stage view tests"""
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import RequestFactory, TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from authentik.core.tests.utils import RequestFactory as AuthentikRequestFactory
|
from authentik.core.tests.utils import RequestFactory as AuthentikRequestFactory
|
||||||
from authentik.core.tests.utils import create_test_flow
|
from authentik.core.tests.utils import create_test_flow
|
||||||
from authentik.flows.models import FlowStageBinding
|
from authentik.flows.models import Flow, FlowStageBinding
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.views.executor import FlowExecutorView
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
@@ -42,6 +44,46 @@ class TestViews(TestCase):
|
|||||||
"/static/dist/assets/images/flow_background.jpg",
|
"/static/dist/assets/images/flow_background.jpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_flow_interface_css_background_preserves_presigned_url_query(self):
|
||||||
|
"""Test flow CSS keeps signed URL query separators intact."""
|
||||||
|
flow = create_test_flow()
|
||||||
|
background_url = (
|
||||||
|
"https://s3.ca-central-1.amazonaws.com/example/media/public/background.png"
|
||||||
|
"?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=credential"
|
||||||
|
"&X-Amz-Signature=signature"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(Flow, "background_url", return_value=background_url):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
f'--ak-global--background-image: url("{background_url}");',
|
||||||
|
html=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_flow_sfe_css_background_preserves_presigned_url_query(self):
|
||||||
|
"""Test SFE flow CSS keeps signed URL query separators intact."""
|
||||||
|
flow = create_test_flow()
|
||||||
|
background_url = (
|
||||||
|
"https://s3.ca-central-1.amazonaws.com/example/media/public/background.png"
|
||||||
|
"?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=credential"
|
||||||
|
"&X-Amz-Signature=signature"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(Flow, "background_url", return_value=background_url):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) + "?sfe"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
f'background-image: url("{background_url}");',
|
||||||
|
html=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def view_tester_factory(view_class: type[StageView]) -> Callable:
|
def view_tester_factory(view_class: type[StageView]) -> Callable:
|
||||||
"""Test a form"""
|
"""Test a form"""
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ class TestEndSessionView(OAuthTestCase):
|
|||||||
self.brand.flow_invalidation = self.invalidation_flow
|
self.brand.flow_invalidation = self.invalidation_flow
|
||||||
self.brand.save()
|
self.brand.save()
|
||||||
|
|
||||||
|
def _id_token_hint(self, host: str) -> str:
|
||||||
|
"""Issue a valid id_token_hint for the test provider under the given host."""
|
||||||
|
return self.provider.encode(
|
||||||
|
{
|
||||||
|
"iss": f"http://{host}/application/o/{self.app.slug}/",
|
||||||
|
"aud": self.provider.client_id,
|
||||||
|
"sub": str(self.user.pk),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_post_logout_redirect_uri_strict_match(self):
|
def test_post_logout_redirect_uri_strict_match(self):
|
||||||
"""Test strict URI matching redirects to flow"""
|
"""Test strict URI matching redirects to flow"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
@@ -61,7 +71,10 @@ class TestEndSessionView(OAuthTestCase):
|
|||||||
"authentik_providers_oauth2:end-session",
|
"authentik_providers_oauth2:end-session",
|
||||||
kwargs={"application_slug": self.app.slug},
|
kwargs={"application_slug": self.app.slug},
|
||||||
),
|
),
|
||||||
{"post_logout_redirect_uri": "http://testserver/logout"},
|
{
|
||||||
|
"post_logout_redirect_uri": "http://testserver/logout",
|
||||||
|
"id_token_hint": self._id_token_hint(self.brand.domain),
|
||||||
|
},
|
||||||
HTTP_HOST=self.brand.domain,
|
HTTP_HOST=self.brand.domain,
|
||||||
)
|
)
|
||||||
# Should redirect to the invalidation flow
|
# Should redirect to the invalidation flow
|
||||||
@@ -69,7 +82,12 @@ class TestEndSessionView(OAuthTestCase):
|
|||||||
self.assertIn(self.invalidation_flow.slug, response.url)
|
self.assertIn(self.invalidation_flow.slug, response.url)
|
||||||
|
|
||||||
def test_post_logout_redirect_uri_strict_no_match(self):
|
def test_post_logout_redirect_uri_strict_no_match(self):
|
||||||
"""Test strict URI not matching still proceeds with flow (no redirect URI in context)"""
|
"""Test strict URI not matching returns an error and does not start logout flow.
|
||||||
|
|
||||||
|
Required by OIDC RP-Initiated Logout 1.0: on an unregistered
|
||||||
|
post_logout_redirect_uri, the OP MUST NOT redirect and MUST NOT proceed with
|
||||||
|
logout that targets the RP.
|
||||||
|
"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
invalid_uri = "http://testserver/other"
|
invalid_uri = "http://testserver/other"
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@@ -77,12 +95,14 @@ class TestEndSessionView(OAuthTestCase):
|
|||||||
"authentik_providers_oauth2:end-session",
|
"authentik_providers_oauth2:end-session",
|
||||||
kwargs={"application_slug": self.app.slug},
|
kwargs={"application_slug": self.app.slug},
|
||||||
),
|
),
|
||||||
{"post_logout_redirect_uri": invalid_uri},
|
{
|
||||||
|
"post_logout_redirect_uri": invalid_uri,
|
||||||
|
"id_token_hint": self._id_token_hint(self.brand.domain),
|
||||||
|
},
|
||||||
HTTP_HOST=self.brand.domain,
|
HTTP_HOST=self.brand.domain,
|
||||||
)
|
)
|
||||||
# Should still redirect to flow, but invalid URI should not be in response
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertNotIn(invalid_uri, response.content.decode())
|
||||||
self.assertNotIn(invalid_uri, response.url)
|
|
||||||
|
|
||||||
def test_post_logout_redirect_uri_regex_match(self):
|
def test_post_logout_redirect_uri_regex_match(self):
|
||||||
"""Test regex URI matching redirects to flow"""
|
"""Test regex URI matching redirects to flow"""
|
||||||
@@ -92,7 +112,10 @@ class TestEndSessionView(OAuthTestCase):
|
|||||||
"authentik_providers_oauth2:end-session",
|
"authentik_providers_oauth2:end-session",
|
||||||
kwargs={"application_slug": self.app.slug},
|
kwargs={"application_slug": self.app.slug},
|
||||||
),
|
),
|
||||||
{"post_logout_redirect_uri": "https://app.example.com/logout"},
|
{
|
||||||
|
"post_logout_redirect_uri": "https://app.example.com/logout",
|
||||||
|
"id_token_hint": self._id_token_hint(self.brand.domain),
|
||||||
|
},
|
||||||
HTTP_HOST=self.brand.domain,
|
HTTP_HOST=self.brand.domain,
|
||||||
)
|
)
|
||||||
# Should redirect to the invalidation flow
|
# Should redirect to the invalidation flow
|
||||||
@@ -100,7 +123,7 @@ class TestEndSessionView(OAuthTestCase):
|
|||||||
self.assertIn(self.invalidation_flow.slug, response.url)
|
self.assertIn(self.invalidation_flow.slug, response.url)
|
||||||
|
|
||||||
def test_post_logout_redirect_uri_regex_no_match(self):
|
def test_post_logout_redirect_uri_regex_no_match(self):
|
||||||
"""Test regex URI not matching"""
|
"""Test regex URI not matching returns an error and does not start logout flow."""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
invalid_uri = "https://malicious.com/logout"
|
invalid_uri = "https://malicious.com/logout"
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@@ -108,12 +131,14 @@ class TestEndSessionView(OAuthTestCase):
|
|||||||
"authentik_providers_oauth2:end-session",
|
"authentik_providers_oauth2:end-session",
|
||||||
kwargs={"application_slug": self.app.slug},
|
kwargs={"application_slug": self.app.slug},
|
||||||
),
|
),
|
||||||
{"post_logout_redirect_uri": invalid_uri},
|
{
|
||||||
|
"post_logout_redirect_uri": invalid_uri,
|
||||||
|
"id_token_hint": self._id_token_hint(self.brand.domain),
|
||||||
|
},
|
||||||
HTTP_HOST=self.brand.domain,
|
HTTP_HOST=self.brand.domain,
|
||||||
)
|
)
|
||||||
# Should still proceed to flow, but invalid URI should not be in response
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertNotIn(invalid_uri, response.content.decode())
|
||||||
self.assertNotIn(invalid_uri, response.url)
|
|
||||||
|
|
||||||
def test_state_parameter_appended_to_uri(self):
|
def test_state_parameter_appended_to_uri(self):
|
||||||
"""Test state parameter is appended to validated redirect URI"""
|
"""Test state parameter is appended to validated redirect URI"""
|
||||||
@@ -123,6 +148,7 @@ class TestEndSessionView(OAuthTestCase):
|
|||||||
{
|
{
|
||||||
"post_logout_redirect_uri": "http://testserver/logout",
|
"post_logout_redirect_uri": "http://testserver/logout",
|
||||||
"state": "test-state-123",
|
"state": "test-state-123",
|
||||||
|
"id_token_hint": self._id_token_hint("testserver"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
@@ -132,6 +158,7 @@ class TestEndSessionView(OAuthTestCase):
|
|||||||
view.request = request
|
view.request = request
|
||||||
view.kwargs = {"application_slug": self.app.slug}
|
view.kwargs = {"application_slug": self.app.slug}
|
||||||
view.resolve_provider_application()
|
view.resolve_provider_application()
|
||||||
|
view.validate()
|
||||||
|
|
||||||
self.assertIn("state=test-state-123", view.post_logout_redirect_uri)
|
self.assertIn("state=test-state-123", view.post_logout_redirect_uri)
|
||||||
|
|
||||||
@@ -146,6 +173,7 @@ class TestEndSessionView(OAuthTestCase):
|
|||||||
{
|
{
|
||||||
"post_logout_redirect_uri": "http://testserver/logout",
|
"post_logout_redirect_uri": "http://testserver/logout",
|
||||||
"state": "xyz789",
|
"state": "xyz789",
|
||||||
|
"id_token_hint": self._id_token_hint(self.brand.domain),
|
||||||
},
|
},
|
||||||
HTTP_HOST=self.brand.domain,
|
HTTP_HOST=self.brand.domain,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from urllib.parse import quote, urlparse
|
|||||||
|
|
||||||
from django.http import Http404, HttpRequest, HttpResponse
|
from django.http import Http404, HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from jwt import PyJWTError
|
||||||
|
from jwt import decode as jwt_decode
|
||||||
|
|
||||||
from authentik.common.oauth.constants import (
|
from authentik.common.oauth.constants import (
|
||||||
FORBIDDEN_URI_SCHEMES,
|
FORBIDDEN_URI_SCHEMES,
|
||||||
@@ -21,11 +23,14 @@ from authentik.flows.planner import (
|
|||||||
from authentik.flows.stage import SessionEndStage
|
from authentik.flows.stage import SessionEndStage
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.views import PolicyAccessView, RequestValidationError
|
from authentik.policies.views import PolicyAccessView
|
||||||
from authentik.providers.iframe_logout import IframeLogoutStageView
|
from authentik.providers.iframe_logout import IframeLogoutStageView
|
||||||
|
from authentik.providers.oauth2.errors import TokenError
|
||||||
from authentik.providers.oauth2.models import (
|
from authentik.providers.oauth2.models import (
|
||||||
AccessToken,
|
AccessToken,
|
||||||
|
JWTAlgorithms,
|
||||||
OAuth2LogoutMethod,
|
OAuth2LogoutMethod,
|
||||||
|
OAuth2Provider,
|
||||||
RedirectURIMatchingMode,
|
RedirectURIMatchingMode,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.tasks import send_backchannel_logout_request
|
from authentik.providers.oauth2.tasks import send_backchannel_logout_request
|
||||||
@@ -47,21 +52,45 @@ class EndSessionView(PolicyAccessView):
|
|||||||
if not self.flow:
|
if not self.flow:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
# Parse end session parameters
|
# Parse end session parameters
|
||||||
query_dict = self.request.POST if self.request.method == "POST" else self.request.GET
|
query_dict = self.request.POST if self.request.method == "POST" else self.request.GET
|
||||||
state = query_dict.get("state")
|
state = query_dict.get("state")
|
||||||
request_redirect_uri = query_dict.get("post_logout_redirect_uri")
|
request_redirect_uri = query_dict.get("post_logout_redirect_uri")
|
||||||
|
id_token_hint = query_dict.get("id_token_hint")
|
||||||
self.post_logout_redirect_uri = None
|
self.post_logout_redirect_uri = None
|
||||||
|
|
||||||
|
# OIDC Certification: Verify id_token_hint. If invalid or missing, throw an error
|
||||||
|
if id_token_hint:
|
||||||
|
# Load a fresh provider instance that's not part of the flow
|
||||||
|
# since it'll have the cryptography Certificate that can't be pickled
|
||||||
|
provider = OAuth2Provider.objects.get(pk=self.provider.pk)
|
||||||
|
key, alg = provider.jwt_key
|
||||||
|
if alg != JWTAlgorithms.HS256:
|
||||||
|
key = provider.signing_key.public_key
|
||||||
|
try:
|
||||||
|
jwt_decode(
|
||||||
|
id_token_hint,
|
||||||
|
key,
|
||||||
|
algorithms=[alg],
|
||||||
|
audience=provider.client_id,
|
||||||
|
issuer=provider.get_issuer(self.request),
|
||||||
|
# ID Tokens are short-lived; a logout request arriving
|
||||||
|
# after expiry is still legitimate and must succeed.
|
||||||
|
options={"verify_exp": False},
|
||||||
|
)
|
||||||
|
except PyJWTError:
|
||||||
|
raise TokenError("invalid_request").with_cause(
|
||||||
|
"id_token_hint_decode_failed"
|
||||||
|
) from None
|
||||||
|
|
||||||
# Validate post_logout_redirect_uri against registered URIs
|
# Validate post_logout_redirect_uri against registered URIs
|
||||||
if request_redirect_uri:
|
if request_redirect_uri:
|
||||||
|
# OIDC Certification: id_token_hint required with post_logout_redirect_uri
|
||||||
|
if not id_token_hint:
|
||||||
|
raise TokenError("invalid_request").with_cause("id_token_hint_missing")
|
||||||
if urlparse(request_redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
if urlparse(request_redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
||||||
raise RequestValidationError(
|
raise TokenError("invalid_request").with_cause("post_logout_redirect_uri")
|
||||||
bad_request_message(
|
|
||||||
self.request,
|
|
||||||
"Forbidden URI scheme in post_logout_redirect_uri",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for allowed in self.provider.post_logout_redirect_uris:
|
for allowed in self.provider.post_logout_redirect_uris:
|
||||||
if allowed.matching_mode == RedirectURIMatchingMode.STRICT:
|
if allowed.matching_mode == RedirectURIMatchingMode.STRICT:
|
||||||
if request_redirect_uri == allowed.url:
|
if request_redirect_uri == allowed.url:
|
||||||
@@ -71,6 +100,10 @@ class EndSessionView(PolicyAccessView):
|
|||||||
if fullmatch(allowed.url, request_redirect_uri):
|
if fullmatch(allowed.url, request_redirect_uri):
|
||||||
self.post_logout_redirect_uri = request_redirect_uri
|
self.post_logout_redirect_uri = request_redirect_uri
|
||||||
break
|
break
|
||||||
|
# OIDC Certification: OP MUST NOT perform post-logout redirection
|
||||||
|
# if the supplied URI does not exactly match a registered one
|
||||||
|
if self.post_logout_redirect_uri is None:
|
||||||
|
raise TokenError("invalid_request").with_cause("invalid_post_logout_redirect_uri")
|
||||||
|
|
||||||
# Append state to the redirect URI if both are present
|
# Append state to the redirect URI if both are present
|
||||||
if self.post_logout_redirect_uri and state:
|
if self.post_logout_redirect_uri and state:
|
||||||
@@ -91,50 +124,43 @@ class EndSessionView(PolicyAccessView):
|
|||||||
"<html><body>Logout successful</body></html>", content_type="text/html", status=200
|
"<html><body>Logout successful</body></html>", content_type="text/html", status=200
|
||||||
)
|
)
|
||||||
|
|
||||||
# Otherwise, continue with normal policy checks
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
"""Dispatch the flow planner for the invalidation flow"""
|
"""Dispatch the flow planner for the invalidation flow"""
|
||||||
|
try:
|
||||||
|
self.validate()
|
||||||
|
except TokenError as exc:
|
||||||
|
return bad_request_message(
|
||||||
|
self.request,
|
||||||
|
exc.description,
|
||||||
|
)
|
||||||
planner = FlowPlanner(self.flow)
|
planner = FlowPlanner(self.flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
|
|
||||||
# Build flow context with logout parameters
|
|
||||||
context = {
|
context = {
|
||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
PLAN_CONTEXT_APPLICATION: self.application,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get session info for logout notifications and token invalidation
|
|
||||||
auth_session = AuthenticatedSession.from_request(request, request.user)
|
auth_session = AuthenticatedSession.from_request(request, request.user)
|
||||||
|
|
||||||
# Add validated redirect URI (with state appended) to context if available
|
|
||||||
if self.post_logout_redirect_uri:
|
if self.post_logout_redirect_uri:
|
||||||
context[PLAN_CONTEXT_POST_LOGOUT_REDIRECT_URI] = self.post_logout_redirect_uri
|
context[PLAN_CONTEXT_POST_LOGOUT_REDIRECT_URI] = self.post_logout_redirect_uri
|
||||||
# Invalidate tokens for this provider/session (RP-initiated logout:
|
|
||||||
# user stays logged into authentik, only this provider's tokens are revoked)
|
|
||||||
if request.user.is_authenticated and auth_session:
|
|
||||||
AccessToken.objects.filter(
|
|
||||||
user=request.user,
|
|
||||||
provider=self.provider,
|
|
||||||
session=auth_session,
|
|
||||||
).delete()
|
|
||||||
session_key = (
|
session_key = (
|
||||||
auth_session.session.session_key if auth_session and auth_session.session else None
|
auth_session.session.session_key if auth_session and auth_session.session else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle frontchannel logout
|
|
||||||
frontchannel_logout_url = None
|
frontchannel_logout_url = None
|
||||||
if self.provider.logout_method == OAuth2LogoutMethod.FRONTCHANNEL:
|
if self.provider.logout_method == OAuth2LogoutMethod.FRONTCHANNEL:
|
||||||
frontchannel_logout_url = build_frontchannel_logout_url(
|
frontchannel_logout_url = build_frontchannel_logout_url(
|
||||||
self.provider, request, session_key
|
self.provider, request, session_key
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle backchannel logout
|
|
||||||
if (
|
if (
|
||||||
self.provider.logout_method == OAuth2LogoutMethod.BACKCHANNEL
|
self.provider.logout_method == OAuth2LogoutMethod.BACKCHANNEL
|
||||||
and self.provider.logout_uri
|
and self.provider.logout_uri
|
||||||
):
|
):
|
||||||
# Find access token to get iss and sub for the logout token
|
|
||||||
access_token = AccessToken.objects.filter(
|
access_token = AccessToken.objects.filter(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
@@ -163,9 +189,16 @@ class EndSessionView(PolicyAccessView):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
access_tokens = AccessToken.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
provider=self.provider,
|
||||||
|
)
|
||||||
|
if auth_session:
|
||||||
|
access_tokens = access_tokens.filter(session=auth_session)
|
||||||
|
access_tokens.delete()
|
||||||
|
|
||||||
plan = planner.plan(request, context)
|
plan = planner.plan(request, context)
|
||||||
|
|
||||||
# Inject iframe logout stage if frontchannel logout is configured
|
|
||||||
if frontchannel_logout_url:
|
if frontchannel_logout_url:
|
||||||
plan.insert_stage(in_memory_stage(IframeLogoutStageView))
|
plan.insert_stage(in_memory_stage(IframeLogoutStageView))
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""RAC Signals"""
|
"""RAC Signals"""
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models.signals import post_delete, post_save, pre_delete
|
from django.db.models.signals import post_delete, post_save, pre_delete
|
||||||
@@ -18,7 +17,7 @@ from authentik.providers.rac.models import ConnectionToken, Endpoint
|
|||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def user_session_deleted(sender, instance: AuthenticatedSession, **_):
|
def user_session_deleted(sender, instance: AuthenticatedSession, **_):
|
||||||
layer = get_channel_layer()
|
layer = get_channel_layer()
|
||||||
async_to_sync(layer.group_send)(
|
layer.group_send_blocking(
|
||||||
build_rac_client_group_session(instance.session.session_key),
|
build_rac_client_group_session(instance.session.session_key),
|
||||||
{"type": "event.disconnect", "reason": "session_logout"},
|
{"type": "event.disconnect", "reason": "session_logout"},
|
||||||
)
|
)
|
||||||
@@ -28,7 +27,7 @@ def user_session_deleted(sender, instance: AuthenticatedSession, **_):
|
|||||||
def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **_):
|
def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **_):
|
||||||
"""Disconnect session when connection token is deleted"""
|
"""Disconnect session when connection token is deleted"""
|
||||||
layer = get_channel_layer()
|
layer = get_channel_layer()
|
||||||
async_to_sync(layer.group_send)(
|
layer.group_send_blocking(
|
||||||
build_rac_client_group_token(instance.token),
|
build_rac_client_group_token(instance.token),
|
||||||
{"type": "event.disconnect", "reason": "token_delete"},
|
{"type": "event.disconnect", "reason": "token_delete"},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
("authentik_core", "0056_user_roles"), # must run before group field is removed
|
||||||
("authentik_rbac", "0009_remove_initialpermissions_mode"),
|
("authentik_rbac", "0009_remove_initialpermissions_mode"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ SPECTACULAR_SETTINGS = {
|
|||||||
},
|
},
|
||||||
"ENUM_NAME_OVERRIDES": {
|
"ENUM_NAME_OVERRIDES": {
|
||||||
"AppEnum": "authentik.lib.api.Apps",
|
"AppEnum": "authentik.lib.api.Apps",
|
||||||
|
"AuthenticationEnum": "authentik.flows.models.FlowAuthenticationRequirement",
|
||||||
"ConsentModeEnum": "authentik.stages.consent.models.ConsentMode",
|
"ConsentModeEnum": "authentik.stages.consent.models.ConsentMode",
|
||||||
"CountryCodeEnum": "django_countries.countries",
|
"CountryCodeEnum": "django_countries.countries",
|
||||||
"DeviceClassesEnum": "authentik.stages.authenticator_validate.models.DeviceClasses",
|
"DeviceClassesEnum": "authentik.stages.authenticator_validate.models.DeviceClasses",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
64
authentik/stages/prompt/migrations/0012_alter_prompt_type.py
Normal file
64
authentik/stages/prompt/migrations/0012_alter_prompt_type.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Generated by Django 5.2.12 on 2026-03-14 02:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"authentik_stages_prompt",
|
||||||
|
"0011_prompt_initial_value_prompt_initial_value_expression_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="prompt",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("text", "Text: Simple Text input"),
|
||||||
|
("text_area", "Text area: Multiline Text Input."),
|
||||||
|
(
|
||||||
|
"text_read_only",
|
||||||
|
"Text (read-only): Simple Text input, but cannot be edited.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"text_area_read_only",
|
||||||
|
"Text area (read-only): Multiline Text input, but cannot be edited.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"username",
|
||||||
|
"Username: Same as Text input, but checks for and prevents duplicate usernames.",
|
||||||
|
),
|
||||||
|
("email", "Email: Text field with Email type."),
|
||||||
|
(
|
||||||
|
"password",
|
||||||
|
"Password: Masked input, multiple inputs of this type on the same prompt need to be identical.",
|
||||||
|
),
|
||||||
|
("number", "Number"),
|
||||||
|
("checkbox", "Checkbox"),
|
||||||
|
(
|
||||||
|
"radio-button-group",
|
||||||
|
"Fixed choice field rendered as a group of radio buttons.",
|
||||||
|
),
|
||||||
|
("dropdown", "Fixed choice field rendered as a dropdown."),
|
||||||
|
("date", "Date"),
|
||||||
|
("date-time", "Date Time"),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
"File: File upload for arbitrary files. File content will be available in flow context as data-URI",
|
||||||
|
),
|
||||||
|
("separator", "Separator: Static Separator Line"),
|
||||||
|
("hidden", "Hidden: Hidden field, can be used to insert data into form."),
|
||||||
|
("static", "Static: Static value, displayed as-is."),
|
||||||
|
("alert_info", "Alert (Info): Static alert box with info styling"),
|
||||||
|
("alert_warning", "Alert (Warning): Static alert box with warning styling"),
|
||||||
|
("alert_danger", "Alert (Danger): Static alert box with danger styling"),
|
||||||
|
("ak-locale", "authentik: Selection of locales authentik supports"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -87,6 +87,11 @@ class FieldTypes(models.TextChoices):
|
|||||||
HIDDEN = "hidden", _("Hidden: Hidden field, can be used to insert data into form.")
|
HIDDEN = "hidden", _("Hidden: Hidden field, can be used to insert data into form.")
|
||||||
STATIC = "static", _("Static: Static value, displayed as-is.")
|
STATIC = "static", _("Static: Static value, displayed as-is.")
|
||||||
|
|
||||||
|
# Alert box types for displaying styled messages
|
||||||
|
ALERT_INFO = "alert_info", _("Alert (Info): Static alert box with info styling")
|
||||||
|
ALERT_WARNING = "alert_warning", _("Alert (Warning): Static alert box with warning styling")
|
||||||
|
ALERT_DANGER = "alert_danger", _("Alert (Danger): Static alert box with danger styling")
|
||||||
|
|
||||||
AK_LOCALE = "ak-locale", _("authentik: Selection of locales authentik supports")
|
AK_LOCALE = "ak-locale", _("authentik: Selection of locales authentik supports")
|
||||||
|
|
||||||
|
|
||||||
@@ -299,7 +304,12 @@ class Prompt(SerializerModel):
|
|||||||
field_class = HiddenField
|
field_class = HiddenField
|
||||||
kwargs["required"] = False
|
kwargs["required"] = False
|
||||||
kwargs["default"] = self.placeholder
|
kwargs["default"] = self.placeholder
|
||||||
case FieldTypes.STATIC:
|
case (
|
||||||
|
FieldTypes.STATIC
|
||||||
|
| FieldTypes.ALERT_INFO
|
||||||
|
| FieldTypes.ALERT_WARNING
|
||||||
|
| FieldTypes.ALERT_DANGER
|
||||||
|
):
|
||||||
kwargs["default"] = self.placeholder
|
kwargs["default"] = self.placeholder
|
||||||
kwargs["required"] = False
|
kwargs["required"] = False
|
||||||
kwargs["label"] = ""
|
kwargs["label"] = ""
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ class PromptChallengeResponse(ChallengeResponse):
|
|||||||
type__in=[
|
type__in=[
|
||||||
FieldTypes.HIDDEN,
|
FieldTypes.HIDDEN,
|
||||||
FieldTypes.STATIC,
|
FieldTypes.STATIC,
|
||||||
|
FieldTypes.ALERT_INFO,
|
||||||
|
FieldTypes.ALERT_WARNING,
|
||||||
|
FieldTypes.ALERT_DANGER,
|
||||||
FieldTypes.TEXT_READ_ONLY,
|
FieldTypes.TEXT_READ_ONLY,
|
||||||
FieldTypes.TEXT_AREA_READ_ONLY,
|
FieldTypes.TEXT_AREA_READ_ONLY,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -330,10 +330,20 @@ class TestPromptStage(FlowTestCase):
|
|||||||
|
|
||||||
def test_static_hidden_overwrite(self):
|
def test_static_hidden_overwrite(self):
|
||||||
"""Test that static and hidden fields ignore any value sent to them"""
|
"""Test that static and hidden fields ignore any value sent to them"""
|
||||||
|
alert_prompt = Prompt.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
field_key="alert_prompt",
|
||||||
|
type=FieldTypes.ALERT_INFO,
|
||||||
|
required=True,
|
||||||
|
placeholder="alert fallback",
|
||||||
|
initial_value="alert content",
|
||||||
|
)
|
||||||
|
self.stage.fields.add(alert_prompt)
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
plan.context[PLAN_CONTEXT_PROMPT] = {"hidden_prompt": "hidden"}
|
plan.context[PLAN_CONTEXT_PROMPT] = {"hidden_prompt": "hidden"}
|
||||||
self.prompt_data["hidden_prompt"] = "foo"
|
self.prompt_data["hidden_prompt"] = "foo"
|
||||||
self.prompt_data["static_prompt"] = "foo"
|
self.prompt_data["static_prompt"] = "foo"
|
||||||
|
self.prompt_data["alert_prompt"] = "foo"
|
||||||
challenge_response = PromptChallengeResponse(
|
challenge_response = PromptChallengeResponse(
|
||||||
None, stage_instance=self.stage, plan=plan, data=self.prompt_data, stage=self.stage_view
|
None, stage_instance=self.stage, plan=plan, data=self.prompt_data, stage=self.stage_view
|
||||||
)
|
)
|
||||||
@@ -341,6 +351,7 @@ class TestPromptStage(FlowTestCase):
|
|||||||
self.assertNotEqual(challenge_response.validated_data["hidden_prompt"], "foo")
|
self.assertNotEqual(challenge_response.validated_data["hidden_prompt"], "foo")
|
||||||
self.assertEqual(challenge_response.validated_data["hidden_prompt"], "hidden")
|
self.assertEqual(challenge_response.validated_data["hidden_prompt"], "hidden")
|
||||||
self.assertNotEqual(challenge_response.validated_data["static_prompt"], "foo")
|
self.assertNotEqual(challenge_response.validated_data["static_prompt"], "foo")
|
||||||
|
self.assertEqual(challenge_response.validated_data["alert_prompt"], "alert content")
|
||||||
|
|
||||||
def test_prompt_placeholder(self):
|
def test_prompt_placeholder(self):
|
||||||
"""Test placeholder and expression"""
|
"""Test placeholder and expression"""
|
||||||
|
|||||||
306
blueprints/example/flow-default-account-lockdown.yaml
Normal file
306
blueprints/example/flow-default-account-lockdown.yaml
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
version: 1
|
||||||
|
metadata:
|
||||||
|
name: Example - Account lockdown flow
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/instantiate: "false"
|
||||||
|
entries:
|
||||||
|
flows:
|
||||||
|
# Main lockdown flow - requires authentication
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
designation: stage_configuration
|
||||||
|
name: Account Lockdown
|
||||||
|
title: Lock Account
|
||||||
|
authentication: require_authenticated
|
||||||
|
identifiers:
|
||||||
|
slug: default-account-lockdown
|
||||||
|
model: authentik_flows.flow
|
||||||
|
id: flow
|
||||||
|
# Self-service completion flow - no authentication required
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
designation: stage_configuration
|
||||||
|
name: Account Lockdown Complete
|
||||||
|
title: Account Locked
|
||||||
|
authentication: none
|
||||||
|
identifiers:
|
||||||
|
slug: default-account-lockdown-complete
|
||||||
|
model: authentik_flows.flow
|
||||||
|
id: completion-flow
|
||||||
|
prompt_fields:
|
||||||
|
# Warning field - danger alert box (content varies based on self-service vs admin)
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
order: 50
|
||||||
|
initial_value: |
|
||||||
|
target_uuid = (http_request.session.get("authentik/flows/get", {}) or {}).get("user_uuid")
|
||||||
|
current_user_uuid = str(getattr(user, "pk", "") or getattr(http_request.user, "pk", ""))
|
||||||
|
is_self_service = not target_uuid or target_uuid == current_user_uuid
|
||||||
|
pending_user = None
|
||||||
|
if target_uuid and not is_self_service:
|
||||||
|
from authentik.core.models import User
|
||||||
|
|
||||||
|
pending_user = User.objects.filter(pk=target_uuid).first()
|
||||||
|
if is_self_service:
|
||||||
|
return (
|
||||||
|
"<p><strong>You are about to lock down your own account.</strong></p>"
|
||||||
|
"<p>This is an emergency action for cutting off access to your account right away.</p>"
|
||||||
|
"<p><strong>This will immediately:</strong></p>"
|
||||||
|
"<ul>"
|
||||||
|
"<li><strong>Invalidate your password</strong> - Your password will be set to a random value "
|
||||||
|
"and cannot be recovered</li>"
|
||||||
|
"<li><strong>Deactivate your account</strong> - Your account will be disabled</li>"
|
||||||
|
"<li><strong>Terminate all your sessions</strong> - You will be logged out everywhere</li>"
|
||||||
|
"<li><strong>Revoke all your tokens</strong> - All your API, app password, recovery, "
|
||||||
|
"verification, and OAuth2 tokens and grants will be revoked</li>"
|
||||||
|
"</ul>"
|
||||||
|
"<p><strong>This action cannot be easily undone.</strong></p>"
|
||||||
|
)
|
||||||
|
|
||||||
|
from django.utils.html import escape
|
||||||
|
|
||||||
|
if pending_user:
|
||||||
|
email = escape(pending_user.email or pending_user.name or "No email")
|
||||||
|
user_html = f"<p><code>{escape(pending_user.username)}</code> ({email})</p>"
|
||||||
|
else:
|
||||||
|
user_html = "<p>the account selected when this one-time lockdown link was created</p>"
|
||||||
|
|
||||||
|
return (
|
||||||
|
"<p><strong>You are about to lock down the following account:</strong></p>"
|
||||||
|
f"{user_html}"
|
||||||
|
"<p>This is an emergency action for cutting off access to the account right away. "
|
||||||
|
"It does not lock the administrator who opened this page.</p>"
|
||||||
|
"<p><strong>This will immediately:</strong></p>"
|
||||||
|
"<ul>"
|
||||||
|
"<li>Invalidate the user's password</li>"
|
||||||
|
"<li>Deactivate the user</li>"
|
||||||
|
"<li>Terminate all sessions - All active sessions will be ended</li>"
|
||||||
|
"<li>Revoke all tokens - All API, app password, recovery, verification, and OAuth2 "
|
||||||
|
"tokens and grants will be revoked</li>"
|
||||||
|
"</ul>"
|
||||||
|
"<p><strong>This action cannot be easily undone.</strong></p>"
|
||||||
|
)
|
||||||
|
initial_value_expression: true
|
||||||
|
required: false
|
||||||
|
type: alert_danger
|
||||||
|
field_key: lockdown_warning
|
||||||
|
label: Warning
|
||||||
|
sub_text: ""
|
||||||
|
identifiers:
|
||||||
|
name: default-account-lockdown-field-warning
|
||||||
|
id: prompt-field-warning
|
||||||
|
model: authentik_stages_prompt.prompt
|
||||||
|
# Info field - when to use lockdown (content varies based on self-service vs admin)
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
order: 100
|
||||||
|
initial_value: |
|
||||||
|
target_uuid = (http_request.session.get("authentik/flows/get", {}) or {}).get("user_uuid")
|
||||||
|
current_user_uuid = str(getattr(user, "pk", "") or getattr(http_request.user, "pk", ""))
|
||||||
|
is_self_service = not target_uuid or target_uuid == current_user_uuid
|
||||||
|
if is_self_service:
|
||||||
|
info = (
|
||||||
|
"Use this if you no longer trust your current password or sessions. "
|
||||||
|
"After lockdown, you will need help from your administrator or security team to regain access."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
info = (
|
||||||
|
"Use this for incident response on the listed account, for example after a compromise report "
|
||||||
|
"or suspicious activity. The reason you enter below will be recorded in the audit log."
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"<p>{info}</p>"
|
||||||
|
'<p><a href="https://docs.goauthentik.io/docs/security/'
|
||||||
|
'account-lockdown?utm_source=authentik" '
|
||||||
|
'target="_blank" rel="noopener noreferrer">Learn more about account lockdown</a></p>'
|
||||||
|
)
|
||||||
|
initial_value_expression: true
|
||||||
|
required: false
|
||||||
|
type: alert_info
|
||||||
|
field_key: lockdown_info
|
||||||
|
label: Information
|
||||||
|
sub_text: ""
|
||||||
|
identifiers:
|
||||||
|
name: default-account-lockdown-field-info
|
||||||
|
id: prompt-field-info
|
||||||
|
model: authentik_stages_prompt.prompt
|
||||||
|
# Reason field - text area for lockdown reason
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
order: 200
|
||||||
|
placeholder: |
|
||||||
|
target_uuid = (http_request.session.get("authentik/flows/get", {}) or {}).get("user_uuid")
|
||||||
|
current_user_uuid = str(getattr(user, "pk", "") or getattr(http_request.user, "pk", ""))
|
||||||
|
is_self_service = not target_uuid or target_uuid == current_user_uuid
|
||||||
|
if is_self_service:
|
||||||
|
return "Describe why you are locking your account..."
|
||||||
|
return "Describe why this account is being locked down..."
|
||||||
|
placeholder_expression: true
|
||||||
|
required: true
|
||||||
|
type: text_area
|
||||||
|
field_key: lockdown_reason
|
||||||
|
label: Reason
|
||||||
|
sub_text: This explanation will be recorded in the audit log.
|
||||||
|
identifiers:
|
||||||
|
name: default-account-lockdown-field-reason
|
||||||
|
id: prompt-field-reason
|
||||||
|
model: authentik_stages_prompt.prompt
|
||||||
|
prompt_stages:
|
||||||
|
# Prompt stage for warnings and reason input
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
fields:
|
||||||
|
- !KeyOf prompt-field-warning
|
||||||
|
- !KeyOf prompt-field-info
|
||||||
|
- !KeyOf prompt-field-reason
|
||||||
|
identifiers:
|
||||||
|
name: default-account-lockdown-prompt
|
||||||
|
id: default-account-lockdown-prompt
|
||||||
|
model: authentik_stages_prompt.promptstage
|
||||||
|
lockdown_stage:
|
||||||
|
# Account lockdown stage - performs the actual lockdown
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
identifiers:
|
||||||
|
name: default-account-lockdown-stage
|
||||||
|
id: default-account-lockdown-stage
|
||||||
|
model: authentik_stages_account_lockdown.accountlockdownstage
|
||||||
|
attrs:
|
||||||
|
deactivate_user: true
|
||||||
|
set_unusable_password: true
|
||||||
|
delete_sessions: true
|
||||||
|
revoke_tokens: true
|
||||||
|
self_service_completion_flow: !Find [authentik_flows.flow, [slug, default-account-lockdown-complete]]
|
||||||
|
completion_prompt:
|
||||||
|
# Completion message field - confirmation shown after an admin-triggered lockdown
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
order: 300
|
||||||
|
initial_value: |
|
||||||
|
target_uuid = (http_request.session.get("authentik/flows/get", {}) or {}).get("user_uuid")
|
||||||
|
from django.utils.html import escape
|
||||||
|
from authentik.core.models import User
|
||||||
|
|
||||||
|
if target_uuid:
|
||||||
|
target = User.objects.filter(pk=target_uuid).first()
|
||||||
|
if target:
|
||||||
|
return f"<p><code>{escape(target.username)}</code> has been locked down.</p>"
|
||||||
|
|
||||||
|
return "<p>The selected account has been locked down.</p>"
|
||||||
|
initial_value_expression: true
|
||||||
|
required: false
|
||||||
|
type: alert_info
|
||||||
|
field_key: lockdown_complete
|
||||||
|
label: Result
|
||||||
|
sub_text: ""
|
||||||
|
identifiers:
|
||||||
|
name: default-account-lockdown-field-complete
|
||||||
|
id: prompt-field-complete
|
||||||
|
model: authentik_stages_prompt.prompt
|
||||||
|
# Prompt stage for admin completion message
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
fields:
|
||||||
|
- !KeyOf prompt-field-complete
|
||||||
|
identifiers:
|
||||||
|
name: default-account-lockdown-complete-prompt
|
||||||
|
id: default-account-lockdown-complete-prompt
|
||||||
|
model: authentik_stages_prompt.promptstage
|
||||||
|
policies:
|
||||||
|
# Expression policy to check if this is NOT a self-service lockdown (admin)
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
name: default-account-lockdown-admin-policy
|
||||||
|
expression: |
|
||||||
|
target_uuid = (request.http_request.session.get("authentik/flows/get", {}) or {}).get("user_uuid")
|
||||||
|
current_user_uuid = str(getattr(request.user, "pk", "") or getattr(request.http_request.user, "pk", ""))
|
||||||
|
return bool(target_uuid) and target_uuid != current_user_uuid
|
||||||
|
identifiers:
|
||||||
|
name: default-account-lockdown-admin-policy
|
||||||
|
id: admin-policy
|
||||||
|
model: authentik_policies_expression.expressionpolicy
|
||||||
|
bindings:
|
||||||
|
# Stage bindings
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
identifiers:
|
||||||
|
order: 0
|
||||||
|
stage: !KeyOf default-account-lockdown-prompt
|
||||||
|
target: !KeyOf flow
|
||||||
|
model: authentik_flows.flowstagebinding
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
identifiers:
|
||||||
|
order: 10
|
||||||
|
stage: !KeyOf default-account-lockdown-stage
|
||||||
|
target: !KeyOf flow
|
||||||
|
model: authentik_flows.flowstagebinding
|
||||||
|
# Admin completion stage binding - shown for admin lockdown only
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
identifiers:
|
||||||
|
order: 20
|
||||||
|
stage: !KeyOf default-account-lockdown-complete-prompt
|
||||||
|
target: !KeyOf flow
|
||||||
|
id: admin-completion-binding
|
||||||
|
model: authentik_flows.flowstagebinding
|
||||||
|
# Bind the admin policy to the admin completion stage
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
enabled: true
|
||||||
|
negate: false
|
||||||
|
order: 0
|
||||||
|
identifiers:
|
||||||
|
policy: !KeyOf admin-policy
|
||||||
|
target: !KeyOf admin-completion-binding
|
||||||
|
model: authentik_policies.policybinding
|
||||||
|
self_service_completion:
|
||||||
|
# Self-service completion message field (for the unauthenticated completion flow)
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
order: 100
|
||||||
|
initial_value: |
|
||||||
|
return (
|
||||||
|
"<h1>Your account has been locked</h1>"
|
||||||
|
"<p>You have been logged out of all sessions and your password has been invalidated.</p>"
|
||||||
|
"<p>To regain access to your account, please contact your IT administrator or security team.</p>"
|
||||||
|
)
|
||||||
|
initial_value_expression: true
|
||||||
|
required: false
|
||||||
|
type: alert_warning
|
||||||
|
field_key: self_lockdown_complete
|
||||||
|
label: Account locked
|
||||||
|
sub_text: ""
|
||||||
|
identifiers:
|
||||||
|
name: default-account-lockdown-self-field-complete
|
||||||
|
id: self-prompt-field-complete
|
||||||
|
model: authentik_stages_prompt.prompt
|
||||||
|
# Prompt stage for self-service completion (unauthenticated)
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
attrs:
|
||||||
|
fields:
|
||||||
|
- !KeyOf self-prompt-field-complete
|
||||||
|
identifiers:
|
||||||
|
name: default-account-lockdown-self-complete-prompt
|
||||||
|
id: default-account-lockdown-self-complete-prompt
|
||||||
|
model: authentik_stages_prompt.promptstage
|
||||||
|
# Bind self-service completion stage to the completion flow
|
||||||
|
- conditions:
|
||||||
|
- !Context goauthentik.io/enterprise/licensed
|
||||||
|
identifiers:
|
||||||
|
order: 0
|
||||||
|
stage: !KeyOf default-account-lockdown-self-complete-prompt
|
||||||
|
target: !Find [authentik_flows.flow, [slug, default-account-lockdown-complete]]
|
||||||
|
model: authentik_flows.flowstagebinding
|
||||||
@@ -1216,6 +1216,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"model",
|
||||||
|
"identifiers"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"const": "authentik_stages_account_lockdown.accountlockdownstage"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"absent",
|
||||||
|
"created",
|
||||||
|
"must_created",
|
||||||
|
"present"
|
||||||
|
],
|
||||||
|
"default": "present"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"$ref": "#/$defs/model_authentik_stages_account_lockdown.accountlockdownstage_permissions"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"$ref": "#/$defs/model_authentik_stages_account_lockdown.accountlockdownstage"
|
||||||
|
},
|
||||||
|
"identifiers": {
|
||||||
|
"$ref": "#/$defs/model_authentik_stages_account_lockdown.accountlockdownstage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -5100,6 +5140,11 @@
|
|||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"title": "Flow device code"
|
"title": "Flow device code"
|
||||||
},
|
},
|
||||||
|
"flow_lockdown": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Flow lockdown"
|
||||||
|
},
|
||||||
"default_application": {
|
"default_application": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
@@ -6094,6 +6139,10 @@
|
|||||||
"authentik_sources_telegram.view_telegramsource",
|
"authentik_sources_telegram.view_telegramsource",
|
||||||
"authentik_sources_telegram.view_telegramsourcepropertymapping",
|
"authentik_sources_telegram.view_telegramsourcepropertymapping",
|
||||||
"authentik_sources_telegram.view_usertelegramsourceconnection",
|
"authentik_sources_telegram.view_usertelegramsourceconnection",
|
||||||
|
"authentik_stages_account_lockdown.add_accountlockdownstage",
|
||||||
|
"authentik_stages_account_lockdown.change_accountlockdownstage",
|
||||||
|
"authentik_stages_account_lockdown.delete_accountlockdownstage",
|
||||||
|
"authentik_stages_account_lockdown.view_accountlockdownstage",
|
||||||
"authentik_stages_authenticator_duo.add_authenticatorduostage",
|
"authentik_stages_authenticator_duo.add_authenticatorduostage",
|
||||||
"authentik_stages_authenticator_duo.add_duodevice",
|
"authentik_stages_authenticator_duo.add_duodevice",
|
||||||
"authentik_stages_authenticator_duo.change_authenticatorduostage",
|
"authentik_stages_authenticator_duo.change_authenticatorduostage",
|
||||||
@@ -7757,6 +7806,69 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"model_authentik_stages_account_lockdown.accountlockdownstage": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Name"
|
||||||
|
},
|
||||||
|
"deactivate_user": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Deactivate user",
|
||||||
|
"description": "Deactivate the user account (set is_active to False)"
|
||||||
|
},
|
||||||
|
"set_unusable_password": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Set unusable password",
|
||||||
|
"description": "Set an unusable password for the user"
|
||||||
|
},
|
||||||
|
"delete_sessions": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Delete sessions",
|
||||||
|
"description": "Delete all active sessions for the user"
|
||||||
|
},
|
||||||
|
"revoke_tokens": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Revoke tokens",
|
||||||
|
"description": "Revoke all tokens for the user (API, app password, recovery, verification, OAuth)"
|
||||||
|
},
|
||||||
|
"self_service_completion_flow": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Self service completion flow",
|
||||||
|
"description": "Flow to redirect users to after self-service lockdown. This flow should not require authentication since the user's session is deleted."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"model_authentik_stages_account_lockdown.accountlockdownstage_permissions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"permission"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"permission": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"add_accountlockdownstage",
|
||||||
|
"change_accountlockdownstage",
|
||||||
|
"delete_accountlockdownstage",
|
||||||
|
"view_accountlockdownstage"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage": {
|
"model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -8952,6 +9064,7 @@
|
|||||||
"authentik.enterprise.providers.ssf",
|
"authentik.enterprise.providers.ssf",
|
||||||
"authentik.enterprise.providers.ws_federation",
|
"authentik.enterprise.providers.ws_federation",
|
||||||
"authentik.enterprise.reports",
|
"authentik.enterprise.reports",
|
||||||
|
"authentik.enterprise.stages.account_lockdown",
|
||||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||||
"authentik.enterprise.stages.mtls",
|
"authentik.enterprise.stages.mtls",
|
||||||
"authentik.enterprise.stages.source"
|
"authentik.enterprise.stages.source"
|
||||||
@@ -9084,6 +9197,7 @@
|
|||||||
"authentik_providers_ssf.ssfprovider",
|
"authentik_providers_ssf.ssfprovider",
|
||||||
"authentik_providers_ws_federation.wsfederationprovider",
|
"authentik_providers_ws_federation.wsfederationprovider",
|
||||||
"authentik_reports.dataexport",
|
"authentik_reports.dataexport",
|
||||||
|
"authentik_stages_account_lockdown.accountlockdownstage",
|
||||||
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
||||||
"authentik_stages_mtls.mutualtlsstage",
|
"authentik_stages_mtls.mutualtlsstage",
|
||||||
"authentik_stages_source.sourcestage"
|
"authentik_stages_source.sourcestage"
|
||||||
@@ -11791,6 +11905,10 @@
|
|||||||
"authentik_sources_telegram.view_telegramsource",
|
"authentik_sources_telegram.view_telegramsource",
|
||||||
"authentik_sources_telegram.view_telegramsourcepropertymapping",
|
"authentik_sources_telegram.view_telegramsourcepropertymapping",
|
||||||
"authentik_sources_telegram.view_usertelegramsourceconnection",
|
"authentik_sources_telegram.view_usertelegramsourceconnection",
|
||||||
|
"authentik_stages_account_lockdown.add_accountlockdownstage",
|
||||||
|
"authentik_stages_account_lockdown.change_accountlockdownstage",
|
||||||
|
"authentik_stages_account_lockdown.delete_accountlockdownstage",
|
||||||
|
"authentik_stages_account_lockdown.view_accountlockdownstage",
|
||||||
"authentik_stages_authenticator_duo.add_authenticatorduostage",
|
"authentik_stages_authenticator_duo.add_authenticatorduostage",
|
||||||
"authentik_stages_authenticator_duo.add_duodevice",
|
"authentik_stages_authenticator_duo.add_duodevice",
|
||||||
"authentik_stages_authenticator_duo.change_authenticatorduostage",
|
"authentik_stages_authenticator_duo.change_authenticatorduostage",
|
||||||
@@ -15657,6 +15775,9 @@
|
|||||||
"separator",
|
"separator",
|
||||||
"hidden",
|
"hidden",
|
||||||
"static",
|
"static",
|
||||||
|
"alert_info",
|
||||||
|
"alert_warning",
|
||||||
|
"alert_danger",
|
||||||
"ak-locale"
|
"ak-locale"
|
||||||
],
|
],
|
||||||
"title": "Type"
|
"title": "Type"
|
||||||
|
|||||||
@@ -73,8 +73,16 @@ entries:
|
|||||||
redirect_uris:
|
redirect_uris:
|
||||||
- matching_mode: strict
|
- matching_mode: strict
|
||||||
url: https://localhost:8443/test/a/authentik/callback
|
url: https://localhost:8443/test/a/authentik/callback
|
||||||
|
redirect_uri_type: authorization
|
||||||
- matching_mode: strict
|
- matching_mode: strict
|
||||||
url: https://host.docker.internal:8443/test/a/authentik/callback
|
url: https://host.docker.internal:8443/test/a/authentik/callback
|
||||||
|
redirect_uri_type: authorization
|
||||||
|
- matching_mode: strict
|
||||||
|
url: https://localhost:8443/test/a/authentik/post_logout_redirect
|
||||||
|
redirect_uri_type: logout
|
||||||
|
- matching_mode: strict
|
||||||
|
url: https://host.docker.internal:8443/test/a/authentik/post_logout_redirect
|
||||||
|
redirect_uri_type: logout
|
||||||
grant_types:
|
grant_types:
|
||||||
- authorization_code
|
- authorization_code
|
||||||
- implicit
|
- implicit
|
||||||
@@ -108,8 +116,16 @@ entries:
|
|||||||
redirect_uris:
|
redirect_uris:
|
||||||
- matching_mode: strict
|
- matching_mode: strict
|
||||||
url: https://localhost:8443/test/a/authentik/callback
|
url: https://localhost:8443/test/a/authentik/callback
|
||||||
|
redirect_uri_type: authorization
|
||||||
- matching_mode: strict
|
- matching_mode: strict
|
||||||
url: https://host.docker.internal:8443/test/a/authentik/callback
|
url: https://host.docker.internal:8443/test/a/authentik/callback
|
||||||
|
redirect_uri_type: authorization
|
||||||
|
- matching_mode: strict
|
||||||
|
url: https://localhost:8443/test/a/authentik/post_logout_redirect
|
||||||
|
redirect_uri_type: logout
|
||||||
|
- matching_mode: strict
|
||||||
|
url: https://host.docker.internal:8443/test/a/authentik/post_logout_redirect
|
||||||
|
redirect_uri_type: logout
|
||||||
grant_types:
|
grant_types:
|
||||||
- authorization_code
|
- authorization_code
|
||||||
- implicit
|
- implicit
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ var healthcheckCmd = &cobra.Command{
|
|||||||
exitCode := 1
|
exitCode := 1
|
||||||
log.WithField("mode", mode).Debug("checking health")
|
log.WithField("mode", mode).Debug("checking health")
|
||||||
switch strings.ToLower(mode) {
|
switch strings.ToLower(mode) {
|
||||||
|
case "allinone":
|
||||||
|
fallthrough
|
||||||
case "server":
|
case "server":
|
||||||
exitCode = check(fmt.Sprintf("http://localhost%s-/health/live/", config.Get().Web.Path))
|
exitCode = check(fmt.Sprintf("http://localhost%s-/health/live/", config.Get().Web.Path))
|
||||||
case "worker":
|
case "worker":
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -7,7 +7,7 @@ require (
|
|||||||
beryju.io/radius-eap v0.1.0
|
beryju.io/radius-eap v0.1.0
|
||||||
github.com/avast/retry-go/v4 v4.7.0
|
github.com/avast/retry-go/v4 v4.7.0
|
||||||
github.com/coreos/go-oidc/v3 v3.18.0
|
github.com/coreos/go-oidc/v3 v3.18.0
|
||||||
github.com/getsentry/sentry-go v0.46.0
|
github.com/getsentry/sentry-go v0.46.1
|
||||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13
|
github.com/go-ldap/ldap/v3 v3.4.13
|
||||||
github.com/go-openapi/runtime v0.29.4
|
github.com/go-openapi/runtime v0.29.4
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/getsentry/sentry-go v0.46.0 h1:mbdDaarbUdOt9X+dx6kDdntkShLEX3/+KyOsVDTPDj0=
|
github.com/getsentry/sentry-go v0.46.1 h1:mZyQFaQYkPxAdDG4HR8gDg6j4CnKYVWt4TF92N7i3XY=
|
||||||
github.com/getsentry/sentry-go v0.46.0/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
|
github.com/getsentry/sentry-go v0.46.1/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function run_authentik {
|
|||||||
echo go run ./cmd/server "$@"
|
echo go run ./cmd/server "$@"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
worker)
|
allinone | worker)
|
||||||
if [[ -x "$(command -v authentik)" ]]; then
|
if [[ -x "$(command -v authentik)" ]]; then
|
||||||
echo authentik "$@"
|
echo authentik "$@"
|
||||||
else
|
else
|
||||||
@@ -105,7 +105,7 @@ elif [[ "$1" == "test-all" ]]; then
|
|||||||
prepare_debug
|
prepare_debug
|
||||||
chmod 777 /root
|
chmod 777 /root
|
||||||
check_if_root_and_run manage test authentik
|
check_if_root_and_run manage test authentik
|
||||||
elif [[ "$1" == "server" ]] || [[ "$1" == "worker" ]]; then
|
elif [[ "$1" == "allinone" ]] || [[ "$1" == "server" ]] || [[ "$1" == "worker" ]]; then
|
||||||
wait_for_db
|
wait_for_db
|
||||||
check_if_root_and_run "$@"
|
check_if_root_and_run "$@"
|
||||||
elif [[ "$1" == "healthcheck" ]]; then
|
elif [[ "$1" == "healthcheck" ]]; then
|
||||||
|
|||||||
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aws-cdk": "^2.1118.4",
|
"aws-cdk": "^2.1119.0",
|
||||||
"cross-env": "^10.1.0"
|
"cross-env": "^10.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -25,9 +25,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/aws-cdk": {
|
"node_modules/aws-cdk": {
|
||||||
"version": "2.1118.4",
|
"version": "2.1119.0",
|
||||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1118.4.tgz",
|
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1119.0.tgz",
|
||||||
"integrity": "sha512-wJfRQdvb+FJ2cni059mYdmjhfwhMskP+PAB59BL9jhon+jYtjy8X3pbj3uzHgAOJwNhh6jGkP8xq36Cffccbbw==",
|
"integrity": "sha512-XBxZEKH3BY4M1EX6x0qBkmOAj8viErjpww14iH6Z3z6nI0YzjZeJ05eEl7eJwzUgv7NTGagWBS9m/eDJW5+dAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml"
|
"aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aws-cdk": "^2.1118.4",
|
"aws-cdk": "^2.1119.0",
|
||||||
"cross-env": "^10.1.0"
|
"cross-env": "^10.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -28,20 +28,45 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
_ = db_conn.cursor()
|
_ = db_conn.cursor()
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path == "/-/metrics/":
|
from django.db import (
|
||||||
from authentik.root.monitoring import monitoring_set
|
DatabaseError,
|
||||||
|
InterfaceError,
|
||||||
|
OperationalError,
|
||||||
|
connections,
|
||||||
|
)
|
||||||
|
from psycopg.errors import AdminShutdown
|
||||||
|
|
||||||
monitoring_set.send_robust(self)
|
from authentik.root.monitoring import monitoring_set
|
||||||
self.send_response(200)
|
|
||||||
|
DATABASE_ERRORS = (
|
||||||
|
AdminShutdown,
|
||||||
|
InterfaceError,
|
||||||
|
DatabaseError,
|
||||||
|
ConnectionError,
|
||||||
|
OperationalError,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.path == "/-/metrics/":
|
||||||
|
try:
|
||||||
|
monitoring_set.send(self)
|
||||||
|
except DATABASE_ERRORS as exc:
|
||||||
|
LOGGER.warning("failed to send monitoring_set", exc=exc)
|
||||||
|
for db_conn in connections.all():
|
||||||
|
db_conn.close()
|
||||||
|
self.send_response(503)
|
||||||
|
else:
|
||||||
|
self.send_response(200)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
elif self.path == "/-/health/ready/":
|
elif self.path == "/-/health/ready/":
|
||||||
from django.db.utils import OperationalError
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.check_db()
|
self.check_db()
|
||||||
except OperationalError:
|
except DATABASE_ERRORS as exc:
|
||||||
|
LOGGER.warning("failed to check database health", exc=exc)
|
||||||
|
for db_conn in connections.all():
|
||||||
|
db_conn.close()
|
||||||
self.send_response(503)
|
self.send_response(503)
|
||||||
self.send_response(200)
|
else:
|
||||||
|
self.send_response(200)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
else:
|
else:
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-04-29 00:28+0000\n"
|
"POT-Creation-Date: 2026-05-01 03:47+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -224,6 +224,14 @@ msgid ""
|
|||||||
"providers are returned. When set to false, backchannel providers are excluded"
|
"providers are returned. When set to false, backchannel providers are excluded"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/core/api/users.py
|
||||||
|
msgid "Invalid password hash format. Must be a valid Django password hash."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/core/api/users.py
|
||||||
|
msgid "Cannot set both password and password_hash. Use only one."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/api/users.py
|
#: authentik/core/api/users.py
|
||||||
msgid "No leading or trailing slashes allowed."
|
msgid "No leading or trailing slashes allowed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -864,10 +872,6 @@ msgstr ""
|
|||||||
msgid "Grace period must be shorter than the interval."
|
msgid "Grace period must be shorter than the interval."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/api/rules.py
|
|
||||||
msgid "Only one type-wide rule for each object type is allowed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/models.py
|
#: authentik/enterprise/lifecycle/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Select which transports should be used to notify the reviewers. If none are "
|
"Select which transports should be used to notify the reviewers. If none are "
|
||||||
@@ -895,7 +899,8 @@ msgid "Go to {self._get_model_name()}"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/models.py
|
#: authentik/enterprise/lifecycle/models.py
|
||||||
msgid "Access review is due for {self.content_type.name} {str(self.object)}"
|
msgid ""
|
||||||
|
"Access review is due for {self.content_type.name.lower()} {object_label}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/models.py
|
#: authentik/enterprise/lifecycle/models.py
|
||||||
@@ -908,7 +913,7 @@ msgid "Access review completed for {self.content_type.name} {str(self.object)}"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/tasks.py
|
#: authentik/enterprise/lifecycle/tasks.py
|
||||||
msgid "Dispatch tasks to validate lifecycle rules."
|
msgid "Dispatch tasks to apply lifecycle rules."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/tasks.py
|
#: authentik/enterprise/lifecycle/tasks.py
|
||||||
@@ -1221,6 +1226,78 @@ msgstr ""
|
|||||||
msgid "Generate data export."
|
msgid "Generate data export."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "User to lock. If omitted, locks the current user (self-service)."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "No lockdown flow configured."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "Lockdown flow is not applicable."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "Choose the target account, then return a flow link."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "No lockdown flow configured or the flow is not applicable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "Permission denied (when targeting another user)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid "Deactivate the user account (set is_active to False)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid "Set an unusable password for the user"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid "Delete all active sessions for the user"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid ""
|
||||||
|
"Revoke all tokens for the user (API, app password, recovery, verification, "
|
||||||
|
"OAuth)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid ""
|
||||||
|
"Flow to redirect users to after self-service lockdown. This flow should not "
|
||||||
|
"require authentication since the user's session is deleted."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid "Account Lockdown Stage"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid "Account Lockdown Stages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/stage.py
|
||||||
|
msgid "No target user specified for account lockdown"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/stage.py
|
||||||
|
msgid "You do not have permission to lock down this account."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/stage.py
|
||||||
|
msgid "Account lockdown failed for this account."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/stage.py
|
||||||
|
msgid "Self-service account lockdown requires a completion flow."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -4448,6 +4525,18 @@ msgstr ""
|
|||||||
msgid "Static: Static value, displayed as-is."
|
msgid "Static: Static value, displayed as-is."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/stages/prompt/models.py
|
||||||
|
msgid "Alert (Info): Static alert box with info styling"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/stages/prompt/models.py
|
||||||
|
msgid "Alert (Warning): Static alert box with warning styling"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/stages/prompt/models.py
|
||||||
|
msgid "Alert (Danger): Static alert box with danger styling"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/stages/prompt/models.py
|
#: authentik/stages/prompt/models.py
|
||||||
msgid "authentik: Selection of locales authentik supports"
|
msgid "authentik: Selection of locales authentik supports"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Forti
|
|||||||
Fortigate
|
Fortigate
|
||||||
Gatus
|
Gatus
|
||||||
Gestionnaire
|
Gestionnaire
|
||||||
|
ghec
|
||||||
Gitea
|
Gitea
|
||||||
Gravitee
|
Gravitee
|
||||||
Homarr
|
Homarr
|
||||||
@@ -52,6 +53,7 @@ Relatedly
|
|||||||
Sidero
|
Sidero
|
||||||
snipeit
|
snipeit
|
||||||
sonarqube
|
sonarqube
|
||||||
|
Technitium
|
||||||
Terrakube
|
Terrakube
|
||||||
Ueberauth
|
Ueberauth
|
||||||
Veeam
|
Veeam
|
||||||
|
|||||||
Binary file not shown.
@@ -15,7 +15,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-04-08 00:28+0000\n"
|
"POT-Creation-Date: 2026-05-01 03:47+0000\n"
|
||||||
"PO-Revision-Date: 2025-12-01 19:09+0000\n"
|
"PO-Revision-Date: 2025-12-01 19:09+0000\n"
|
||||||
"Last-Translator: Sp P, 2026\n"
|
"Last-Translator: Sp P, 2026\n"
|
||||||
"Language-Team: French (France) (https://app.transifex.com/authentik/teams/119923/fr_FR/)\n"
|
"Language-Team: French (France) (https://app.transifex.com/authentik/teams/119923/fr_FR/)\n"
|
||||||
@@ -263,6 +263,18 @@ msgstr ""
|
|||||||
"fournisseurs backchannels sont retournés. Si faux, les fournisseurs "
|
"fournisseurs backchannels sont retournés. Si faux, les fournisseurs "
|
||||||
"backchannels sont exclus"
|
"backchannels sont exclus"
|
||||||
|
|
||||||
|
#: authentik/core/api/users.py
|
||||||
|
msgid "Invalid password hash format. Must be a valid Django password hash."
|
||||||
|
msgstr ""
|
||||||
|
"Format de hachage de mot de passe invalide. Cela doit être un hachage de mot"
|
||||||
|
" de passe Django valide."
|
||||||
|
|
||||||
|
#: authentik/core/api/users.py
|
||||||
|
msgid "Cannot set both password and password_hash. Use only one."
|
||||||
|
msgstr ""
|
||||||
|
"Impossible de définir à la fois password (mot de passe) et password_hash "
|
||||||
|
"(hachage de mot de passe). N'en utiliser qu'un seul."
|
||||||
|
|
||||||
#: authentik/core/api/users.py
|
#: authentik/core/api/users.py
|
||||||
msgid "No leading or trailing slashes allowed."
|
msgid "No leading or trailing slashes allowed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -443,6 +455,11 @@ msgid "Open launch URL in a new browser tab or window."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Ouvrir l'URL de lancement dans une nouvelle fenêtre ou un nouvel onglet."
|
"Ouvrir l'URL de lancement dans une nouvelle fenêtre ou un nouvel onglet."
|
||||||
|
|
||||||
|
#: authentik/core/models.py
|
||||||
|
msgid "Hide this application from the user's My applications page."
|
||||||
|
msgstr ""
|
||||||
|
"Masquer cette application dans la page Mes applications de l'utilisateur."
|
||||||
|
|
||||||
#: authentik/core/models.py
|
#: authentik/core/models.py
|
||||||
msgid "Application"
|
msgid "Application"
|
||||||
msgstr "Application"
|
msgstr "Application"
|
||||||
@@ -810,6 +827,14 @@ msgstr "Nonce Apple"
|
|||||||
msgid "Apple Nonces"
|
msgid "Apple Nonces"
|
||||||
msgstr "Nonces Apple"
|
msgstr "Nonces Apple"
|
||||||
|
|
||||||
|
#: authentik/endpoints/connectors/agent/models.py
|
||||||
|
msgid "Apple Independent Secure Enclave"
|
||||||
|
msgstr "Secure Enclave indépendante d'Apple"
|
||||||
|
|
||||||
|
#: authentik/endpoints/connectors/agent/models.py
|
||||||
|
msgid "Apple Independent Secure Enclaves"
|
||||||
|
msgstr "Secure Enclaves indépendantes d'Apple"
|
||||||
|
|
||||||
#: authentik/endpoints/facts.py
|
#: authentik/endpoints/facts.py
|
||||||
msgid "Operating System name, such as 'Server 2022' or 'Ubuntu'"
|
msgid "Operating System name, such as 'Server 2022' or 'Ubuntu'"
|
||||||
msgstr "Nom du système d'exploitation, comme 'Server 2022' ou 'Ubuntu'"
|
msgstr "Nom du système d'exploitation, comme 'Server 2022' ou 'Ubuntu'"
|
||||||
@@ -936,12 +961,6 @@ msgstr "Soit un groupe de réviseurs soit un réviseur doit être défini."
|
|||||||
msgid "Grace period must be shorter than the interval."
|
msgid "Grace period must be shorter than the interval."
|
||||||
msgstr "La période de grâce doit être plus courte que l'intervalle."
|
msgstr "La période de grâce doit être plus courte que l'intervalle."
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/api/rules.py
|
|
||||||
msgid "Only one type-wide rule for each object type is allowed."
|
|
||||||
msgstr ""
|
|
||||||
"Une seule règle pour l'ensemble du type est autorisée pour chaque type "
|
|
||||||
"d'objet."
|
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/models.py
|
#: authentik/enterprise/lifecycle/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Select which transports should be used to notify the reviewers. If none are "
|
"Select which transports should be used to notify the reviewers. If none are "
|
||||||
@@ -972,10 +991,11 @@ msgid "Go to {self._get_model_name()}"
|
|||||||
msgstr "Aller à {self._get_model_name()}"
|
msgstr "Aller à {self._get_model_name()}"
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/models.py
|
#: authentik/enterprise/lifecycle/models.py
|
||||||
msgid "Access review is due for {self.content_type.name} {str(self.object)}"
|
msgid ""
|
||||||
|
"Access review is due for {self.content_type.name.lower()} {object_label}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"La révision d'accès est attendue pour {self.content_type.name} "
|
"La révision de l'accès doit être effectuée pour "
|
||||||
"{str(self.object)}"
|
"{self.content_type.name.lower()} {object_label}"
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/models.py
|
#: authentik/enterprise/lifecycle/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -992,8 +1012,8 @@ msgstr ""
|
|||||||
"{str(self.object)}"
|
"{str(self.object)}"
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/tasks.py
|
#: authentik/enterprise/lifecycle/tasks.py
|
||||||
msgid "Dispatch tasks to validate lifecycle rules."
|
msgid "Dispatch tasks to apply lifecycle rules."
|
||||||
msgstr "Déclenche les tâches pour valider les règles de cycle de vie"
|
msgstr "Déclencher les tâches pour appliquer les règles de cycle de vie"
|
||||||
|
|
||||||
#: authentik/enterprise/lifecycle/tasks.py
|
#: authentik/enterprise/lifecycle/tasks.py
|
||||||
msgid "Apply lifecycle rule."
|
msgid "Apply lifecycle rule."
|
||||||
@@ -1336,6 +1356,86 @@ msgstr "Télécharger"
|
|||||||
msgid "Generate data export."
|
msgid "Generate data export."
|
||||||
msgstr "Générer un export de données."
|
msgstr "Générer un export de données."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "User to lock. If omitted, locks the current user (self-service)."
|
||||||
|
msgstr ""
|
||||||
|
"Utilisateur à bloquer. Si non renseigné, bloque l'utilisateur actuel (libre "
|
||||||
|
"service)."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "No lockdown flow configured."
|
||||||
|
msgstr "Aucun flux de blocage configuré."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "Lockdown flow is not applicable."
|
||||||
|
msgstr "Le flux de blocage n'est pas applicable."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "Choose the target account, then return a flow link."
|
||||||
|
msgstr "Choisit le compte cible, puis renvoie un lien de flux."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "No lockdown flow configured or the flow is not applicable"
|
||||||
|
msgstr "Aucun flux de blocage configuré, ou le flux n'est pas applicable"
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/api.py
|
||||||
|
msgid "Permission denied (when targeting another user)"
|
||||||
|
msgstr "Permission refusée (lors du ciblage d'un autre utilisateur)"
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid "Deactivate the user account (set is_active to False)"
|
||||||
|
msgstr "Désactiver le compte de l'utilisateur (définir is_active à False)."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid "Set an unusable password for the user"
|
||||||
|
msgstr "Définit un mot de passe inutilisable pour cet utilisateur."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid "Delete all active sessions for the user"
|
||||||
|
msgstr "Supprimer toutes les sessions actives pour cet utilisateur."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid ""
|
||||||
|
"Revoke all tokens for the user (API, app password, recovery, verification, "
|
||||||
|
"OAuth)"
|
||||||
|
msgstr ""
|
||||||
|
"Révoquer tous les jetons pour cet utilisateur (API, mot de passe applicatif,"
|
||||||
|
" récupération, vérification, OAuth)"
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid ""
|
||||||
|
"Flow to redirect users to after self-service lockdown. This flow should not "
|
||||||
|
"require authentication since the user's session is deleted."
|
||||||
|
msgstr ""
|
||||||
|
"Flux vers lequel rediriger les utilisateurs après le blocage en libre "
|
||||||
|
"service. Ce flux ne doit pas nécessiter d'authentification car la session "
|
||||||
|
"utilisateur est supprimée."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid "Account Lockdown Stage"
|
||||||
|
msgstr "Etape de blocage de compte"
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/models.py
|
||||||
|
msgid "Account Lockdown Stages"
|
||||||
|
msgstr "Etapes de blocage de compte"
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/stage.py
|
||||||
|
msgid "No target user specified for account lockdown"
|
||||||
|
msgstr "Aucun utilisateur ciblé défini pour le blocage de compte"
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/stage.py
|
||||||
|
msgid "You do not have permission to lock down this account."
|
||||||
|
msgstr "Vous n'avez pas la permission de bloquer ce compte."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/stage.py
|
||||||
|
msgid "Account lockdown failed for this account."
|
||||||
|
msgstr "Echec du blocage de compte pour ce compte."
|
||||||
|
|
||||||
|
#: authentik/enterprise/stages/account_lockdown/stage.py
|
||||||
|
msgid "Self-service account lockdown requires a completion flow."
|
||||||
|
msgstr ""
|
||||||
|
"Le blocage de compte en libre service nécessite un flux de finalisation."
|
||||||
|
|
||||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1469,11 +1569,11 @@ msgstr "Évènement utilisateur"
|
|||||||
|
|
||||||
#: authentik/events/models.py
|
#: authentik/events/models.py
|
||||||
msgid "Notification Transport"
|
msgid "Notification Transport"
|
||||||
msgstr "Transport de Notification"
|
msgstr "Transport de notification"
|
||||||
|
|
||||||
#: authentik/events/models.py
|
#: authentik/events/models.py
|
||||||
msgid "Notification Transports"
|
msgid "Notification Transports"
|
||||||
msgstr "Transports de notification"
|
msgstr "Transports de notifications"
|
||||||
|
|
||||||
#: authentik/events/models.py
|
#: authentik/events/models.py
|
||||||
msgid "Notice"
|
msgid "Notice"
|
||||||
@@ -1745,6 +1845,10 @@ msgstr "Jeton du flux"
|
|||||||
msgid "Flow Tokens"
|
msgid "Flow Tokens"
|
||||||
msgstr "Jetons du flux"
|
msgstr "Jetons du flux"
|
||||||
|
|
||||||
|
#: authentik/flows/planner.py
|
||||||
|
msgid "This link is invalid or has expired. Please request a new one."
|
||||||
|
msgstr "Ce lien est invalide ou a expiré. Veuillez un demander un nouveau."
|
||||||
|
|
||||||
#: authentik/flows/views/executor.py
|
#: authentik/flows/views/executor.py
|
||||||
msgid "Invalid next URL"
|
msgid "Invalid next URL"
|
||||||
msgstr "URL suivante invalide"
|
msgstr "URL suivante invalide"
|
||||||
@@ -2772,8 +2876,12 @@ msgstr ""
|
|||||||
"restriction d'audience ne sera ajoutée."
|
"restriction d'audience ne sera ajoutée."
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
#: authentik/providers/saml/models.py
|
||||||
msgid "Also known as EntityID"
|
msgid ""
|
||||||
msgstr "Aussi appelé EntityID"
|
"Also known as EntityID. Providing a value overrides the default issuer "
|
||||||
|
"generated by authentik."
|
||||||
|
msgstr ""
|
||||||
|
"Aussi appelé EntityID. Fournir une valeur remplace l'émetteur par défaut "
|
||||||
|
"généré par authentik."
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
#: authentik/providers/saml/models.py
|
||||||
msgid "SLS URL"
|
msgid "SLS URL"
|
||||||
@@ -2994,6 +3102,10 @@ msgstr "SAML NameID pour cette session"
|
|||||||
msgid "SAML NameID format"
|
msgid "SAML NameID format"
|
||||||
msgstr "Format SAML NameID"
|
msgstr "Format SAML NameID"
|
||||||
|
|
||||||
|
#: authentik/providers/saml/models.py
|
||||||
|
msgid "SAML Issuer used for this session"
|
||||||
|
msgstr "Émetteur SAML utilisé pour cette session"
|
||||||
|
|
||||||
#: authentik/providers/saml/models.py
|
#: authentik/providers/saml/models.py
|
||||||
msgid "SAML Session"
|
msgid "SAML Session"
|
||||||
msgstr "Session SAML"
|
msgstr "Session SAML"
|
||||||
@@ -3026,6 +3138,10 @@ msgstr "Salesforce"
|
|||||||
msgid "Webex"
|
msgid "Webex"
|
||||||
msgstr "Webex"
|
msgstr "Webex"
|
||||||
|
|
||||||
|
#: authentik/providers/scim/models.py
|
||||||
|
msgid "vCenter"
|
||||||
|
msgstr "vCenter"
|
||||||
|
|
||||||
#: authentik/providers/scim/models.py
|
#: authentik/providers/scim/models.py
|
||||||
msgid "Group filters used to define sync-scope for groups."
|
msgid "Group filters used to define sync-scope for groups."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -3749,8 +3865,8 @@ msgid ""
|
|||||||
"Which servers a user has to be a member of to be granted access. Empty list "
|
"Which servers a user has to be a member of to be granted access. Empty list "
|
||||||
"allows every server."
|
"allows every server."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"De quels serveurs un utilisateur doit être membre afin d'être autorisé. Une "
|
"De quels serveurs un utilisateur doit être membre afin d'obtenir l'accès. "
|
||||||
"liste vide autorise tous les serveurs."
|
"Une liste vide autorise tous les serveurs."
|
||||||
|
|
||||||
#: authentik/sources/plex/models.py
|
#: authentik/sources/plex/models.py
|
||||||
msgid "Allow friends to authenticate, even if you don't share a server."
|
msgid "Allow friends to authenticate, even if you don't share a server."
|
||||||
@@ -4455,11 +4571,11 @@ msgstr "Activer les utilisateurs à la complétion de l'étape."
|
|||||||
|
|
||||||
#: authentik/stages/email/models.py
|
#: authentik/stages/email/models.py
|
||||||
msgid "Email Stage"
|
msgid "Email Stage"
|
||||||
msgstr "Étape Courriel"
|
msgstr "Étape de Courriel"
|
||||||
|
|
||||||
#: authentik/stages/email/models.py
|
#: authentik/stages/email/models.py
|
||||||
msgid "Email Stages"
|
msgid "Email Stages"
|
||||||
msgstr "Étapes Courriel"
|
msgstr "Étapes de Courriel"
|
||||||
|
|
||||||
#: authentik/stages/email/stage.py
|
#: authentik/stages/email/stage.py
|
||||||
msgid "Successfully verified Email."
|
msgid "Successfully verified Email."
|
||||||
@@ -4933,6 +5049,19 @@ msgstr ""
|
|||||||
msgid "Static: Static value, displayed as-is."
|
msgid "Static: Static value, displayed as-is."
|
||||||
msgstr "Statique : valeur statique, affichée comme telle."
|
msgstr "Statique : valeur statique, affichée comme telle."
|
||||||
|
|
||||||
|
#: authentik/stages/prompt/models.py
|
||||||
|
msgid "Alert (Info): Static alert box with info styling"
|
||||||
|
msgstr "Alerte (Info) : message d'alerte statique au format information"
|
||||||
|
|
||||||
|
#: authentik/stages/prompt/models.py
|
||||||
|
msgid "Alert (Warning): Static alert box with warning styling"
|
||||||
|
msgstr ""
|
||||||
|
"Alerte (Avertissement) : message d'alerte statique au format avertissement"
|
||||||
|
|
||||||
|
#: authentik/stages/prompt/models.py
|
||||||
|
msgid "Alert (Danger): Static alert box with danger styling"
|
||||||
|
msgstr "Alerte (Danger) : message d'alerte statique au format danger"
|
||||||
|
|
||||||
#: authentik/stages/prompt/models.py
|
#: authentik/stages/prompt/models.py
|
||||||
msgid "authentik: Selection of locales authentik supports"
|
msgid "authentik: Selection of locales authentik supports"
|
||||||
msgstr "authentik : sélection des locales prises en charges par authentik"
|
msgstr "authentik : sélection des locales prises en charges par authentik"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Utilities to run an axum server.
|
//! Utilities to run an axum server.
|
||||||
|
|
||||||
use std::{net, os::unix};
|
use std::{net, os::unix, path::PathBuf};
|
||||||
|
|
||||||
use ak_common::arbiter::{Arbiter, Tasks};
|
use ak_common::arbiter::{Arbiter, Tasks};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
@@ -21,26 +21,20 @@ async fn run_plain(
|
|||||||
name: &str,
|
name: &str,
|
||||||
router: Router,
|
router: Router,
|
||||||
addr: net::SocketAddr,
|
addr: net::SocketAddr,
|
||||||
allow_failure: bool,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!(addr = addr.to_string(), "starting {name} server");
|
info!(addr = addr.to_string(), "starting {name} server");
|
||||||
|
|
||||||
let handle = Handle::new();
|
let handle = Handle::new();
|
||||||
arbiter.add_net_handle(handle.clone()).await;
|
arbiter.add_net_handle(handle.clone()).await;
|
||||||
|
|
||||||
let res = axum_server::Server::bind(addr)
|
axum_server::Server::bind(addr)
|
||||||
.acceptor(CatchPanicAcceptor::new(
|
.acceptor(CatchPanicAcceptor::new(
|
||||||
ProxyProtocolAcceptor::new().acceptor(DefaultAcceptor::new()),
|
ProxyProtocolAcceptor::new().acceptor(DefaultAcceptor::new()),
|
||||||
arbiter.clone(),
|
arbiter.clone(),
|
||||||
))
|
))
|
||||||
.handle(handle)
|
.handle(handle)
|
||||||
.serve(router.into_make_service_with_connect_info::<net::SocketAddr>())
|
.serve(router.into_make_service_with_connect_info::<net::SocketAddr>())
|
||||||
.await;
|
.await?;
|
||||||
if res.is_err() && allow_failure {
|
|
||||||
arbiter.shutdown().await;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
res?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -49,60 +43,59 @@ async fn run_plain(
|
|||||||
///
|
///
|
||||||
/// `name` is only used for observability purposes and should describe which module is starting the
|
/// `name` is only used for observability purposes and should describe which module is starting the
|
||||||
/// server.
|
/// server.
|
||||||
///
|
|
||||||
/// `allow_failure` allows the server to fail silently.
|
|
||||||
pub fn start_plain(
|
pub fn start_plain(
|
||||||
tasks: &mut Tasks,
|
tasks: &mut Tasks,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
router: Router,
|
router: Router,
|
||||||
addr: net::SocketAddr,
|
addr: net::SocketAddr,
|
||||||
allow_failure: bool,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let arbiter = tasks.arbiter();
|
let arbiter = tasks.arbiter();
|
||||||
tasks
|
tasks
|
||||||
.build_task()
|
.build_task()
|
||||||
.name(&format!("{}::run_plain({name}, {addr})", module_path!()))
|
.name(&format!("{}::run_plain({name}, {addr})", module_path!()))
|
||||||
.spawn(run_plain(arbiter, name, router, addr, allow_failure))?;
|
.spawn(run_plain(arbiter, name, router, addr))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct UnixSocketGuard(PathBuf);
|
||||||
|
|
||||||
|
impl Drop for UnixSocketGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
trace!(path = ?self.0, "removing socket");
|
||||||
|
if let Err(err) = std::fs::remove_file(&self.0) {
|
||||||
|
trace!(?err, "failed to remove socket, ignoring");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn run_unix(
|
pub(crate) async fn run_unix(
|
||||||
arbiter: Arbiter,
|
arbiter: Arbiter,
|
||||||
name: &str,
|
name: &str,
|
||||||
router: Router,
|
router: Router,
|
||||||
addr: unix::net::SocketAddr,
|
addr: unix::net::SocketAddr,
|
||||||
allow_failure: bool,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!(?addr, "starting {name} server");
|
info!(?addr, "starting {name} server");
|
||||||
|
|
||||||
let handle = Handle::new();
|
let handle = Handle::new();
|
||||||
arbiter.add_unix_handle(handle.clone()).await;
|
arbiter.add_unix_handle(handle.clone()).await;
|
||||||
|
|
||||||
if !allow_failure && let Some(path) = addr.as_pathname() {
|
let _guard = if let Some(path) = addr.as_pathname() {
|
||||||
trace!(?addr, "removing socket");
|
trace!(?addr, "removing socket");
|
||||||
if let Err(err) = std::fs::remove_file(path) {
|
if let Err(err) = std::fs::remove_file(path) {
|
||||||
trace!(?err, "failed to remove socket, ignoring");
|
trace!(?err, "failed to remove socket, ignoring");
|
||||||
}
|
}
|
||||||
}
|
Some(UnixSocketGuard(path.to_owned()))
|
||||||
let res = axum_server::Server::bind(addr.clone())
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
axum_server::Server::bind(addr.clone())
|
||||||
.acceptor(CatchPanicAcceptor::new(
|
.acceptor(CatchPanicAcceptor::new(
|
||||||
DefaultAcceptor::new(),
|
DefaultAcceptor::new(),
|
||||||
arbiter.clone(),
|
arbiter.clone(),
|
||||||
))
|
))
|
||||||
.handle(handle)
|
.handle(handle)
|
||||||
.serve(router.into_make_service())
|
.serve(router.into_make_service())
|
||||||
.await;
|
.await?;
|
||||||
if !allow_failure && let Some(path) = addr.as_pathname() {
|
|
||||||
trace!(?addr, "removing socket");
|
|
||||||
if let Err(err) = std::fs::remove_file(path) {
|
|
||||||
trace!(?err, "failed to remove socket, ignoring");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if res.is_err() && allow_failure {
|
|
||||||
arbiter.shutdown().await;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
res?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -111,20 +104,17 @@ pub(crate) async fn run_unix(
|
|||||||
///
|
///
|
||||||
/// `name` is only used for observability purposes and should describe which module is starting the
|
/// `name` is only used for observability purposes and should describe which module is starting the
|
||||||
/// server.
|
/// server.
|
||||||
///
|
|
||||||
/// `allow_failure` allows the server to fail silently.
|
|
||||||
pub fn start_unix(
|
pub fn start_unix(
|
||||||
tasks: &mut Tasks,
|
tasks: &mut Tasks,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
router: Router,
|
router: Router,
|
||||||
addr: unix::net::SocketAddr,
|
addr: unix::net::SocketAddr,
|
||||||
allow_failure: bool,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let arbiter = tasks.arbiter();
|
let arbiter = tasks.arbiter();
|
||||||
tasks
|
tasks
|
||||||
.build_task()
|
.build_task()
|
||||||
.name(&format!("{}::run_unix({name}, {addr:?})", module_path!()))
|
.name(&format!("{}::run_unix({name}, {addr:?})", module_path!()))
|
||||||
.spawn(run_unix(arbiter, name, router, addr, allow_failure))?;
|
.spawn(run_unix(arbiter, name, router, addr))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ mod json {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut json_layer = json_subscriber::fmt::layer()
|
let mut json_layer = json_subscriber::fmt::layer()
|
||||||
|
.with_level(false)
|
||||||
.with_timer(LocalTime::new(time_format))
|
.with_timer(LocalTime::new(time_format))
|
||||||
.with_file(true)
|
.with_file(true)
|
||||||
.with_line_number(true)
|
.with_line_number(true)
|
||||||
@@ -109,6 +110,11 @@ mod json {
|
|||||||
let inner_layer = json_layer.inner_layer_mut();
|
let inner_layer = json_layer.inner_layer_mut();
|
||||||
inner_layer.with_thread_ids("thread_id");
|
inner_layer.with_thread_ids("thread_id");
|
||||||
inner_layer.with_thread_names("thread_name");
|
inner_layer.with_thread_names("thread_name");
|
||||||
|
inner_layer.add_dynamic_field("level", |event, _| {
|
||||||
|
Some(serde_json::Value::String(
|
||||||
|
event.metadata().level().as_str().to_lowercase(),
|
||||||
|
))
|
||||||
|
});
|
||||||
inner_layer.add_dynamic_field("pid", |_, _| {
|
inner_layer.add_dynamic_field("pid", |_, _| {
|
||||||
Some(serde_json::Value::Number(serde_json::Number::from(
|
Some(serde_json::Value::Number(serde_json::Number::from(
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
|
|||||||
9
packages/client-go/api_core.go
generated
9
packages/client-go/api_core.go
generated
@@ -39,6 +39,7 @@ type ApiCoreBrandsListRequest struct {
|
|||||||
flowAuthentication *string
|
flowAuthentication *string
|
||||||
flowDeviceCode *string
|
flowDeviceCode *string
|
||||||
flowInvalidation *string
|
flowInvalidation *string
|
||||||
|
flowLockdown *string
|
||||||
flowRecovery *string
|
flowRecovery *string
|
||||||
flowUnenrollment *string
|
flowUnenrollment *string
|
||||||
flowUserSettings *string
|
flowUserSettings *string
|
||||||
@@ -104,6 +105,11 @@ func (r ApiCoreBrandsListRequest) FlowInvalidation(flowInvalidation string) ApiC
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r ApiCoreBrandsListRequest) FlowLockdown(flowLockdown string) ApiCoreBrandsListRequest {
|
||||||
|
r.flowLockdown = &flowLockdown
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func (r ApiCoreBrandsListRequest) FlowRecovery(flowRecovery string) ApiCoreBrandsListRequest {
|
func (r ApiCoreBrandsListRequest) FlowRecovery(flowRecovery string) ApiCoreBrandsListRequest {
|
||||||
r.flowRecovery = &flowRecovery
|
r.flowRecovery = &flowRecovery
|
||||||
return r
|
return r
|
||||||
@@ -230,6 +236,9 @@ func (a *CoreAPIService) CoreBrandsListExecute(r ApiCoreBrandsListRequest) (*Pag
|
|||||||
if r.flowInvalidation != nil {
|
if r.flowInvalidation != nil {
|
||||||
parameterAddToHeaderOrQuery(localVarQueryParams, "flow_invalidation", r.flowInvalidation, "form", "")
|
parameterAddToHeaderOrQuery(localVarQueryParams, "flow_invalidation", r.flowInvalidation, "form", "")
|
||||||
}
|
}
|
||||||
|
if r.flowLockdown != nil {
|
||||||
|
parameterAddToHeaderOrQuery(localVarQueryParams, "flow_lockdown", r.flowLockdown, "form", "")
|
||||||
|
}
|
||||||
if r.flowRecovery != nil {
|
if r.flowRecovery != nil {
|
||||||
parameterAddToHeaderOrQuery(localVarQueryParams, "flow_recovery", r.flowRecovery, "form", "")
|
parameterAddToHeaderOrQuery(localVarQueryParams, "flow_recovery", r.flowRecovery, "form", "")
|
||||||
}
|
}
|
||||||
|
|||||||
48
packages/client-go/model_brand.go
generated
48
packages/client-go/model_brand.go
generated
@@ -36,6 +36,7 @@ type Brand struct {
|
|||||||
FlowUnenrollment NullableString `json:"flow_unenrollment,omitempty"`
|
FlowUnenrollment NullableString `json:"flow_unenrollment,omitempty"`
|
||||||
FlowUserSettings NullableString `json:"flow_user_settings,omitempty"`
|
FlowUserSettings NullableString `json:"flow_user_settings,omitempty"`
|
||||||
FlowDeviceCode NullableString `json:"flow_device_code,omitempty"`
|
FlowDeviceCode NullableString `json:"flow_device_code,omitempty"`
|
||||||
|
FlowLockdown NullableString `json:"flow_lockdown,omitempty"`
|
||||||
// When set, external users will be redirected to this application after authenticating.
|
// When set, external users will be redirected to this application after authenticating.
|
||||||
DefaultApplication NullableString `json:"default_application,omitempty"`
|
DefaultApplication NullableString `json:"default_application,omitempty"`
|
||||||
// Web Certificate used by the authentik Core webserver.
|
// Web Certificate used by the authentik Core webserver.
|
||||||
@@ -565,6 +566,49 @@ func (o *Brand) UnsetFlowDeviceCode() {
|
|||||||
o.FlowDeviceCode.Unset()
|
o.FlowDeviceCode.Unset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFlowLockdown returns the FlowLockdown field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||||
|
func (o *Brand) GetFlowLockdown() string {
|
||||||
|
if o == nil || IsNil(o.FlowLockdown.Get()) {
|
||||||
|
var ret string
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
return *o.FlowLockdown.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFlowLockdownOk returns a tuple with the FlowLockdown field value if set, nil otherwise
|
||||||
|
// and a boolean to check if the value has been set.
|
||||||
|
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||||
|
func (o *Brand) GetFlowLockdownOk() (*string, bool) {
|
||||||
|
if o == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return o.FlowLockdown.Get(), o.FlowLockdown.IsSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasFlowLockdown returns a boolean if a field has been set.
|
||||||
|
func (o *Brand) HasFlowLockdown() bool {
|
||||||
|
if o != nil && o.FlowLockdown.IsSet() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFlowLockdown gets a reference to the given NullableString and assigns it to the FlowLockdown field.
|
||||||
|
func (o *Brand) SetFlowLockdown(v string) {
|
||||||
|
o.FlowLockdown.Set(&v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFlowLockdownNil sets the value for FlowLockdown to be an explicit nil
|
||||||
|
func (o *Brand) SetFlowLockdownNil() {
|
||||||
|
o.FlowLockdown.Set(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetFlowLockdown ensures that no value is present for FlowLockdown, not even an explicit nil
|
||||||
|
func (o *Brand) UnsetFlowLockdown() {
|
||||||
|
o.FlowLockdown.Unset()
|
||||||
|
}
|
||||||
|
|
||||||
// GetDefaultApplication returns the DefaultApplication field value if set, zero value otherwise (both if not set or set to explicit null).
|
// GetDefaultApplication returns the DefaultApplication field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||||
func (o *Brand) GetDefaultApplication() string {
|
func (o *Brand) GetDefaultApplication() string {
|
||||||
if o == nil || IsNil(o.DefaultApplication.Get()) {
|
if o == nil || IsNil(o.DefaultApplication.Get()) {
|
||||||
@@ -763,6 +807,9 @@ func (o Brand) ToMap() (map[string]interface{}, error) {
|
|||||||
if o.FlowDeviceCode.IsSet() {
|
if o.FlowDeviceCode.IsSet() {
|
||||||
toSerialize["flow_device_code"] = o.FlowDeviceCode.Get()
|
toSerialize["flow_device_code"] = o.FlowDeviceCode.Get()
|
||||||
}
|
}
|
||||||
|
if o.FlowLockdown.IsSet() {
|
||||||
|
toSerialize["flow_lockdown"] = o.FlowLockdown.Get()
|
||||||
|
}
|
||||||
if o.DefaultApplication.IsSet() {
|
if o.DefaultApplication.IsSet() {
|
||||||
toSerialize["default_application"] = o.DefaultApplication.Get()
|
toSerialize["default_application"] = o.DefaultApplication.Get()
|
||||||
}
|
}
|
||||||
@@ -833,6 +880,7 @@ func (o *Brand) UnmarshalJSON(data []byte) (err error) {
|
|||||||
delete(additionalProperties, "flow_unenrollment")
|
delete(additionalProperties, "flow_unenrollment")
|
||||||
delete(additionalProperties, "flow_user_settings")
|
delete(additionalProperties, "flow_user_settings")
|
||||||
delete(additionalProperties, "flow_device_code")
|
delete(additionalProperties, "flow_device_code")
|
||||||
|
delete(additionalProperties, "flow_lockdown")
|
||||||
delete(additionalProperties, "default_application")
|
delete(additionalProperties, "default_application")
|
||||||
delete(additionalProperties, "web_certificate")
|
delete(additionalProperties, "web_certificate")
|
||||||
delete(additionalProperties, "client_certificates")
|
delete(additionalProperties, "client_certificates")
|
||||||
|
|||||||
6
packages/client-go/model_prompt_type_enum.go
generated
6
packages/client-go/model_prompt_type_enum.go
generated
@@ -38,6 +38,9 @@ const (
|
|||||||
PROMPTTYPEENUM_SEPARATOR PromptTypeEnum = "separator"
|
PROMPTTYPEENUM_SEPARATOR PromptTypeEnum = "separator"
|
||||||
PROMPTTYPEENUM_HIDDEN PromptTypeEnum = "hidden"
|
PROMPTTYPEENUM_HIDDEN PromptTypeEnum = "hidden"
|
||||||
PROMPTTYPEENUM_STATIC PromptTypeEnum = "static"
|
PROMPTTYPEENUM_STATIC PromptTypeEnum = "static"
|
||||||
|
PROMPTTYPEENUM_ALERT_INFO PromptTypeEnum = "alert_info"
|
||||||
|
PROMPTTYPEENUM_ALERT_WARNING PromptTypeEnum = "alert_warning"
|
||||||
|
PROMPTTYPEENUM_ALERT_DANGER PromptTypeEnum = "alert_danger"
|
||||||
PROMPTTYPEENUM_AK_LOCALE PromptTypeEnum = "ak-locale"
|
PROMPTTYPEENUM_AK_LOCALE PromptTypeEnum = "ak-locale"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,6 +63,9 @@ var AllowedPromptTypeEnumEnumValues = []PromptTypeEnum{
|
|||||||
"separator",
|
"separator",
|
||||||
"hidden",
|
"hidden",
|
||||||
"static",
|
"static",
|
||||||
|
"alert_info",
|
||||||
|
"alert_warning",
|
||||||
|
"alert_danger",
|
||||||
"ak-locale",
|
"ak-locale",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
packages/client-rust/src/apis/core_api.rs
generated
5
packages/client-rust/src/apis/core_api.rs
generated
@@ -71,6 +71,7 @@ pub async fn core_brands_list(
|
|||||||
flow_authentication: Option<&str>,
|
flow_authentication: Option<&str>,
|
||||||
flow_device_code: Option<&str>,
|
flow_device_code: Option<&str>,
|
||||||
flow_invalidation: Option<&str>,
|
flow_invalidation: Option<&str>,
|
||||||
|
flow_lockdown: Option<&str>,
|
||||||
flow_recovery: Option<&str>,
|
flow_recovery: Option<&str>,
|
||||||
flow_unenrollment: Option<&str>,
|
flow_unenrollment: Option<&str>,
|
||||||
flow_user_settings: Option<&str>,
|
flow_user_settings: Option<&str>,
|
||||||
@@ -92,6 +93,7 @@ pub async fn core_brands_list(
|
|||||||
let p_query_flow_authentication = flow_authentication;
|
let p_query_flow_authentication = flow_authentication;
|
||||||
let p_query_flow_device_code = flow_device_code;
|
let p_query_flow_device_code = flow_device_code;
|
||||||
let p_query_flow_invalidation = flow_invalidation;
|
let p_query_flow_invalidation = flow_invalidation;
|
||||||
|
let p_query_flow_lockdown = flow_lockdown;
|
||||||
let p_query_flow_recovery = flow_recovery;
|
let p_query_flow_recovery = flow_recovery;
|
||||||
let p_query_flow_unenrollment = flow_unenrollment;
|
let p_query_flow_unenrollment = flow_unenrollment;
|
||||||
let p_query_flow_user_settings = flow_user_settings;
|
let p_query_flow_user_settings = flow_user_settings;
|
||||||
@@ -154,6 +156,9 @@ pub async fn core_brands_list(
|
|||||||
if let Some(ref param_value) = p_query_flow_invalidation {
|
if let Some(ref param_value) = p_query_flow_invalidation {
|
||||||
req_builder = req_builder.query(&[("flow_invalidation", ¶m_value.to_string())]);
|
req_builder = req_builder.query(&[("flow_invalidation", ¶m_value.to_string())]);
|
||||||
}
|
}
|
||||||
|
if let Some(ref param_value) = p_query_flow_lockdown {
|
||||||
|
req_builder = req_builder.query(&[("flow_lockdown", ¶m_value.to_string())]);
|
||||||
|
}
|
||||||
if let Some(ref param_value) = p_query_flow_recovery {
|
if let Some(ref param_value) = p_query_flow_recovery {
|
||||||
req_builder = req_builder.query(&[("flow_recovery", ¶m_value.to_string())]);
|
req_builder = req_builder.query(&[("flow_recovery", ¶m_value.to_string())]);
|
||||||
}
|
}
|
||||||
|
|||||||
8
packages/client-rust/src/models/brand.rs
generated
8
packages/client-rust/src/models/brand.rs
generated
@@ -78,6 +78,13 @@ pub struct Brand {
|
|||||||
skip_serializing_if = "Option::is_none"
|
skip_serializing_if = "Option::is_none"
|
||||||
)]
|
)]
|
||||||
pub flow_device_code: Option<Option<uuid::Uuid>>,
|
pub flow_device_code: Option<Option<uuid::Uuid>>,
|
||||||
|
#[serde(
|
||||||
|
rename = "flow_lockdown",
|
||||||
|
default,
|
||||||
|
with = "::serde_with::rust::double_option",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub flow_lockdown: Option<Option<uuid::Uuid>>,
|
||||||
/// When set, external users will be redirected to this application after authenticating.
|
/// When set, external users will be redirected to this application after authenticating.
|
||||||
#[serde(
|
#[serde(
|
||||||
rename = "default_application",
|
rename = "default_application",
|
||||||
@@ -122,6 +129,7 @@ impl Brand {
|
|||||||
flow_unenrollment: None,
|
flow_unenrollment: None,
|
||||||
flow_user_settings: None,
|
flow_user_settings: None,
|
||||||
flow_device_code: None,
|
flow_device_code: None,
|
||||||
|
flow_lockdown: None,
|
||||||
default_application: None,
|
default_application: None,
|
||||||
web_certificate: None,
|
web_certificate: None,
|
||||||
client_certificates: None,
|
client_certificates: None,
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ pub enum PromptTypeEnum {
|
|||||||
Hidden,
|
Hidden,
|
||||||
#[serde(rename = "static")]
|
#[serde(rename = "static")]
|
||||||
Static,
|
Static,
|
||||||
|
#[serde(rename = "alert_info")]
|
||||||
|
AlertInfo,
|
||||||
|
#[serde(rename = "alert_warning")]
|
||||||
|
AlertWarning,
|
||||||
|
#[serde(rename = "alert_danger")]
|
||||||
|
AlertDanger,
|
||||||
#[serde(rename = "ak-locale")]
|
#[serde(rename = "ak-locale")]
|
||||||
AkLocale,
|
AkLocale,
|
||||||
}
|
}
|
||||||
@@ -71,6 +77,9 @@ impl std::fmt::Display for PromptTypeEnum {
|
|||||||
Self::Separator => write!(f, "separator"),
|
Self::Separator => write!(f, "separator"),
|
||||||
Self::Hidden => write!(f, "hidden"),
|
Self::Hidden => write!(f, "hidden"),
|
||||||
Self::Static => write!(f, "static"),
|
Self::Static => write!(f, "static"),
|
||||||
|
Self::AlertInfo => write!(f, "alert_info"),
|
||||||
|
Self::AlertWarning => write!(f, "alert_warning"),
|
||||||
|
Self::AlertDanger => write!(f, "alert_danger"),
|
||||||
Self::AkLocale => write!(f, "ak-locale"),
|
Self::AkLocale => write!(f, "ak-locale"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
71
packages/client-ts/src/apis/CoreApi.ts
generated
71
packages/client-ts/src/apis/CoreApi.ts
generated
@@ -52,6 +52,7 @@ import type {
|
|||||||
TransactionApplicationResponse,
|
TransactionApplicationResponse,
|
||||||
UsedBy,
|
UsedBy,
|
||||||
User,
|
User,
|
||||||
|
UserAccountLockdownRequest,
|
||||||
UserAccountRequest,
|
UserAccountRequest,
|
||||||
UserConsent,
|
UserConsent,
|
||||||
UserPasswordHashSetRequest,
|
UserPasswordHashSetRequest,
|
||||||
@@ -102,6 +103,7 @@ import {
|
|||||||
TransactionApplicationRequestToJSON,
|
TransactionApplicationRequestToJSON,
|
||||||
TransactionApplicationResponseFromJSON,
|
TransactionApplicationResponseFromJSON,
|
||||||
UsedByFromJSON,
|
UsedByFromJSON,
|
||||||
|
UserAccountLockdownRequestToJSON,
|
||||||
UserAccountRequestToJSON,
|
UserAccountRequestToJSON,
|
||||||
UserConsentFromJSON,
|
UserConsentFromJSON,
|
||||||
UserFromJSON,
|
UserFromJSON,
|
||||||
@@ -245,6 +247,7 @@ export interface CoreBrandsListRequest {
|
|||||||
flowAuthentication?: string;
|
flowAuthentication?: string;
|
||||||
flowDeviceCode?: string;
|
flowDeviceCode?: string;
|
||||||
flowInvalidation?: string;
|
flowInvalidation?: string;
|
||||||
|
flowLockdown?: string;
|
||||||
flowRecovery?: string;
|
flowRecovery?: string;
|
||||||
flowUnenrollment?: string;
|
flowUnenrollment?: string;
|
||||||
flowUserSettings?: string;
|
flowUserSettings?: string;
|
||||||
@@ -403,6 +406,10 @@ export interface CoreUserConsentUsedByListRequest {
|
|||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CoreUsersAccountLockdownCreateRequest {
|
||||||
|
userAccountLockdownRequest?: UserAccountLockdownRequest;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CoreUsersCreateRequest {
|
export interface CoreUsersCreateRequest {
|
||||||
userRequest: UserRequest;
|
userRequest: UserRequest;
|
||||||
}
|
}
|
||||||
@@ -2214,6 +2221,10 @@ export class CoreApi extends runtime.BaseAPI {
|
|||||||
queryParameters["flow_invalidation"] = requestParameters["flowInvalidation"];
|
queryParameters["flow_invalidation"] = requestParameters["flowInvalidation"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requestParameters["flowLockdown"] != null) {
|
||||||
|
queryParameters["flow_lockdown"] = requestParameters["flowLockdown"];
|
||||||
|
}
|
||||||
|
|
||||||
if (requestParameters["flowRecovery"] != null) {
|
if (requestParameters["flowRecovery"] != null) {
|
||||||
queryParameters["flow_recovery"] = requestParameters["flowRecovery"];
|
queryParameters["flow_recovery"] = requestParameters["flowRecovery"];
|
||||||
}
|
}
|
||||||
@@ -4189,6 +4200,66 @@ export class CoreApi extends runtime.BaseAPI {
|
|||||||
return await response.value();
|
return await response.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates request options for coreUsersAccountLockdownCreate without sending the request
|
||||||
|
*/
|
||||||
|
async coreUsersAccountLockdownCreateRequestOpts(
|
||||||
|
requestParameters: CoreUsersAccountLockdownCreateRequest,
|
||||||
|
): Promise<runtime.RequestOpts> {
|
||||||
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
const headerParameters: runtime.HTTPHeaders = {};
|
||||||
|
|
||||||
|
headerParameters["Content-Type"] = "application/json";
|
||||||
|
|
||||||
|
if (this.configuration && this.configuration.accessToken) {
|
||||||
|
const token = this.configuration.accessToken;
|
||||||
|
const tokenString = await token("authentik", []);
|
||||||
|
|
||||||
|
if (tokenString) {
|
||||||
|
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlPath = `/core/users/account_lockdown/`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: urlPath,
|
||||||
|
method: "POST",
|
||||||
|
headers: headerParameters,
|
||||||
|
query: queryParameters,
|
||||||
|
body: UserAccountLockdownRequestToJSON(requestParameters["userAccountLockdownRequest"]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose the target account, then return a flow link.
|
||||||
|
*/
|
||||||
|
async coreUsersAccountLockdownCreateRaw(
|
||||||
|
requestParameters: CoreUsersAccountLockdownCreateRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<runtime.ApiResponse<Link>> {
|
||||||
|
const requestOptions =
|
||||||
|
await this.coreUsersAccountLockdownCreateRequestOpts(requestParameters);
|
||||||
|
const response = await this.request(requestOptions, initOverrides);
|
||||||
|
|
||||||
|
return new runtime.JSONApiResponse(response, (jsonValue) => LinkFromJSON(jsonValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose the target account, then return a flow link.
|
||||||
|
*/
|
||||||
|
async coreUsersAccountLockdownCreate(
|
||||||
|
requestParameters: CoreUsersAccountLockdownCreateRequest = {},
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<Link> {
|
||||||
|
const response = await this.coreUsersAccountLockdownCreateRaw(
|
||||||
|
requestParameters,
|
||||||
|
initOverrides,
|
||||||
|
);
|
||||||
|
return await response.value();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates request options for coreUsersCreate without sending the request
|
* Creates request options for coreUsersCreate without sending the request
|
||||||
*/
|
*/
|
||||||
|
|||||||
45
packages/client-ts/src/apis/LifecycleApi.ts
generated
45
packages/client-ts/src/apis/LifecycleApi.ts
generated
@@ -40,9 +40,12 @@ export interface LifecycleIterationsCreateRequest {
|
|||||||
lifecycleIterationRequest: LifecycleIterationRequest;
|
lifecycleIterationRequest: LifecycleIterationRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LifecycleIterationsLatestRetrieveRequest {
|
export interface LifecycleIterationsListLatestRequest {
|
||||||
contentType: string;
|
contentType: string;
|
||||||
objectId: string;
|
objectId: string;
|
||||||
|
ordering?: string;
|
||||||
|
search?: string;
|
||||||
|
userIsReviewer?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LifecycleIterationsListOpenRequest {
|
export interface LifecycleIterationsListOpenRequest {
|
||||||
@@ -157,27 +160,39 @@ export class LifecycleApi extends runtime.BaseAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates request options for lifecycleIterationsLatestRetrieve without sending the request
|
* Creates request options for lifecycleIterationsListLatest without sending the request
|
||||||
*/
|
*/
|
||||||
async lifecycleIterationsLatestRetrieveRequestOpts(
|
async lifecycleIterationsListLatestRequestOpts(
|
||||||
requestParameters: LifecycleIterationsLatestRetrieveRequest,
|
requestParameters: LifecycleIterationsListLatestRequest,
|
||||||
): Promise<runtime.RequestOpts> {
|
): Promise<runtime.RequestOpts> {
|
||||||
if (requestParameters["contentType"] == null) {
|
if (requestParameters["contentType"] == null) {
|
||||||
throw new runtime.RequiredError(
|
throw new runtime.RequiredError(
|
||||||
"contentType",
|
"contentType",
|
||||||
'Required parameter "contentType" was null or undefined when calling lifecycleIterationsLatestRetrieve().',
|
'Required parameter "contentType" was null or undefined when calling lifecycleIterationsListLatest().',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestParameters["objectId"] == null) {
|
if (requestParameters["objectId"] == null) {
|
||||||
throw new runtime.RequiredError(
|
throw new runtime.RequiredError(
|
||||||
"objectId",
|
"objectId",
|
||||||
'Required parameter "objectId" was null or undefined when calling lifecycleIterationsLatestRetrieve().',
|
'Required parameter "objectId" was null or undefined when calling lifecycleIterationsListLatest().',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryParameters: any = {};
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
if (requestParameters["ordering"] != null) {
|
||||||
|
queryParameters["ordering"] = requestParameters["ordering"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["search"] != null) {
|
||||||
|
queryParameters["search"] = requestParameters["search"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["userIsReviewer"] != null) {
|
||||||
|
queryParameters["user_is_reviewer"] = requestParameters["userIsReviewer"];
|
||||||
|
}
|
||||||
|
|
||||||
const headerParameters: runtime.HTTPHeaders = {};
|
const headerParameters: runtime.HTTPHeaders = {};
|
||||||
|
|
||||||
if (this.configuration && this.configuration.accessToken) {
|
if (this.configuration && this.configuration.accessToken) {
|
||||||
@@ -210,27 +225,27 @@ export class LifecycleApi extends runtime.BaseAPI {
|
|||||||
/**
|
/**
|
||||||
* Mixin to validate that a valid enterprise license exists before allowing to save the object
|
* Mixin to validate that a valid enterprise license exists before allowing to save the object
|
||||||
*/
|
*/
|
||||||
async lifecycleIterationsLatestRetrieveRaw(
|
async lifecycleIterationsListLatestRaw(
|
||||||
requestParameters: LifecycleIterationsLatestRetrieveRequest,
|
requestParameters: LifecycleIterationsListLatestRequest,
|
||||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
): Promise<runtime.ApiResponse<LifecycleIteration>> {
|
): Promise<runtime.ApiResponse<Array<LifecycleIteration>>> {
|
||||||
const requestOptions =
|
const requestOptions =
|
||||||
await this.lifecycleIterationsLatestRetrieveRequestOpts(requestParameters);
|
await this.lifecycleIterationsListLatestRequestOpts(requestParameters);
|
||||||
const response = await this.request(requestOptions, initOverrides);
|
const response = await this.request(requestOptions, initOverrides);
|
||||||
|
|
||||||
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||||
LifecycleIterationFromJSON(jsonValue),
|
jsonValue.map(LifecycleIterationFromJSON),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mixin to validate that a valid enterprise license exists before allowing to save the object
|
* Mixin to validate that a valid enterprise license exists before allowing to save the object
|
||||||
*/
|
*/
|
||||||
async lifecycleIterationsLatestRetrieve(
|
async lifecycleIterationsListLatest(
|
||||||
requestParameters: LifecycleIterationsLatestRetrieveRequest,
|
requestParameters: LifecycleIterationsListLatestRequest,
|
||||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
): Promise<LifecycleIteration> {
|
): Promise<Array<LifecycleIteration>> {
|
||||||
const response = await this.lifecycleIterationsLatestRetrieveRaw(
|
const response = await this.lifecycleIterationsListLatestRaw(
|
||||||
requestParameters,
|
requestParameters,
|
||||||
initOverrides,
|
initOverrides,
|
||||||
);
|
);
|
||||||
|
|||||||
576
packages/client-ts/src/apis/StagesApi.ts
generated
576
packages/client-ts/src/apis/StagesApi.ts
generated
@@ -13,6 +13,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
AccountLockdownStage,
|
||||||
|
AccountLockdownStageRequest,
|
||||||
AuthenticatorAttachmentEnum,
|
AuthenticatorAttachmentEnum,
|
||||||
AuthenticatorDuoStage,
|
AuthenticatorDuoStage,
|
||||||
AuthenticatorDuoStageDeviceImportResponse,
|
AuthenticatorDuoStageDeviceImportResponse,
|
||||||
@@ -61,6 +63,7 @@ import type {
|
|||||||
MutualTLSStageRequest,
|
MutualTLSStageRequest,
|
||||||
NetworkBindingEnum,
|
NetworkBindingEnum,
|
||||||
NotConfiguredActionEnum,
|
NotConfiguredActionEnum,
|
||||||
|
PaginatedAccountLockdownStageList,
|
||||||
PaginatedAuthenticatorDuoStageList,
|
PaginatedAuthenticatorDuoStageList,
|
||||||
PaginatedAuthenticatorEmailStageList,
|
PaginatedAuthenticatorEmailStageList,
|
||||||
PaginatedAuthenticatorEndpointGDTCStageList,
|
PaginatedAuthenticatorEndpointGDTCStageList,
|
||||||
@@ -92,6 +95,7 @@ import type {
|
|||||||
PaginatedWebAuthnDeviceTypeList,
|
PaginatedWebAuthnDeviceTypeList,
|
||||||
PasswordStage,
|
PasswordStage,
|
||||||
PasswordStageRequest,
|
PasswordStageRequest,
|
||||||
|
PatchedAccountLockdownStageRequest,
|
||||||
PatchedAuthenticatorDuoStageRequest,
|
PatchedAuthenticatorDuoStageRequest,
|
||||||
PatchedAuthenticatorEmailStageRequest,
|
PatchedAuthenticatorEmailStageRequest,
|
||||||
PatchedAuthenticatorEndpointGDTCStageRequest,
|
PatchedAuthenticatorEndpointGDTCStageRequest,
|
||||||
@@ -150,6 +154,8 @@ import type {
|
|||||||
WebAuthnDeviceType,
|
WebAuthnDeviceType,
|
||||||
} from "../models/index";
|
} from "../models/index";
|
||||||
import {
|
import {
|
||||||
|
AccountLockdownStageFromJSON,
|
||||||
|
AccountLockdownStageRequestToJSON,
|
||||||
AuthenticatorDuoStageDeviceImportResponseFromJSON,
|
AuthenticatorDuoStageDeviceImportResponseFromJSON,
|
||||||
AuthenticatorDuoStageFromJSON,
|
AuthenticatorDuoStageFromJSON,
|
||||||
AuthenticatorDuoStageManualDeviceImportRequestToJSON,
|
AuthenticatorDuoStageManualDeviceImportRequestToJSON,
|
||||||
@@ -190,6 +196,7 @@ import {
|
|||||||
InvitationStageRequestToJSON,
|
InvitationStageRequestToJSON,
|
||||||
MutualTLSStageFromJSON,
|
MutualTLSStageFromJSON,
|
||||||
MutualTLSStageRequestToJSON,
|
MutualTLSStageRequestToJSON,
|
||||||
|
PaginatedAccountLockdownStageListFromJSON,
|
||||||
PaginatedAuthenticatorDuoStageListFromJSON,
|
PaginatedAuthenticatorDuoStageListFromJSON,
|
||||||
PaginatedAuthenticatorEmailStageListFromJSON,
|
PaginatedAuthenticatorEmailStageListFromJSON,
|
||||||
PaginatedAuthenticatorEndpointGDTCStageListFromJSON,
|
PaginatedAuthenticatorEndpointGDTCStageListFromJSON,
|
||||||
@@ -221,6 +228,7 @@ import {
|
|||||||
PaginatedWebAuthnDeviceTypeListFromJSON,
|
PaginatedWebAuthnDeviceTypeListFromJSON,
|
||||||
PasswordStageFromJSON,
|
PasswordStageFromJSON,
|
||||||
PasswordStageRequestToJSON,
|
PasswordStageRequestToJSON,
|
||||||
|
PatchedAccountLockdownStageRequestToJSON,
|
||||||
PatchedAuthenticatorDuoStageRequestToJSON,
|
PatchedAuthenticatorDuoStageRequestToJSON,
|
||||||
PatchedAuthenticatorEmailStageRequestToJSON,
|
PatchedAuthenticatorEmailStageRequestToJSON,
|
||||||
PatchedAuthenticatorEndpointGDTCStageRequestToJSON,
|
PatchedAuthenticatorEndpointGDTCStageRequestToJSON,
|
||||||
@@ -273,6 +281,46 @@ import {
|
|||||||
} from "../models/index";
|
} from "../models/index";
|
||||||
import * as runtime from "../runtime";
|
import * as runtime from "../runtime";
|
||||||
|
|
||||||
|
export interface StagesAccountLockdownCreateRequest {
|
||||||
|
accountLockdownStageRequest: AccountLockdownStageRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StagesAccountLockdownDestroyRequest {
|
||||||
|
stageUuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StagesAccountLockdownListRequest {
|
||||||
|
deactivateUser?: boolean;
|
||||||
|
deleteSessions?: boolean;
|
||||||
|
name?: string;
|
||||||
|
ordering?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
revokeTokens?: boolean;
|
||||||
|
search?: string;
|
||||||
|
selfServiceCompletionFlow?: string;
|
||||||
|
setUnusablePassword?: boolean;
|
||||||
|
stageUuid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StagesAccountLockdownPartialUpdateRequest {
|
||||||
|
stageUuid: string;
|
||||||
|
patchedAccountLockdownStageRequest?: PatchedAccountLockdownStageRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StagesAccountLockdownRetrieveRequest {
|
||||||
|
stageUuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StagesAccountLockdownUpdateRequest {
|
||||||
|
stageUuid: string;
|
||||||
|
accountLockdownStageRequest: AccountLockdownStageRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StagesAccountLockdownUsedByListRequest {
|
||||||
|
stageUuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StagesAllDestroyRequest {
|
export interface StagesAllDestroyRequest {
|
||||||
stageUuid: string;
|
stageUuid: string;
|
||||||
}
|
}
|
||||||
@@ -1366,6 +1414,534 @@ export interface StagesUserWriteUsedByListRequest {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class StagesApi extends runtime.BaseAPI {
|
export class StagesApi extends runtime.BaseAPI {
|
||||||
|
/**
|
||||||
|
* Creates request options for stagesAccountLockdownCreate without sending the request
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownCreateRequestOpts(
|
||||||
|
requestParameters: StagesAccountLockdownCreateRequest,
|
||||||
|
): Promise<runtime.RequestOpts> {
|
||||||
|
if (requestParameters["accountLockdownStageRequest"] == null) {
|
||||||
|
throw new runtime.RequiredError(
|
||||||
|
"accountLockdownStageRequest",
|
||||||
|
'Required parameter "accountLockdownStageRequest" was null or undefined when calling stagesAccountLockdownCreate().',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
const headerParameters: runtime.HTTPHeaders = {};
|
||||||
|
|
||||||
|
headerParameters["Content-Type"] = "application/json";
|
||||||
|
|
||||||
|
if (this.configuration && this.configuration.accessToken) {
|
||||||
|
const token = this.configuration.accessToken;
|
||||||
|
const tokenString = await token("authentik", []);
|
||||||
|
|
||||||
|
if (tokenString) {
|
||||||
|
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlPath = `/stages/account_lockdown/`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: urlPath,
|
||||||
|
method: "POST",
|
||||||
|
headers: headerParameters,
|
||||||
|
query: queryParameters,
|
||||||
|
body: AccountLockdownStageRequestToJSON(
|
||||||
|
requestParameters["accountLockdownStageRequest"],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownCreateRaw(
|
||||||
|
requestParameters: StagesAccountLockdownCreateRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<runtime.ApiResponse<AccountLockdownStage>> {
|
||||||
|
const requestOptions = await this.stagesAccountLockdownCreateRequestOpts(requestParameters);
|
||||||
|
const response = await this.request(requestOptions, initOverrides);
|
||||||
|
|
||||||
|
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||||
|
AccountLockdownStageFromJSON(jsonValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownCreate(
|
||||||
|
requestParameters: StagesAccountLockdownCreateRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<AccountLockdownStage> {
|
||||||
|
const response = await this.stagesAccountLockdownCreateRaw(
|
||||||
|
requestParameters,
|
||||||
|
initOverrides,
|
||||||
|
);
|
||||||
|
return await response.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates request options for stagesAccountLockdownDestroy without sending the request
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownDestroyRequestOpts(
|
||||||
|
requestParameters: StagesAccountLockdownDestroyRequest,
|
||||||
|
): Promise<runtime.RequestOpts> {
|
||||||
|
if (requestParameters["stageUuid"] == null) {
|
||||||
|
throw new runtime.RequiredError(
|
||||||
|
"stageUuid",
|
||||||
|
'Required parameter "stageUuid" was null or undefined when calling stagesAccountLockdownDestroy().',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
const headerParameters: runtime.HTTPHeaders = {};
|
||||||
|
|
||||||
|
if (this.configuration && this.configuration.accessToken) {
|
||||||
|
const token = this.configuration.accessToken;
|
||||||
|
const tokenString = await token("authentik", []);
|
||||||
|
|
||||||
|
if (tokenString) {
|
||||||
|
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlPath = `/stages/account_lockdown/{stage_uuid}/`;
|
||||||
|
urlPath = urlPath.replace(
|
||||||
|
`{${"stage_uuid"}}`,
|
||||||
|
encodeURIComponent(String(requestParameters["stageUuid"])),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: urlPath,
|
||||||
|
method: "DELETE",
|
||||||
|
headers: headerParameters,
|
||||||
|
query: queryParameters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownDestroyRaw(
|
||||||
|
requestParameters: StagesAccountLockdownDestroyRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<runtime.ApiResponse<void>> {
|
||||||
|
const requestOptions =
|
||||||
|
await this.stagesAccountLockdownDestroyRequestOpts(requestParameters);
|
||||||
|
const response = await this.request(requestOptions, initOverrides);
|
||||||
|
|
||||||
|
return new runtime.VoidApiResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownDestroy(
|
||||||
|
requestParameters: StagesAccountLockdownDestroyRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.stagesAccountLockdownDestroyRaw(requestParameters, initOverrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates request options for stagesAccountLockdownList without sending the request
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownListRequestOpts(
|
||||||
|
requestParameters: StagesAccountLockdownListRequest,
|
||||||
|
): Promise<runtime.RequestOpts> {
|
||||||
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
if (requestParameters["deactivateUser"] != null) {
|
||||||
|
queryParameters["deactivate_user"] = requestParameters["deactivateUser"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["deleteSessions"] != null) {
|
||||||
|
queryParameters["delete_sessions"] = requestParameters["deleteSessions"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["name"] != null) {
|
||||||
|
queryParameters["name"] = requestParameters["name"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["ordering"] != null) {
|
||||||
|
queryParameters["ordering"] = requestParameters["ordering"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["page"] != null) {
|
||||||
|
queryParameters["page"] = requestParameters["page"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["pageSize"] != null) {
|
||||||
|
queryParameters["page_size"] = requestParameters["pageSize"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["revokeTokens"] != null) {
|
||||||
|
queryParameters["revoke_tokens"] = requestParameters["revokeTokens"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["search"] != null) {
|
||||||
|
queryParameters["search"] = requestParameters["search"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["selfServiceCompletionFlow"] != null) {
|
||||||
|
queryParameters["self_service_completion_flow"] =
|
||||||
|
requestParameters["selfServiceCompletionFlow"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["setUnusablePassword"] != null) {
|
||||||
|
queryParameters["set_unusable_password"] = requestParameters["setUnusablePassword"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["stageUuid"] != null) {
|
||||||
|
queryParameters["stage_uuid"] = requestParameters["stageUuid"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerParameters: runtime.HTTPHeaders = {};
|
||||||
|
|
||||||
|
if (this.configuration && this.configuration.accessToken) {
|
||||||
|
const token = this.configuration.accessToken;
|
||||||
|
const tokenString = await token("authentik", []);
|
||||||
|
|
||||||
|
if (tokenString) {
|
||||||
|
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlPath = `/stages/account_lockdown/`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: urlPath,
|
||||||
|
method: "GET",
|
||||||
|
headers: headerParameters,
|
||||||
|
query: queryParameters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownListRaw(
|
||||||
|
requestParameters: StagesAccountLockdownListRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<runtime.ApiResponse<PaginatedAccountLockdownStageList>> {
|
||||||
|
const requestOptions = await this.stagesAccountLockdownListRequestOpts(requestParameters);
|
||||||
|
const response = await this.request(requestOptions, initOverrides);
|
||||||
|
|
||||||
|
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||||
|
PaginatedAccountLockdownStageListFromJSON(jsonValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownList(
|
||||||
|
requestParameters: StagesAccountLockdownListRequest = {},
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<PaginatedAccountLockdownStageList> {
|
||||||
|
const response = await this.stagesAccountLockdownListRaw(requestParameters, initOverrides);
|
||||||
|
return await response.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates request options for stagesAccountLockdownPartialUpdate without sending the request
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownPartialUpdateRequestOpts(
|
||||||
|
requestParameters: StagesAccountLockdownPartialUpdateRequest,
|
||||||
|
): Promise<runtime.RequestOpts> {
|
||||||
|
if (requestParameters["stageUuid"] == null) {
|
||||||
|
throw new runtime.RequiredError(
|
||||||
|
"stageUuid",
|
||||||
|
'Required parameter "stageUuid" was null or undefined when calling stagesAccountLockdownPartialUpdate().',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
const headerParameters: runtime.HTTPHeaders = {};
|
||||||
|
|
||||||
|
headerParameters["Content-Type"] = "application/json";
|
||||||
|
|
||||||
|
if (this.configuration && this.configuration.accessToken) {
|
||||||
|
const token = this.configuration.accessToken;
|
||||||
|
const tokenString = await token("authentik", []);
|
||||||
|
|
||||||
|
if (tokenString) {
|
||||||
|
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlPath = `/stages/account_lockdown/{stage_uuid}/`;
|
||||||
|
urlPath = urlPath.replace(
|
||||||
|
`{${"stage_uuid"}}`,
|
||||||
|
encodeURIComponent(String(requestParameters["stageUuid"])),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: urlPath,
|
||||||
|
method: "PATCH",
|
||||||
|
headers: headerParameters,
|
||||||
|
query: queryParameters,
|
||||||
|
body: PatchedAccountLockdownStageRequestToJSON(
|
||||||
|
requestParameters["patchedAccountLockdownStageRequest"],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownPartialUpdateRaw(
|
||||||
|
requestParameters: StagesAccountLockdownPartialUpdateRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<runtime.ApiResponse<AccountLockdownStage>> {
|
||||||
|
const requestOptions =
|
||||||
|
await this.stagesAccountLockdownPartialUpdateRequestOpts(requestParameters);
|
||||||
|
const response = await this.request(requestOptions, initOverrides);
|
||||||
|
|
||||||
|
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||||
|
AccountLockdownStageFromJSON(jsonValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownPartialUpdate(
|
||||||
|
requestParameters: StagesAccountLockdownPartialUpdateRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<AccountLockdownStage> {
|
||||||
|
const response = await this.stagesAccountLockdownPartialUpdateRaw(
|
||||||
|
requestParameters,
|
||||||
|
initOverrides,
|
||||||
|
);
|
||||||
|
return await response.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates request options for stagesAccountLockdownRetrieve without sending the request
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownRetrieveRequestOpts(
|
||||||
|
requestParameters: StagesAccountLockdownRetrieveRequest,
|
||||||
|
): Promise<runtime.RequestOpts> {
|
||||||
|
if (requestParameters["stageUuid"] == null) {
|
||||||
|
throw new runtime.RequiredError(
|
||||||
|
"stageUuid",
|
||||||
|
'Required parameter "stageUuid" was null or undefined when calling stagesAccountLockdownRetrieve().',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
const headerParameters: runtime.HTTPHeaders = {};
|
||||||
|
|
||||||
|
if (this.configuration && this.configuration.accessToken) {
|
||||||
|
const token = this.configuration.accessToken;
|
||||||
|
const tokenString = await token("authentik", []);
|
||||||
|
|
||||||
|
if (tokenString) {
|
||||||
|
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlPath = `/stages/account_lockdown/{stage_uuid}/`;
|
||||||
|
urlPath = urlPath.replace(
|
||||||
|
`{${"stage_uuid"}}`,
|
||||||
|
encodeURIComponent(String(requestParameters["stageUuid"])),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: urlPath,
|
||||||
|
method: "GET",
|
||||||
|
headers: headerParameters,
|
||||||
|
query: queryParameters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownRetrieveRaw(
|
||||||
|
requestParameters: StagesAccountLockdownRetrieveRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<runtime.ApiResponse<AccountLockdownStage>> {
|
||||||
|
const requestOptions =
|
||||||
|
await this.stagesAccountLockdownRetrieveRequestOpts(requestParameters);
|
||||||
|
const response = await this.request(requestOptions, initOverrides);
|
||||||
|
|
||||||
|
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||||
|
AccountLockdownStageFromJSON(jsonValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownRetrieve(
|
||||||
|
requestParameters: StagesAccountLockdownRetrieveRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<AccountLockdownStage> {
|
||||||
|
const response = await this.stagesAccountLockdownRetrieveRaw(
|
||||||
|
requestParameters,
|
||||||
|
initOverrides,
|
||||||
|
);
|
||||||
|
return await response.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates request options for stagesAccountLockdownUpdate without sending the request
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownUpdateRequestOpts(
|
||||||
|
requestParameters: StagesAccountLockdownUpdateRequest,
|
||||||
|
): Promise<runtime.RequestOpts> {
|
||||||
|
if (requestParameters["stageUuid"] == null) {
|
||||||
|
throw new runtime.RequiredError(
|
||||||
|
"stageUuid",
|
||||||
|
'Required parameter "stageUuid" was null or undefined when calling stagesAccountLockdownUpdate().',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestParameters["accountLockdownStageRequest"] == null) {
|
||||||
|
throw new runtime.RequiredError(
|
||||||
|
"accountLockdownStageRequest",
|
||||||
|
'Required parameter "accountLockdownStageRequest" was null or undefined when calling stagesAccountLockdownUpdate().',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
const headerParameters: runtime.HTTPHeaders = {};
|
||||||
|
|
||||||
|
headerParameters["Content-Type"] = "application/json";
|
||||||
|
|
||||||
|
if (this.configuration && this.configuration.accessToken) {
|
||||||
|
const token = this.configuration.accessToken;
|
||||||
|
const tokenString = await token("authentik", []);
|
||||||
|
|
||||||
|
if (tokenString) {
|
||||||
|
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlPath = `/stages/account_lockdown/{stage_uuid}/`;
|
||||||
|
urlPath = urlPath.replace(
|
||||||
|
`{${"stage_uuid"}}`,
|
||||||
|
encodeURIComponent(String(requestParameters["stageUuid"])),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: urlPath,
|
||||||
|
method: "PUT",
|
||||||
|
headers: headerParameters,
|
||||||
|
query: queryParameters,
|
||||||
|
body: AccountLockdownStageRequestToJSON(
|
||||||
|
requestParameters["accountLockdownStageRequest"],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownUpdateRaw(
|
||||||
|
requestParameters: StagesAccountLockdownUpdateRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<runtime.ApiResponse<AccountLockdownStage>> {
|
||||||
|
const requestOptions = await this.stagesAccountLockdownUpdateRequestOpts(requestParameters);
|
||||||
|
const response = await this.request(requestOptions, initOverrides);
|
||||||
|
|
||||||
|
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||||
|
AccountLockdownStageFromJSON(jsonValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Viewset
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownUpdate(
|
||||||
|
requestParameters: StagesAccountLockdownUpdateRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<AccountLockdownStage> {
|
||||||
|
const response = await this.stagesAccountLockdownUpdateRaw(
|
||||||
|
requestParameters,
|
||||||
|
initOverrides,
|
||||||
|
);
|
||||||
|
return await response.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates request options for stagesAccountLockdownUsedByList without sending the request
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownUsedByListRequestOpts(
|
||||||
|
requestParameters: StagesAccountLockdownUsedByListRequest,
|
||||||
|
): Promise<runtime.RequestOpts> {
|
||||||
|
if (requestParameters["stageUuid"] == null) {
|
||||||
|
throw new runtime.RequiredError(
|
||||||
|
"stageUuid",
|
||||||
|
'Required parameter "stageUuid" was null or undefined when calling stagesAccountLockdownUsedByList().',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
const headerParameters: runtime.HTTPHeaders = {};
|
||||||
|
|
||||||
|
if (this.configuration && this.configuration.accessToken) {
|
||||||
|
const token = this.configuration.accessToken;
|
||||||
|
const tokenString = await token("authentik", []);
|
||||||
|
|
||||||
|
if (tokenString) {
|
||||||
|
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlPath = `/stages/account_lockdown/{stage_uuid}/used_by/`;
|
||||||
|
urlPath = urlPath.replace(
|
||||||
|
`{${"stage_uuid"}}`,
|
||||||
|
encodeURIComponent(String(requestParameters["stageUuid"])),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: urlPath,
|
||||||
|
method: "GET",
|
||||||
|
headers: headerParameters,
|
||||||
|
query: queryParameters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all objects that use this object
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownUsedByListRaw(
|
||||||
|
requestParameters: StagesAccountLockdownUsedByListRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<runtime.ApiResponse<Array<UsedBy>>> {
|
||||||
|
const requestOptions =
|
||||||
|
await this.stagesAccountLockdownUsedByListRequestOpts(requestParameters);
|
||||||
|
const response = await this.request(requestOptions, initOverrides);
|
||||||
|
|
||||||
|
return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(UsedByFromJSON));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all objects that use this object
|
||||||
|
*/
|
||||||
|
async stagesAccountLockdownUsedByList(
|
||||||
|
requestParameters: StagesAccountLockdownUsedByListRequest,
|
||||||
|
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||||
|
): Promise<Array<UsedBy>> {
|
||||||
|
const response = await this.stagesAccountLockdownUsedByListRaw(
|
||||||
|
requestParameters,
|
||||||
|
initOverrides,
|
||||||
|
);
|
||||||
|
return await response.value();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates request options for stagesAllDestroy without sending the request
|
* Creates request options for stagesAllDestroy without sending the request
|
||||||
*/
|
*/
|
||||||
|
|||||||
166
packages/client-ts/src/models/AccountLockdownStage.ts
generated
Normal file
166
packages/client-ts/src/models/AccountLockdownStage.ts
generated
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* authentik
|
||||||
|
* Making authentication simple.
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||||
|
* Contact: hello@goauthentik.io
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { FlowSet } from "./FlowSet";
|
||||||
|
import { FlowSetFromJSON } from "./FlowSet";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Serializer
|
||||||
|
* @export
|
||||||
|
* @interface AccountLockdownStage
|
||||||
|
*/
|
||||||
|
export interface AccountLockdownStage {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
readonly pk: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Get object type so that we know how to edit the object
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
readonly component: string;
|
||||||
|
/**
|
||||||
|
* Return object's verbose_name
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
readonly verboseName: string;
|
||||||
|
/**
|
||||||
|
* Return object's plural verbose_name
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
readonly verboseNamePlural: string;
|
||||||
|
/**
|
||||||
|
* Return internal model name
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
readonly metaModelName: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<FlowSet>}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
readonly flowSet: Array<FlowSet>;
|
||||||
|
/**
|
||||||
|
* Deactivate the user account (set is_active to False)
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
deactivateUser?: boolean;
|
||||||
|
/**
|
||||||
|
* Set an unusable password for the user
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
setUnusablePassword?: boolean;
|
||||||
|
/**
|
||||||
|
* Delete all active sessions for the user
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
deleteSessions?: boolean;
|
||||||
|
/**
|
||||||
|
* Revoke all tokens for the user (API, app password, recovery, verification, OAuth)
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
revokeTokens?: boolean;
|
||||||
|
/**
|
||||||
|
* Flow to redirect users to after self-service lockdown. This flow should not require authentication since the user's session is deleted.
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AccountLockdownStage
|
||||||
|
*/
|
||||||
|
selfServiceCompletionFlow?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given object implements the AccountLockdownStage interface.
|
||||||
|
*/
|
||||||
|
export function instanceOfAccountLockdownStage(value: object): value is AccountLockdownStage {
|
||||||
|
if (!("pk" in value) || value["pk"] === undefined) return false;
|
||||||
|
if (!("name" in value) || value["name"] === undefined) return false;
|
||||||
|
if (!("component" in value) || value["component"] === undefined) return false;
|
||||||
|
if (!("verboseName" in value) || value["verboseName"] === undefined) return false;
|
||||||
|
if (!("verboseNamePlural" in value) || value["verboseNamePlural"] === undefined) return false;
|
||||||
|
if (!("metaModelName" in value) || value["metaModelName"] === undefined) return false;
|
||||||
|
if (!("flowSet" in value) || value["flowSet"] === undefined) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountLockdownStageFromJSON(json: any): AccountLockdownStage {
|
||||||
|
return AccountLockdownStageFromJSONTyped(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountLockdownStageFromJSONTyped(
|
||||||
|
json: any,
|
||||||
|
ignoreDiscriminator: boolean,
|
||||||
|
): AccountLockdownStage {
|
||||||
|
if (json == null) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pk: json["pk"],
|
||||||
|
name: json["name"],
|
||||||
|
component: json["component"],
|
||||||
|
verboseName: json["verbose_name"],
|
||||||
|
verboseNamePlural: json["verbose_name_plural"],
|
||||||
|
metaModelName: json["meta_model_name"],
|
||||||
|
flowSet: (json["flow_set"] as Array<any>).map(FlowSetFromJSON),
|
||||||
|
deactivateUser: json["deactivate_user"] == null ? undefined : json["deactivate_user"],
|
||||||
|
setUnusablePassword:
|
||||||
|
json["set_unusable_password"] == null ? undefined : json["set_unusable_password"],
|
||||||
|
deleteSessions: json["delete_sessions"] == null ? undefined : json["delete_sessions"],
|
||||||
|
revokeTokens: json["revoke_tokens"] == null ? undefined : json["revoke_tokens"],
|
||||||
|
selfServiceCompletionFlow:
|
||||||
|
json["self_service_completion_flow"] == null
|
||||||
|
? undefined
|
||||||
|
: json["self_service_completion_flow"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountLockdownStageToJSON(json: any): AccountLockdownStage {
|
||||||
|
return AccountLockdownStageToJSONTyped(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountLockdownStageToJSONTyped(
|
||||||
|
value?: Omit<
|
||||||
|
AccountLockdownStage,
|
||||||
|
"pk" | "component" | "verbose_name" | "verbose_name_plural" | "meta_model_name" | "flow_set"
|
||||||
|
> | null,
|
||||||
|
ignoreDiscriminator: boolean = false,
|
||||||
|
): any {
|
||||||
|
if (value == null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: value["name"],
|
||||||
|
deactivate_user: value["deactivateUser"],
|
||||||
|
set_unusable_password: value["setUnusablePassword"],
|
||||||
|
delete_sessions: value["deleteSessions"],
|
||||||
|
revoke_tokens: value["revokeTokens"],
|
||||||
|
self_service_completion_flow: value["selfServiceCompletionFlow"],
|
||||||
|
};
|
||||||
|
}
|
||||||
114
packages/client-ts/src/models/AccountLockdownStageRequest.ts
generated
Normal file
114
packages/client-ts/src/models/AccountLockdownStageRequest.ts
generated
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* authentik
|
||||||
|
* Making authentication simple.
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||||
|
* Contact: hello@goauthentik.io
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccountLockdownStage Serializer
|
||||||
|
* @export
|
||||||
|
* @interface AccountLockdownStageRequest
|
||||||
|
*/
|
||||||
|
export interface AccountLockdownStageRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AccountLockdownStageRequest
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Deactivate the user account (set is_active to False)
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AccountLockdownStageRequest
|
||||||
|
*/
|
||||||
|
deactivateUser?: boolean;
|
||||||
|
/**
|
||||||
|
* Set an unusable password for the user
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AccountLockdownStageRequest
|
||||||
|
*/
|
||||||
|
setUnusablePassword?: boolean;
|
||||||
|
/**
|
||||||
|
* Delete all active sessions for the user
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AccountLockdownStageRequest
|
||||||
|
*/
|
||||||
|
deleteSessions?: boolean;
|
||||||
|
/**
|
||||||
|
* Revoke all tokens for the user (API, app password, recovery, verification, OAuth)
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AccountLockdownStageRequest
|
||||||
|
*/
|
||||||
|
revokeTokens?: boolean;
|
||||||
|
/**
|
||||||
|
* Flow to redirect users to after self-service lockdown. This flow should not require authentication since the user's session is deleted.
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AccountLockdownStageRequest
|
||||||
|
*/
|
||||||
|
selfServiceCompletionFlow?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given object implements the AccountLockdownStageRequest interface.
|
||||||
|
*/
|
||||||
|
export function instanceOfAccountLockdownStageRequest(
|
||||||
|
value: object,
|
||||||
|
): value is AccountLockdownStageRequest {
|
||||||
|
if (!("name" in value) || value["name"] === undefined) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountLockdownStageRequestFromJSON(json: any): AccountLockdownStageRequest {
|
||||||
|
return AccountLockdownStageRequestFromJSONTyped(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountLockdownStageRequestFromJSONTyped(
|
||||||
|
json: any,
|
||||||
|
ignoreDiscriminator: boolean,
|
||||||
|
): AccountLockdownStageRequest {
|
||||||
|
if (json == null) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: json["name"],
|
||||||
|
deactivateUser: json["deactivate_user"] == null ? undefined : json["deactivate_user"],
|
||||||
|
setUnusablePassword:
|
||||||
|
json["set_unusable_password"] == null ? undefined : json["set_unusable_password"],
|
||||||
|
deleteSessions: json["delete_sessions"] == null ? undefined : json["delete_sessions"],
|
||||||
|
revokeTokens: json["revoke_tokens"] == null ? undefined : json["revoke_tokens"],
|
||||||
|
selfServiceCompletionFlow:
|
||||||
|
json["self_service_completion_flow"] == null
|
||||||
|
? undefined
|
||||||
|
: json["self_service_completion_flow"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountLockdownStageRequestToJSON(json: any): AccountLockdownStageRequest {
|
||||||
|
return AccountLockdownStageRequestToJSONTyped(json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountLockdownStageRequestToJSONTyped(
|
||||||
|
value?: AccountLockdownStageRequest | null,
|
||||||
|
ignoreDiscriminator: boolean = false,
|
||||||
|
): any {
|
||||||
|
if (value == null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: value["name"],
|
||||||
|
deactivate_user: value["deactivateUser"],
|
||||||
|
set_unusable_password: value["setUnusablePassword"],
|
||||||
|
delete_sessions: value["deleteSessions"],
|
||||||
|
revoke_tokens: value["revokeTokens"],
|
||||||
|
self_service_completion_flow: value["selfServiceCompletionFlow"],
|
||||||
|
};
|
||||||
|
}
|
||||||
1
packages/client-ts/src/models/AppEnum.ts
generated
1
packages/client-ts/src/models/AppEnum.ts
generated
@@ -94,6 +94,7 @@ export const AppEnum = {
|
|||||||
AuthentikEnterpriseProvidersSsf: "authentik.enterprise.providers.ssf",
|
AuthentikEnterpriseProvidersSsf: "authentik.enterprise.providers.ssf",
|
||||||
AuthentikEnterpriseProvidersWsFederation: "authentik.enterprise.providers.ws_federation",
|
AuthentikEnterpriseProvidersWsFederation: "authentik.enterprise.providers.ws_federation",
|
||||||
AuthentikEnterpriseReports: "authentik.enterprise.reports",
|
AuthentikEnterpriseReports: "authentik.enterprise.reports",
|
||||||
|
AuthentikEnterpriseStagesAccountLockdown: "authentik.enterprise.stages.account_lockdown",
|
||||||
AuthentikEnterpriseStagesAuthenticatorEndpointGdtc:
|
AuthentikEnterpriseStagesAuthenticatorEndpointGdtc:
|
||||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||||
AuthentikEnterpriseStagesMtls: "authentik.enterprise.stages.mtls",
|
AuthentikEnterpriseStagesMtls: "authentik.enterprise.stages.mtls",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface AuthenticatedSessionUserAgentDevice {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof AuthenticatedSessionUserAgentDevice
|
* @memberof AuthenticatedSessionUserAgentDevice
|
||||||
*/
|
*/
|
||||||
brand: string;
|
brand: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -35,7 +35,7 @@ export interface AuthenticatedSessionUserAgentDevice {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof AuthenticatedSessionUserAgentDevice
|
* @memberof AuthenticatedSessionUserAgentDevice
|
||||||
*/
|
*/
|
||||||
model: string;
|
model: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,25 +29,25 @@ export interface AuthenticatedSessionUserAgentOs {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof AuthenticatedSessionUserAgentOs
|
* @memberof AuthenticatedSessionUserAgentOs
|
||||||
*/
|
*/
|
||||||
major: string;
|
major: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof AuthenticatedSessionUserAgentOs
|
* @memberof AuthenticatedSessionUserAgentOs
|
||||||
*/
|
*/
|
||||||
minor: string;
|
minor: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof AuthenticatedSessionUserAgentOs
|
* @memberof AuthenticatedSessionUserAgentOs
|
||||||
*/
|
*/
|
||||||
patch: string;
|
patch: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof AuthenticatedSessionUserAgentOs
|
* @memberof AuthenticatedSessionUserAgentOs
|
||||||
*/
|
*/
|
||||||
patchMinor: string;
|
patchMinor: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
8
packages/client-ts/src/models/Brand.ts
generated
8
packages/client-ts/src/models/Brand.ts
generated
@@ -102,6 +102,12 @@ export interface Brand {
|
|||||||
* @memberof Brand
|
* @memberof Brand
|
||||||
*/
|
*/
|
||||||
flowDeviceCode?: string | null;
|
flowDeviceCode?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof Brand
|
||||||
|
*/
|
||||||
|
flowLockdown?: string | null;
|
||||||
/**
|
/**
|
||||||
* When set, external users will be redirected to this application after authenticating.
|
* When set, external users will be redirected to this application after authenticating.
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -166,6 +172,7 @@ export function BrandFromJSONTyped(json: any, ignoreDiscriminator: boolean): Bra
|
|||||||
flowUserSettings:
|
flowUserSettings:
|
||||||
json["flow_user_settings"] == null ? undefined : json["flow_user_settings"],
|
json["flow_user_settings"] == null ? undefined : json["flow_user_settings"],
|
||||||
flowDeviceCode: json["flow_device_code"] == null ? undefined : json["flow_device_code"],
|
flowDeviceCode: json["flow_device_code"] == null ? undefined : json["flow_device_code"],
|
||||||
|
flowLockdown: json["flow_lockdown"] == null ? undefined : json["flow_lockdown"],
|
||||||
defaultApplication:
|
defaultApplication:
|
||||||
json["default_application"] == null ? undefined : json["default_application"],
|
json["default_application"] == null ? undefined : json["default_application"],
|
||||||
webCertificate: json["web_certificate"] == null ? undefined : json["web_certificate"],
|
webCertificate: json["web_certificate"] == null ? undefined : json["web_certificate"],
|
||||||
@@ -201,6 +208,7 @@ export function BrandToJSONTyped(
|
|||||||
flow_unenrollment: value["flowUnenrollment"],
|
flow_unenrollment: value["flowUnenrollment"],
|
||||||
flow_user_settings: value["flowUserSettings"],
|
flow_user_settings: value["flowUserSettings"],
|
||||||
flow_device_code: value["flowDeviceCode"],
|
flow_device_code: value["flowDeviceCode"],
|
||||||
|
flow_lockdown: value["flowLockdown"],
|
||||||
default_application: value["defaultApplication"],
|
default_application: value["defaultApplication"],
|
||||||
web_certificate: value["webCertificate"],
|
web_certificate: value["webCertificate"],
|
||||||
client_certificates: value["clientCertificates"],
|
client_certificates: value["clientCertificates"],
|
||||||
|
|||||||
8
packages/client-ts/src/models/BrandRequest.ts
generated
8
packages/client-ts/src/models/BrandRequest.ts
generated
@@ -96,6 +96,12 @@ export interface BrandRequest {
|
|||||||
* @memberof BrandRequest
|
* @memberof BrandRequest
|
||||||
*/
|
*/
|
||||||
flowDeviceCode?: string | null;
|
flowDeviceCode?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof BrandRequest
|
||||||
|
*/
|
||||||
|
flowLockdown?: string | null;
|
||||||
/**
|
/**
|
||||||
* When set, external users will be redirected to this application after authenticating.
|
* When set, external users will be redirected to this application after authenticating.
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -158,6 +164,7 @@ export function BrandRequestFromJSONTyped(json: any, ignoreDiscriminator: boolea
|
|||||||
flowUserSettings:
|
flowUserSettings:
|
||||||
json["flow_user_settings"] == null ? undefined : json["flow_user_settings"],
|
json["flow_user_settings"] == null ? undefined : json["flow_user_settings"],
|
||||||
flowDeviceCode: json["flow_device_code"] == null ? undefined : json["flow_device_code"],
|
flowDeviceCode: json["flow_device_code"] == null ? undefined : json["flow_device_code"],
|
||||||
|
flowLockdown: json["flow_lockdown"] == null ? undefined : json["flow_lockdown"],
|
||||||
defaultApplication:
|
defaultApplication:
|
||||||
json["default_application"] == null ? undefined : json["default_application"],
|
json["default_application"] == null ? undefined : json["default_application"],
|
||||||
webCertificate: json["web_certificate"] == null ? undefined : json["web_certificate"],
|
webCertificate: json["web_certificate"] == null ? undefined : json["web_certificate"],
|
||||||
@@ -193,6 +200,7 @@ export function BrandRequestToJSONTyped(
|
|||||||
flow_unenrollment: value["flowUnenrollment"],
|
flow_unenrollment: value["flowUnenrollment"],
|
||||||
flow_user_settings: value["flowUserSettings"],
|
flow_user_settings: value["flowUserSettings"],
|
||||||
flow_device_code: value["flowDeviceCode"],
|
flow_device_code: value["flowDeviceCode"],
|
||||||
|
flow_lockdown: value["flowLockdown"],
|
||||||
default_application: value["defaultApplication"],
|
default_application: value["defaultApplication"],
|
||||||
web_certificate: value["webCertificate"],
|
web_certificate: value["webCertificate"],
|
||||||
client_certificates: value["clientCertificates"],
|
client_certificates: value["clientCertificates"],
|
||||||
|
|||||||
8
packages/client-ts/src/models/CurrentBrand.ts
generated
8
packages/client-ts/src/models/CurrentBrand.ts
generated
@@ -117,6 +117,12 @@ export interface CurrentBrand {
|
|||||||
* @memberof CurrentBrand
|
* @memberof CurrentBrand
|
||||||
*/
|
*/
|
||||||
flowDeviceCode?: string;
|
flowDeviceCode?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof CurrentBrand
|
||||||
|
*/
|
||||||
|
flowLockdown?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -177,6 +183,7 @@ export function CurrentBrandFromJSONTyped(json: any, ignoreDiscriminator: boolea
|
|||||||
flowUserSettings:
|
flowUserSettings:
|
||||||
json["flow_user_settings"] == null ? undefined : json["flow_user_settings"],
|
json["flow_user_settings"] == null ? undefined : json["flow_user_settings"],
|
||||||
flowDeviceCode: json["flow_device_code"] == null ? undefined : json["flow_device_code"],
|
flowDeviceCode: json["flow_device_code"] == null ? undefined : json["flow_device_code"],
|
||||||
|
flowLockdown: json["flow_lockdown"] == null ? undefined : json["flow_lockdown"],
|
||||||
defaultLocale: json["default_locale"],
|
defaultLocale: json["default_locale"],
|
||||||
flags: CurrentBrandFlagsFromJSON(json["flags"]),
|
flags: CurrentBrandFlagsFromJSON(json["flags"]),
|
||||||
};
|
};
|
||||||
@@ -213,6 +220,7 @@ export function CurrentBrandToJSONTyped(
|
|||||||
flow_unenrollment: value["flowUnenrollment"],
|
flow_unenrollment: value["flowUnenrollment"],
|
||||||
flow_user_settings: value["flowUserSettings"],
|
flow_user_settings: value["flowUserSettings"],
|
||||||
flow_device_code: value["flowDeviceCode"],
|
flow_device_code: value["flowDeviceCode"],
|
||||||
|
flow_lockdown: value["flowLockdown"],
|
||||||
flags: CurrentBrandFlagsToJSON(value["flags"]),
|
flags: CurrentBrandFlagsToJSON(value["flags"]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
12
packages/client-ts/src/models/EventsRequestedEnum.ts
generated
12
packages/client-ts/src/models/EventsRequestedEnum.ts
generated
@@ -19,8 +19,20 @@
|
|||||||
export const EventsRequestedEnum = {
|
export const EventsRequestedEnum = {
|
||||||
HttpsSchemasOpenidNetSeceventCaepEventTypeSessionRevoked:
|
HttpsSchemasOpenidNetSeceventCaepEventTypeSessionRevoked:
|
||||||
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
|
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
|
||||||
|
HttpsSchemasOpenidNetSeceventCaepEventTypeTokenClaimsChange:
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/token-claims-change",
|
||||||
HttpsSchemasOpenidNetSeceventCaepEventTypeCredentialChange:
|
HttpsSchemasOpenidNetSeceventCaepEventTypeCredentialChange:
|
||||||
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
|
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
|
||||||
|
HttpsSchemasOpenidNetSeceventCaepEventTypeAssuranceLevelChange:
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/assurance-level-change",
|
||||||
|
HttpsSchemasOpenidNetSeceventCaepEventTypeDeviceComplianceChange:
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/device-compliance-change",
|
||||||
|
HttpsSchemasOpenidNetSeceventCaepEventTypeSessionEstablished:
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/session-established",
|
||||||
|
HttpsSchemasOpenidNetSeceventCaepEventTypeSessionPresented:
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/session-presented",
|
||||||
|
HttpsSchemasOpenidNetSeceventCaepEventTypeRiskLevelChange:
|
||||||
|
"https://schemas.openid.net/secevent/caep/event-type/risk-level-change",
|
||||||
HttpsSchemasOpenidNetSeceventSsfEventTypeVerification:
|
HttpsSchemasOpenidNetSeceventSsfEventTypeVerification:
|
||||||
"https://schemas.openid.net/secevent/ssf/event-type/verification",
|
"https://schemas.openid.net/secevent/ssf/event-type/verification",
|
||||||
UnknownDefaultOpenApi: "11184809",
|
UnknownDefaultOpenApi: "11184809",
|
||||||
|
|||||||
42
packages/client-ts/src/models/LifecycleIteration.ts
generated
42
packages/client-ts/src/models/LifecycleIteration.ts
generated
@@ -16,12 +16,10 @@ import type { ContentTypeEnum } from "./ContentTypeEnum";
|
|||||||
import { ContentTypeEnumFromJSON, ContentTypeEnumToJSON } from "./ContentTypeEnum";
|
import { ContentTypeEnumFromJSON, ContentTypeEnumToJSON } from "./ContentTypeEnum";
|
||||||
import type { LifecycleIterationStateEnum } from "./LifecycleIterationStateEnum";
|
import type { LifecycleIterationStateEnum } from "./LifecycleIterationStateEnum";
|
||||||
import { LifecycleIterationStateEnumFromJSON } from "./LifecycleIterationStateEnum";
|
import { LifecycleIterationStateEnumFromJSON } from "./LifecycleIterationStateEnum";
|
||||||
|
import type { RelatedRule } from "./RelatedRule";
|
||||||
|
import { RelatedRuleFromJSON } from "./RelatedRule";
|
||||||
import type { Review } from "./Review";
|
import type { Review } from "./Review";
|
||||||
import { ReviewFromJSON } from "./Review";
|
import { ReviewFromJSON } from "./Review";
|
||||||
import type { ReviewerGroup } from "./ReviewerGroup";
|
|
||||||
import { ReviewerGroupFromJSON } from "./ReviewerGroup";
|
|
||||||
import type { ReviewerUser } from "./ReviewerUser";
|
|
||||||
import { ReviewerUserFromJSON } from "./ReviewerUser";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mixin to validate that a valid enterprise license
|
* Mixin to validate that a valid enterprise license
|
||||||
@@ -90,30 +88,18 @@ export interface LifecycleIteration {
|
|||||||
* @memberof LifecycleIteration
|
* @memberof LifecycleIteration
|
||||||
*/
|
*/
|
||||||
readonly reviews: Array<Review>;
|
readonly reviews: Array<Review>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {RelatedRule}
|
||||||
|
* @memberof LifecycleIteration
|
||||||
|
*/
|
||||||
|
readonly rule: RelatedRule;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof LifecycleIteration
|
* @memberof LifecycleIteration
|
||||||
*/
|
*/
|
||||||
readonly userCanReview: boolean;
|
readonly userCanReview: boolean;
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {Array<ReviewerGroup>}
|
|
||||||
* @memberof LifecycleIteration
|
|
||||||
*/
|
|
||||||
readonly reviewerGroups: Array<ReviewerGroup>;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
* @memberof LifecycleIteration
|
|
||||||
*/
|
|
||||||
readonly minReviewers: number;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {Array<ReviewerUser>}
|
|
||||||
* @memberof LifecycleIteration
|
|
||||||
*/
|
|
||||||
readonly reviewers: Array<ReviewerUser>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,10 +116,8 @@ export function instanceOfLifecycleIteration(value: object): value is LifecycleI
|
|||||||
if (!("gracePeriodEnd" in value) || value["gracePeriodEnd"] === undefined) return false;
|
if (!("gracePeriodEnd" in value) || value["gracePeriodEnd"] === undefined) return false;
|
||||||
if (!("nextReviewDate" in value) || value["nextReviewDate"] === undefined) return false;
|
if (!("nextReviewDate" in value) || value["nextReviewDate"] === undefined) return false;
|
||||||
if (!("reviews" in value) || value["reviews"] === undefined) return false;
|
if (!("reviews" in value) || value["reviews"] === undefined) return false;
|
||||||
|
if (!("rule" in value) || value["rule"] === undefined) return false;
|
||||||
if (!("userCanReview" in value) || value["userCanReview"] === undefined) return false;
|
if (!("userCanReview" in value) || value["userCanReview"] === undefined) return false;
|
||||||
if (!("reviewerGroups" in value) || value["reviewerGroups"] === undefined) return false;
|
|
||||||
if (!("minReviewers" in value) || value["minReviewers"] === undefined) return false;
|
|
||||||
if (!("reviewers" in value) || value["reviewers"] === undefined) return false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,10 +143,8 @@ export function LifecycleIterationFromJSONTyped(
|
|||||||
gracePeriodEnd: new Date(json["grace_period_end"]),
|
gracePeriodEnd: new Date(json["grace_period_end"]),
|
||||||
nextReviewDate: new Date(json["next_review_date"]),
|
nextReviewDate: new Date(json["next_review_date"]),
|
||||||
reviews: (json["reviews"] as Array<any>).map(ReviewFromJSON),
|
reviews: (json["reviews"] as Array<any>).map(ReviewFromJSON),
|
||||||
|
rule: RelatedRuleFromJSON(json["rule"]),
|
||||||
userCanReview: json["user_can_review"],
|
userCanReview: json["user_can_review"],
|
||||||
reviewerGroups: (json["reviewer_groups"] as Array<any>).map(ReviewerGroupFromJSON),
|
|
||||||
minReviewers: json["min_reviewers"],
|
|
||||||
reviewers: (json["reviewers"] as Array<any>).map(ReviewerUserFromJSON),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,10 +164,8 @@ export function LifecycleIterationToJSONTyped(
|
|||||||
| "grace_period_end"
|
| "grace_period_end"
|
||||||
| "next_review_date"
|
| "next_review_date"
|
||||||
| "reviews"
|
| "reviews"
|
||||||
|
| "rule"
|
||||||
| "user_can_review"
|
| "user_can_review"
|
||||||
| "reviewer_groups"
|
|
||||||
| "min_reviewers"
|
|
||||||
| "reviewers"
|
|
||||||
> | null,
|
> | null,
|
||||||
ignoreDiscriminator: boolean = false,
|
ignoreDiscriminator: boolean = false,
|
||||||
): any {
|
): any {
|
||||||
|
|||||||
2
packages/client-ts/src/models/ModelEnum.ts
generated
2
packages/client-ts/src/models/ModelEnum.ts
generated
@@ -175,6 +175,8 @@ export const ModelEnum = {
|
|||||||
AuthentikProvidersWsFederationWsfederationprovider:
|
AuthentikProvidersWsFederationWsfederationprovider:
|
||||||
"authentik_providers_ws_federation.wsfederationprovider",
|
"authentik_providers_ws_federation.wsfederationprovider",
|
||||||
AuthentikReportsDataexport: "authentik_reports.dataexport",
|
AuthentikReportsDataexport: "authentik_reports.dataexport",
|
||||||
|
AuthentikStagesAccountLockdownAccountlockdownstage:
|
||||||
|
"authentik_stages_account_lockdown.accountlockdownstage",
|
||||||
AuthentikStagesAuthenticatorEndpointGdtcAuthenticatorendpointgdtcstage:
|
AuthentikStagesAuthenticatorEndpointGdtcAuthenticatorendpointgdtcstage:
|
||||||
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
||||||
AuthentikStagesMtlsMutualtlsstage: "authentik_stages_mtls.mutualtlsstage",
|
AuthentikStagesMtlsMutualtlsstage: "authentik_stages_mtls.mutualtlsstage",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user