Compare commits
52 Commits
web/maint/
...
rust-proxy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d141dd331 | ||
|
|
6848eb744b | ||
|
|
2aab203559 | ||
|
|
780b500402 | ||
|
|
f63328aa1f | ||
|
|
986656407f | ||
|
|
4eeaebfcdb | ||
|
|
1914c2d4de | ||
|
|
49f609e5f8 | ||
|
|
7f2a3a1524 | ||
|
|
e574c6439f | ||
|
|
e0316ff2e8 | ||
|
|
2c3d11a4c3 | ||
|
|
a3c50ae92a | ||
|
|
3ef36b9e9e | ||
|
|
691e173cad | ||
|
|
68a6b04749 | ||
|
|
046dbdabe2 | ||
|
|
aae1b32c61 | ||
|
|
87a95eddea | ||
|
|
71025a83ad | ||
|
|
00f0cfe6e4 | ||
|
|
b19f43c8e1 | ||
|
|
5053167a05 | ||
|
|
f4e868210d | ||
|
|
ee954d64f8 | ||
|
|
69facf209f | ||
|
|
561cd8c97b | ||
|
|
d14afe242d | ||
|
|
349a97b1df | ||
|
|
31d8ddc887 | ||
|
|
78f5d85a8b | ||
|
|
c2636d72a4 | ||
|
|
f4d6ebf024 | ||
|
|
75a62b7dca | ||
|
|
9581b90961 | ||
|
|
7dbc01c051 | ||
|
|
e188ddc2ab | ||
|
|
ae073544fe | ||
|
|
a4e0ae9ecd | ||
|
|
086510230d | ||
|
|
8d32228c90 | ||
|
|
1295e2d595 | ||
|
|
008c9fb723 | ||
|
|
9be1b618a5 | ||
|
|
2afe5b5a7b | ||
|
|
af4ccba51e | ||
|
|
d09260f64f | ||
|
|
923c1f465a | ||
|
|
e5208185f9 | ||
|
|
b5deeaa822 | ||
|
|
cceb952429 |
2
.github/actions/setup/action.yml
vendored
@@ -64,7 +64,7 @@ runs:
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2
|
||||
uses: taiki-e/install-action@ec28e287910af896fd98e04056d31fa68607e7ad # v2
|
||||
with:
|
||||
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
||||
- name: Setup node (web)
|
||||
|
||||
6
.github/workflows/qa-codeql.yml
vendored
@@ -28,10 +28,10 @@ jobs:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4.35.3
|
||||
uses: github/codeql-action/init@v4.35.4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4.35.3
|
||||
uses: github/codeql-action/autobuild@v4.35.4
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4.35.3
|
||||
uses: github/codeql-action/analyze@v4.35.4
|
||||
|
||||
20
.npmrc
Normal file
@@ -0,0 +1,20 @@
|
||||
# Block lifecycle scripts (preinstall/install/postinstall/prepare) from dependencies.
|
||||
# This neutralizes the dominant npm supply-chain attack vector.
|
||||
#
|
||||
# Packages that legitimately need a build step (e.g. esbuild, chromedriver, tree-sitter)
|
||||
# must be rebuilt explicitly:
|
||||
#
|
||||
# npm rebuild --foreground-scripts esbuild chromedriver tree-sitter tree-sitter-json
|
||||
ignore-scripts=true
|
||||
|
||||
# Fail fast if the active Node/npm doesn't match the "engines" field.
|
||||
engine-strict=true
|
||||
|
||||
# Pin exact versions so `npm install <pkg>` writes "1.2.3" not "^1.2.3".
|
||||
save-exact=true
|
||||
|
||||
# Surface CVE warnings during install; doesn't block.
|
||||
audit=true
|
||||
|
||||
# Suppress funding banners.
|
||||
fund=false
|
||||
@@ -34,6 +34,7 @@ packages/django-channels-postgres @goauthentik/backend
|
||||
packages/django-postgres-cache @goauthentik/backend
|
||||
packages/django-dramatiq-postgres @goauthentik/backend
|
||||
# Web packages
|
||||
.npmrc @goauthentik/frontend
|
||||
tsconfig.json @goauthentik/frontend
|
||||
package.json @goauthentik/frontend
|
||||
package-lock.json @goauthentik/frontend
|
||||
|
||||
189
Cargo.lock
generated
@@ -143,6 +143,45 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
|
||||
dependencies = [
|
||||
"asn1-rs-derive",
|
||||
"asn1-rs-impl",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-derive"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-impl"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
@@ -176,10 +215,13 @@ dependencies = [
|
||||
"arc-swap",
|
||||
"argh",
|
||||
"authentik-axum",
|
||||
"authentik-client",
|
||||
"authentik-common",
|
||||
"axum",
|
||||
"axum-server",
|
||||
"color-eyre",
|
||||
"eyre",
|
||||
"futures",
|
||||
"hyper-unix-socket",
|
||||
"hyper-util",
|
||||
"metrics",
|
||||
@@ -187,9 +229,18 @@ dependencies = [
|
||||
"nix 0.31.2",
|
||||
"pyo3",
|
||||
"pyo3-build-config",
|
||||
"rand 0.10.1",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"sqlx",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tower",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"which",
|
||||
]
|
||||
@@ -241,12 +292,14 @@ dependencies = [
|
||||
"config",
|
||||
"console-subscriber",
|
||||
"eyre",
|
||||
"futures",
|
||||
"glob",
|
||||
"ipnet",
|
||||
"json-subscriber",
|
||||
"nix 0.31.2",
|
||||
"notify",
|
||||
"pin-project-lite",
|
||||
"rcgen",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"rustls",
|
||||
@@ -264,6 +317,7 @@ dependencies = [
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -542,6 +596,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
@@ -779,6 +844,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.4.0"
|
||||
@@ -873,6 +947,20 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "10.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
@@ -1291,6 +1379,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
@@ -2172,6 +2261,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.6"
|
||||
@@ -2401,6 +2500,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
@@ -2802,6 +2910,17 @@ dependencies = [
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.2",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
@@ -2840,6 +2959,12 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_xoshiro"
|
||||
version = "0.7.0"
|
||||
@@ -2867,6 +2992,19 @@ dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"rustls-pki-types",
|
||||
"time",
|
||||
"x509-parser",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -3030,6 +3168,15 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusticata-macros"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
@@ -3409,7 +3556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -3420,7 +3567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -3924,9 +4071,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.2"
|
||||
version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -3990,8 +4137,12 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tungstenite",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4205,8 +4356,11 @@ dependencies = [
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.4",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5077,6 +5231,24 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"aws-lc-rs",
|
||||
"data-encoding",
|
||||
"der-parser",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"oid-registry",
|
||||
"rusticata-macros",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust2"
|
||||
version = "0.10.4"
|
||||
@@ -5088,6 +5260,15 @@ dependencies = [
|
||||
"hashlink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
|
||||
27
Cargo.toml
@@ -50,6 +50,11 @@ notify = "= 8.2.0"
|
||||
pin-project-lite = "= 0.2.17"
|
||||
pyo3 = "= 0.28.3"
|
||||
pyo3-build-config = "= 0.28.3"
|
||||
rand = "= 0.10.1"
|
||||
rcgen = { version = "= 0.14.7", default-features = false, features = [
|
||||
"aws_lc_rs",
|
||||
"fips",
|
||||
] }
|
||||
regex = "= 1.12.3"
|
||||
reqwest = { version = "= 0.13.3", features = [
|
||||
"form",
|
||||
@@ -97,9 +102,13 @@ sqlx = { version = "= 0.8.6", default-features = false, features = [
|
||||
tempfile = "= 3.27.0"
|
||||
thiserror = "= 2.0.18"
|
||||
time = { version = "= 0.3.47", features = ["macros"] }
|
||||
tokio = { version = "= 1.52.2", features = ["full", "tracing"] }
|
||||
tokio = { version = "= 1.52.3", features = ["full", "tracing"] }
|
||||
tokio-retry2 = "= 0.9.1"
|
||||
tokio-rustls = "= 0.26.4"
|
||||
tokio-tungstenite = { version = "= 0.29.0", features = [
|
||||
"rustls-tls-webpki-roots",
|
||||
"url",
|
||||
] }
|
||||
tokio-util = { version = "= 0.7.18", features = ["full"] }
|
||||
tower = "= 0.5.3"
|
||||
tower-http = { version = "= 0.6.10", features = ["timeout"] }
|
||||
@@ -260,28 +269,40 @@ publish.workspace = true
|
||||
[features]
|
||||
default = ["core", "proxy"]
|
||||
core = ["ak-common/core", "dep:pyo3", "dep:sqlx"]
|
||||
proxy = ["ak-common/proxy"]
|
||||
proxy = ["ak-common/proxy", "dep:ak-client"]
|
||||
|
||||
[build-dependencies]
|
||||
pyo3-build-config.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ak-axum.workspace = true
|
||||
ak-client = { workspace = true, optional = true }
|
||||
ak-common.workspace = true
|
||||
arc-swap.workspace = true
|
||||
argh.workspace = true
|
||||
axum-server.workspace = true
|
||||
axum.workspace = true
|
||||
color-eyre.workspace = true
|
||||
eyre.workspace = true
|
||||
futures.workspace = true
|
||||
hyper-unix-socket.workspace = true
|
||||
hyper-util.workspace = true
|
||||
metrics.workspace = true
|
||||
metrics-exporter-prometheus.workspace = true
|
||||
metrics.workspace = true
|
||||
nix.workspace = true
|
||||
pyo3 = { workspace = true, optional = true }
|
||||
rand.workspace = true
|
||||
rustls.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_repr.workspace = true
|
||||
sqlx = { workspace = true, optional = true }
|
||||
time.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
tokio.workspace = true
|
||||
tower.workspace = true
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
which.workspace = true
|
||||
|
||||
|
||||
18
Makefile
@@ -125,7 +125,7 @@ core-i18n-extract:
|
||||
--ignore website \
|
||||
-l en
|
||||
|
||||
install: node-install docs-install core-install ## Install all requires dependencies for `node`, `docs` and `core`
|
||||
install: node-install web-install core-install ## Install all requires dependencies for `node`, `web` and `core`
|
||||
|
||||
dev-drop-db:
|
||||
$(eval pg_user := $(shell $(UV) run python -m authentik.lib.config postgresql.user 2>/dev/null))
|
||||
@@ -228,14 +228,26 @@ gen-dev-config: ## Generate a local development config file
|
||||
## Node.js
|
||||
#########################
|
||||
|
||||
# Packages whose install/postinstall scripts are required for correct
|
||||
# operation (binary downloads, native bindings). The root .npmrc sets
|
||||
# `ignore-scripts=true` to block dependency lifecycle scripts by default;
|
||||
# this list is rebuilt explicitly with scripts re-enabled. Audit any
|
||||
# additions: each entry runs arbitrary code at install time.
|
||||
TRUSTED_INSTALL_SCRIPTS := esbuild chromedriver tree-sitter tree-sitter-json
|
||||
|
||||
node-install: ## Install the necessary libraries to build Node.js packages
|
||||
npm ci
|
||||
npm ci --prefix web
|
||||
|
||||
#########################
|
||||
## Web
|
||||
#########################
|
||||
|
||||
web-install: ## Install the necessary libraries to build the Authentik UI
|
||||
npm ci --prefix web
|
||||
|
||||
web-postinstall: ## Trigger postinstall scripts for packages with native bindings or binary downloads, which are blocked by default for security reasons.
|
||||
npm rebuild --prefix web --ignore-scripts=false --foreground-scripts $(TRUSTED_INSTALL_SCRIPTS)
|
||||
|
||||
web-build: node-install ## Build the Authentik UI
|
||||
npm run --prefix web build
|
||||
|
||||
@@ -268,7 +280,7 @@ web-i18n-extract:
|
||||
|
||||
docs: docs-lint-fix docs-build ## Automatically fix formatting issues in the Authentik docs source code, lint the code, and compile it
|
||||
|
||||
docs-install:
|
||||
docs-install: node-install
|
||||
npm ci --prefix website
|
||||
|
||||
docs-lint-fix: lint-spellcheck
|
||||
|
||||
@@ -42,11 +42,29 @@ def validate_auth(header: bytes, format="bearer") -> str | None:
|
||||
return auth_credentials
|
||||
|
||||
|
||||
class IPCUser(AnonymousUser):
|
||||
class VirtualUser(AnonymousUser):
|
||||
is_active = True
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
def all_roles(self):
|
||||
return []
|
||||
|
||||
|
||||
class IPCUser(VirtualUser):
|
||||
"""'Virtual' user for IPC communication between authentik core and the authentik router"""
|
||||
|
||||
username = "authentik:system"
|
||||
is_active = True
|
||||
is_superuser = True
|
||||
|
||||
@property
|
||||
@@ -62,17 +80,6 @@ class IPCUser(AnonymousUser):
|
||||
def has_module_perms(self, module):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
def all_roles(self):
|
||||
return []
|
||||
|
||||
|
||||
class TokenAuthentication(BaseAuthentication):
|
||||
"""Token-based authentication using HTTP Bearer authentication"""
|
||||
|
||||
@@ -246,6 +246,25 @@ class GroupSerializer(ModelSerializer):
|
||||
)
|
||||
return superuser
|
||||
|
||||
def validate_users(self, users: list) -> list:
|
||||
"""Require add_user_to_group permission when adding new members via group PATCH."""
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return users
|
||||
if not self.instance:
|
||||
return users
|
||||
# BulkManyRelatedField returns raw PKs, not model instances
|
||||
current_user_pks = set(self.instance.users.values_list("pk", flat=True))
|
||||
new_users = [u for u in users if u not in current_user_pks]
|
||||
if not new_users:
|
||||
return users
|
||||
has_perm = request.user.has_perm(
|
||||
"authentik_core.add_user_to_group"
|
||||
) or request.user.has_perm("authentik_core.add_user_to_group", self.instance)
|
||||
if not has_perm:
|
||||
raise ValidationError(_("User does not have permission to add members to this group."))
|
||||
return users
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = [
|
||||
|
||||
@@ -297,6 +297,36 @@ class UserSerializer(ModelSerializer):
|
||||
raise ValidationError(_("Setting a user to internal service account is not allowed."))
|
||||
return user_type
|
||||
|
||||
def validate_groups(self, groups: list) -> list:
|
||||
"""Require enable_group_superuser permission when adding a user to a superuser group."""
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return groups
|
||||
current_groups = set(self.instance.groups.all()) if self.instance else set()
|
||||
for group in groups:
|
||||
if not group.is_superuser:
|
||||
continue
|
||||
if group in current_groups:
|
||||
continue
|
||||
if not request.user.has_perm("authentik_core.enable_group_superuser"):
|
||||
raise ValidationError(
|
||||
_("User does not have permission to add members to a superuser group.")
|
||||
)
|
||||
return groups
|
||||
|
||||
def validate_roles(self, roles: list) -> list:
|
||||
"""Require change_role permission when assigning new roles to a user."""
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return roles
|
||||
current_roles = set(self.instance.roles.all()) if self.instance else set()
|
||||
new_roles = [r for r in roles if r not in current_roles]
|
||||
if not new_roles:
|
||||
return roles
|
||||
if not request.user.has_perm("authentik_rbac.change_role"):
|
||||
raise ValidationError(_("User does not have permission to assign roles."))
|
||||
return roles
|
||||
|
||||
def validate(self, attrs: dict) -> dict:
|
||||
if self.instance and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
|
||||
raise ValidationError(_("Can't modify internal service account users"))
|
||||
|
||||
@@ -158,3 +158,58 @@ class TestGroupsAPI(APITestCase):
|
||||
data={"name": generate_id(), "is_superuser": True},
|
||||
)
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_patch_users_no_perm(self):
|
||||
"""PATCH group with new users without add_user_to_group must be rejected."""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.view_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.change_group", group)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
||||
data={"users": [self.user.pk]},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
def test_patch_users_with_global_perm(self):
|
||||
"""PATCH group with new users with global add_user_to_group must succeed."""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.view_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.change_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.add_user_to_group")
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
||||
data={"users": [self.user.pk]},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_patch_users_with_obj_perm(self):
|
||||
"""PATCH group with new users with object-level add_user_to_group must succeed."""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.view_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.change_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.add_user_to_group", group)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
||||
data={"users": [self.user.pk]},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_patch_existing_users_no_perm(self):
|
||||
"""PATCH group keeping existing membership without add_user_to_group must succeed."""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
group.users.add(self.user)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.view_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.change_group", group)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
||||
data={"users": [self.user.pk]},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
@@ -12,6 +12,7 @@ from authentik.brands.models import Brand
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
AuthenticatedSession,
|
||||
Group,
|
||||
Session,
|
||||
Token,
|
||||
User,
|
||||
@@ -25,6 +26,7 @@ from authentik.core.tests.utils import (
|
||||
)
|
||||
from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.rbac.models import Role
|
||||
from authentik.stages.email.models import EmailStage
|
||||
|
||||
INVALID_PASSWORD_HASH = "not-a-valid-hash"
|
||||
@@ -939,3 +941,79 @@ class TestUsersAPI(APITestCase):
|
||||
self.assertIn(user2.pk, pks)
|
||||
# Verify user2 comes before user1 in descending order
|
||||
self.assertLess(pks.index(user2.pk), pks.index(user1.pk))
|
||||
|
||||
|
||||
class TestUsersAPIGroupRoleValidation(APITestCase):
|
||||
"""Test that PATCH /api/v3/core/users/{pk}/ enforces group and role permission checks."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.actor = create_test_user()
|
||||
self.target = create_test_user()
|
||||
|
||||
def _patch(self, data: dict):
|
||||
self.client.force_login(self.actor)
|
||||
return self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": self.target.pk}),
|
||||
data=data,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
def test_patch_superuser_group_no_perm(self):
|
||||
"""Assigning a superuser group without enable_group_superuser must be rejected."""
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
|
||||
group = Group.objects.create(name=generate_id(), is_superuser=True)
|
||||
res = self._patch({"groups": [str(group.pk)]})
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
def test_patch_superuser_group_with_perm(self):
|
||||
"""Assigning a superuser group with enable_group_superuser must succeed."""
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.enable_group_superuser")
|
||||
group = Group.objects.create(name=generate_id(), is_superuser=True)
|
||||
res = self._patch({"groups": [str(group.pk)]})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_patch_non_superuser_group_no_perm(self):
|
||||
"""Assigning a non-superuser group without special permission must succeed."""
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
|
||||
group = Group.objects.create(name=generate_id(), is_superuser=False)
|
||||
res = self._patch({"groups": [str(group.pk)]})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_patch_existing_superuser_group_no_perm(self):
|
||||
"""Keeping an existing superuser group membership without the permission must succeed."""
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
|
||||
group = Group.objects.create(name=generate_id(), is_superuser=True)
|
||||
self.target.groups.add(group)
|
||||
res = self._patch({"groups": [str(group.pk)]})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_patch_role_no_perm(self):
|
||||
"""Assigning a new role without change_role must be rejected."""
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
|
||||
role = Role.objects.create(name=generate_id())
|
||||
res = self._patch({"roles": [str(role.pk)]})
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
def test_patch_role_with_perm(self):
|
||||
"""Assigning a new role with change_role must succeed."""
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
|
||||
self.actor.assign_perms_to_managed_role("authentik_rbac.change_role")
|
||||
role = Role.objects.create(name=generate_id())
|
||||
res = self._patch({"roles": [str(role.pk)]})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_patch_existing_role_no_perm(self):
|
||||
"""Keeping an existing role without change_role must succeed."""
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
|
||||
role = Role.objects.create(name=generate_id())
|
||||
self.target.roles.add(role)
|
||||
res = self._patch({"roles": [str(role.pk)]})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
@@ -7,7 +7,7 @@ from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_sche
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.fields import ChoiceField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.relations import PrimaryKeyRelatedField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@@ -44,7 +44,6 @@ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_ME
|
||||
|
||||
|
||||
class AgentConnectorSerializer(ConnectorSerializer):
|
||||
|
||||
class Meta(ConnectorSerializer.Meta):
|
||||
model = AgentConnector
|
||||
fields = ConnectorSerializer.Meta.fields + [
|
||||
@@ -63,7 +62,6 @@ class AgentConnectorSerializer(ConnectorSerializer):
|
||||
|
||||
|
||||
class MDMConfigSerializer(PassiveSerializer):
|
||||
|
||||
platform = ChoiceField(choices=OSFamily.choices)
|
||||
enrollment_token = PrimaryKeyRelatedField(
|
||||
queryset=EnrollmentToken.objects.including_expired().all()
|
||||
@@ -89,7 +87,6 @@ class AgentConnectorViewSet(
|
||||
UsedByMixin,
|
||||
ModelViewSet,
|
||||
):
|
||||
|
||||
queryset = AgentConnector.objects.all()
|
||||
serializer_class = AgentConnectorSerializer
|
||||
search_fields = ["name"]
|
||||
@@ -121,6 +118,8 @@ class AgentConnectorViewSet(
|
||||
methods=["POST"],
|
||||
detail=False,
|
||||
authentication_classes=[AgentEnrollmentAuth],
|
||||
# Permissions are handled via AgentEnrollmentAuth
|
||||
permission_classes=[AllowAny],
|
||||
)
|
||||
def enroll(self, request: Request):
|
||||
token: EnrollmentToken = request.auth
|
||||
@@ -151,7 +150,13 @@ class AgentConnectorViewSet(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses=AgentConfigSerializer(),
|
||||
)
|
||||
@action(methods=["GET"], detail=False, authentication_classes=[AgentAuth])
|
||||
@action(
|
||||
methods=["GET"],
|
||||
detail=False,
|
||||
authentication_classes=[AgentAuth],
|
||||
# Permissions are handled via AgentAuth
|
||||
permission_classes=[AllowAny],
|
||||
)
|
||||
def agent_config(self, request: Request):
|
||||
token: DeviceToken = request.auth
|
||||
connector: AgentConnector = token.device.connector.agentconnector
|
||||
@@ -165,7 +170,13 @@ class AgentConnectorViewSet(
|
||||
request=DeviceFacts(),
|
||||
responses={204: OpenApiResponse(description="Successfully checked in")},
|
||||
)
|
||||
@action(methods=["POST"], detail=False, authentication_classes=[AgentAuth])
|
||||
@action(
|
||||
methods=["POST"],
|
||||
detail=False,
|
||||
authentication_classes=[AgentAuth],
|
||||
# Permissions are handled via AgentAuth
|
||||
permission_classes=[AllowAny],
|
||||
)
|
||||
def check_in(self, request: Request):
|
||||
token: DeviceToken = request.auth
|
||||
data = DeviceFacts(data=request.data)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest
|
||||
from django.utils.timezone import now
|
||||
from drf_spectacular.extensions import OpenApiAuthenticationExtension
|
||||
@@ -9,7 +10,7 @@ from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.request import Request
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.authentication import IPCUser, validate_auth
|
||||
from authentik.api.authentication import VirtualUser, validate_auth
|
||||
from authentik.core.middleware import CTX_AUTH_VIA
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.apps import MANAGED_KEY
|
||||
@@ -25,9 +26,18 @@ LOGGER = get_logger()
|
||||
PLATFORM_ISSUER = "goauthentik.io/platform"
|
||||
|
||||
|
||||
class DeviceUser(IPCUser):
|
||||
class DeviceUser(VirtualUser):
|
||||
|
||||
username = "authentik:endpoints:device"
|
||||
|
||||
def has_perm(self, perm: str, obj: Model | None = None) -> bool:
|
||||
if perm in [
|
||||
"authentik_core.view_user",
|
||||
"authentik_core.view_group",
|
||||
]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class AgentEnrollmentAuth(BaseAuthentication):
|
||||
|
||||
|
||||
@@ -223,3 +223,17 @@ class TestAgentAPI(APITestCase):
|
||||
data={"platform": OSFamily.macOS, "enrollment_token": self.token.pk},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_users_list(self):
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_other_api_forbidden(self):
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:application-list"),
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.urls import reverse
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from structlog.stdlib import get_logger
|
||||
@@ -25,7 +26,13 @@ class AgentConnectorViewSetMixin:
|
||||
request=OpenApiTypes.NONE,
|
||||
responses=AgentAuthenticationResponse(),
|
||||
)
|
||||
@action(methods=["POST"], detail=False, authentication_classes=[AgentAuth])
|
||||
@action(
|
||||
methods=["POST"],
|
||||
detail=False,
|
||||
authentication_classes=[AgentAuth],
|
||||
# Permissions are handled via AgentAuth
|
||||
permission_classes=[AllowAny],
|
||||
)
|
||||
@enterprise_action
|
||||
def auth_ia(self, request: Request) -> Response:
|
||||
token: DeviceToken = request.auth
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
@@ -55,7 +56,9 @@ class SignInRequest:
|
||||
_, provider = req.get_app_provider()
|
||||
if not req.wreply:
|
||||
req.wreply = provider.acs_url
|
||||
if not req.wreply.startswith(provider.acs_url):
|
||||
reply = urlparse(req.wreply)
|
||||
configured = urlparse(provider.acs_url)
|
||||
if not (reply[:2] == configured[:2] and reply.path.startswith(configured.path)):
|
||||
raise ValueError("Invalid wreply")
|
||||
return req
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
@@ -32,7 +33,9 @@ class SignOutRequest:
|
||||
_, provider = req.get_app_provider()
|
||||
if not req.wreply:
|
||||
req.wreply = provider.acs_url
|
||||
if not req.wreply.startswith(provider.acs_url):
|
||||
reply = urlparse(req.wreply)
|
||||
configured = urlparse(provider.acs_url)
|
||||
if not (reply[:2] == configured[:2] and reply.path.startswith(configured.path)):
|
||||
raise ValueError("Invalid wreply")
|
||||
return req
|
||||
|
||||
|
||||
@@ -27,12 +27,27 @@ class TestWSFedSignIn(TestCase):
|
||||
name=generate_id(),
|
||||
authorization_flow=self.flow,
|
||||
signing_kp=self.cert,
|
||||
acs_url="https://t.goauthentik.io",
|
||||
audience="foo",
|
||||
)
|
||||
self.app = Application.objects.create(
|
||||
name=generate_id(), slug=generate_id(), provider=self.provider
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_wreply(self):
|
||||
request = self.factory.get(
|
||||
"/?wreply=https://t.goauthentik.io/foo&wa=wsignin1.0&wtrealm=foo",
|
||||
user=get_anonymous_user(),
|
||||
)
|
||||
SignInRequest.parse(request)
|
||||
with self.assertRaises(ValueError):
|
||||
request = self.factory.get(
|
||||
"/?wreply=https://t.goauthentik.io.invalid.com&wa=wsignin1.0&wtrealm=foo",
|
||||
user=get_anonymous_user(),
|
||||
)
|
||||
SignInRequest.parse(request)
|
||||
|
||||
def test_token_gen(self):
|
||||
request = self.factory.get("/", user=get_anonymous_user())
|
||||
proc = SignInProcessor(
|
||||
|
||||
@@ -9,10 +9,10 @@ from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
|
||||
from authentik.providers.oauth2.api.providers import OAuth2ProviderSerializer
|
||||
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class ExpiringBaseGrantModelSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Serializer for BaseGrantModel and ExpiringBaseGrant"""
|
||||
|
||||
user = UserSerializer()
|
||||
provider = OAuth2ProviderSerializer()
|
||||
provider = ProviderSerializer()
|
||||
scope = ListField(child=CharField())
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""authentik saml source processor"""
|
||||
|
||||
from base64 import b64decode
|
||||
from datetime import UTC, datetime
|
||||
from time import mktime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -40,6 +41,7 @@ from authentik.sources.saml.exceptions import (
|
||||
InvalidSignature,
|
||||
MismatchedRequestID,
|
||||
MissingSAMLResponse,
|
||||
SAMLException,
|
||||
UnsupportedNameIDFormat,
|
||||
)
|
||||
from authentik.sources.saml.models import (
|
||||
@@ -95,6 +97,7 @@ class ResponseProcessor:
|
||||
|
||||
self._verify_request_id()
|
||||
self._verify_status()
|
||||
self._verify_conditions()
|
||||
|
||||
def _decrypt_response(self):
|
||||
"""Decrypt SAMLResponse EncryptedAssertion Element"""
|
||||
@@ -126,6 +129,20 @@ class ResponseProcessor:
|
||||
)
|
||||
self._assertion = decrypted_assertion
|
||||
|
||||
def _verify_conditions(self):
|
||||
conditions = self.get_assertion().find(f"{{{NS_SAML_ASSERTION}}}Conditions")
|
||||
if conditions is None:
|
||||
return
|
||||
_now = now()
|
||||
before = conditions.attrib.get("NotBefore")
|
||||
if before:
|
||||
if datetime.fromisoformat(before).replace(tzinfo=UTC) > _now:
|
||||
raise SAMLException("Assertion is not valid yet or expired.")
|
||||
on_or_after = conditions.attrib.get("NotOnOrAfter")
|
||||
if on_or_after:
|
||||
if datetime.fromisoformat(on_or_after).replace(tzinfo=UTC) < _now:
|
||||
raise SAMLException("Assertion is not valid yet or expired.")
|
||||
|
||||
def _verify_signature(self, signature_node: _Element):
|
||||
"""Verify a single signature node"""
|
||||
xmlsec.tree.add_ids(self._root, ["ID"])
|
||||
@@ -215,10 +232,9 @@ class ResponseProcessor:
|
||||
user has an attribute that refers to our Source for cleanup. The user is also deleted
|
||||
on logout and periodically."""
|
||||
# Create a temporary User
|
||||
name_id = self._get_name_id()
|
||||
username = name_id.text
|
||||
name_id_el, name_id = self._get_name_id()
|
||||
# trim username to ensure it is max 150 chars
|
||||
username = f"ak-{username[: USERNAME_MAX_LENGTH - 14]}-transient"
|
||||
username = f"ak-{name_id[: USERNAME_MAX_LENGTH - 14]}-transient"
|
||||
expiry = mktime(
|
||||
(now() + timedelta_from_string(self._source.temporary_user_delete_after)).timetuple()
|
||||
)
|
||||
@@ -234,20 +250,18 @@ class ResponseProcessor:
|
||||
},
|
||||
path=self._source.get_user_path(),
|
||||
)
|
||||
LOGGER.debug("Created temporary user for NameID Transient", username=name_id.text)
|
||||
LOGGER.debug("Created temporary user for NameID Transient", username=name_id)
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
UserSAMLSourceConnection.objects.create(
|
||||
source=self._source, user=user, identifier=name_id.text
|
||||
)
|
||||
UserSAMLSourceConnection.objects.create(source=self._source, user=user, identifier=name_id)
|
||||
return SAMLSourceFlowManager(
|
||||
source=self._source,
|
||||
request=self._http_request,
|
||||
identifier=str(name_id.text),
|
||||
identifier=str(name_id),
|
||||
user_info={
|
||||
"root": self._root,
|
||||
"assertion": self.get_assertion(),
|
||||
"name_id": name_id,
|
||||
"name_id": name_id_el,
|
||||
},
|
||||
policy_context={},
|
||||
)
|
||||
@@ -258,7 +272,7 @@ class ResponseProcessor:
|
||||
return self._assertion
|
||||
return self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
|
||||
|
||||
def _get_name_id(self) -> Element:
|
||||
def _get_name_id(self) -> tuple[Element, str]:
|
||||
"""Get NameID Element"""
|
||||
assertion = self.get_assertion()
|
||||
if assertion is None:
|
||||
@@ -269,12 +283,11 @@ class ResponseProcessor:
|
||||
name_id = subject.find(f"{{{NS_SAML_ASSERTION}}}NameID")
|
||||
if name_id is None:
|
||||
raise ValueError("NameID element not found")
|
||||
return name_id
|
||||
return name_id, "".join(name_id.itertext())
|
||||
|
||||
def _get_name_id_filter(self) -> dict[str, str]:
|
||||
"""Returns the subject's NameID as a Filter for the `User`"""
|
||||
name_id_el = self._get_name_id()
|
||||
name_id = name_id_el.text
|
||||
name_id_el, name_id = self._get_name_id()
|
||||
if not name_id:
|
||||
raise UnsupportedNameIDFormat("Subject's NameID is empty.")
|
||||
_format = name_id_el.attrib["Format"]
|
||||
@@ -295,26 +308,26 @@ class ResponseProcessor:
|
||||
|
||||
def prepare_flow_manager(self) -> SourceFlowManager:
|
||||
"""Prepare flow plan depending on whether or not the user exists"""
|
||||
name_id = self._get_name_id()
|
||||
name_id_el, name_id = self._get_name_id()
|
||||
# Sanity check, show a warning if NameIDPolicy doesn't match what we go
|
||||
if self._source.name_id_policy != name_id.attrib["Format"]:
|
||||
if self._source.name_id_policy != name_id_el.attrib["Format"]:
|
||||
LOGGER.warning(
|
||||
"NameID from IdP doesn't match our policy",
|
||||
expected=self._source.name_id_policy,
|
||||
got=name_id.attrib["Format"],
|
||||
got=name_id_el.attrib["Format"],
|
||||
)
|
||||
# transient NameIDs are handled separately as they don't have to go through flows.
|
||||
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
|
||||
if name_id_el.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
|
||||
return self._handle_name_id_transient()
|
||||
|
||||
return SAMLSourceFlowManager(
|
||||
source=self._source,
|
||||
request=self._http_request,
|
||||
identifier=str(name_id.text),
|
||||
identifier=str(name_id),
|
||||
user_info={
|
||||
"root": self._root,
|
||||
"assertion": self.get_assertion(),
|
||||
"name_id": name_id,
|
||||
"name_id": name_id_el,
|
||||
},
|
||||
policy_context={
|
||||
"saml_response": etree.tostring(self._root),
|
||||
|
||||
@@ -4,6 +4,7 @@ from base64 import b64encode
|
||||
|
||||
from defusedxml.lxml import fromstring
|
||||
from django.test import TestCase
|
||||
from freezegun import freeze_time
|
||||
|
||||
from authentik.common.saml.constants import NS_SAML_ASSERTION
|
||||
from authentik.core.tests.utils import RequestFactory, create_test_flow
|
||||
@@ -34,6 +35,7 @@ class TestPropertyMappings(TestCase):
|
||||
pre_authentication_flow=create_test_flow(),
|
||||
)
|
||||
|
||||
@freeze_time("2022-10-14T14:15:00")
|
||||
def test_user_base_properties(self):
|
||||
"""Test user base properties"""
|
||||
properties = self.source.get_base_user_properties(
|
||||
@@ -61,6 +63,7 @@ class TestPropertyMappings(TestCase):
|
||||
properties = self.source.get_base_group_properties(root=ROOT, group_id=group_id)
|
||||
self.assertEqual(properties, {"name": group_id})
|
||||
|
||||
@freeze_time("2022-10-14T14:15:00")
|
||||
def test_user_property_mappings(self):
|
||||
"""Test user property mappings"""
|
||||
self.source.user_property_mappings.add(
|
||||
@@ -94,6 +97,7 @@ class TestPropertyMappings(TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
@freeze_time("2022-10-14T14:15:00")
|
||||
def test_group_property_mappings(self):
|
||||
"""Test group property mappings"""
|
||||
self.source.group_property_mappings.add(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from base64 import b64encode
|
||||
|
||||
from django.test import TestCase
|
||||
from freezegun import freeze_time
|
||||
|
||||
from authentik.core.tests.utils import RequestFactory, create_test_cert, create_test_flow
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
@@ -46,6 +47,7 @@ class TestResponseProcessor(TestCase):
|
||||
):
|
||||
ResponseProcessor(self.source, request).parse()
|
||||
|
||||
@freeze_time("2022-10-14T14:15:00")
|
||||
def test_success(self):
|
||||
"""Test success"""
|
||||
request = self.factory.post(
|
||||
@@ -72,6 +74,7 @@ class TestResponseProcessor(TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
@freeze_time("2022-10-14T14:16:40Z")
|
||||
def test_success_with_status_message_and_detail(self):
|
||||
"""Test success with StatusMessage and StatusDetail present (should not raise error)"""
|
||||
request = self.factory.post(
|
||||
@@ -88,6 +91,7 @@ class TestResponseProcessor(TestCase):
|
||||
sfm = parser.prepare_flow_manager()
|
||||
self.assertEqual(sfm.user_properties["username"], "jens@goauthentik.io")
|
||||
|
||||
@freeze_time("2022-10-14T14:16:40Z")
|
||||
def test_error_with_message_and_detail(self):
|
||||
"""Test error status with StatusMessage and StatusDetail includes both in error"""
|
||||
request = self.factory.post(
|
||||
@@ -105,6 +109,7 @@ class TestResponseProcessor(TestCase):
|
||||
self.assertIn("User account is disabled", str(ctx.exception))
|
||||
self.assertIn("Authentication failed", str(ctx.exception))
|
||||
|
||||
@freeze_time("2024-08-07T15:48:09.325Z")
|
||||
def test_encrypted_correct(self):
|
||||
"""Test encrypted"""
|
||||
key = load_fixture("fixtures/encrypted-key.pem")
|
||||
@@ -142,6 +147,7 @@ class TestResponseProcessor(TestCase):
|
||||
with self.assertRaises(InvalidEncryption):
|
||||
parser.parse()
|
||||
|
||||
@freeze_time("2022-10-14T14:16:40Z")
|
||||
def test_verification_assertion(self):
|
||||
"""Test verifying signature inside assertion"""
|
||||
key = load_fixture("fixtures/signature_cert.pem")
|
||||
@@ -164,6 +170,7 @@ class TestResponseProcessor(TestCase):
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
|
||||
@freeze_time("2014-07-17T01:02:18Z")
|
||||
def test_verification_assertion_duplicate(self):
|
||||
"""Test verifying signature inside assertion, where the response has another assertion
|
||||
before our signed assertion"""
|
||||
@@ -186,9 +193,35 @@ class TestResponseProcessor(TestCase):
|
||||
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
self.assertNotEqual(parser._get_name_id().text, "bad")
|
||||
self.assertEqual(parser._get_name_id().text, "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7")
|
||||
self.assertNotEqual(parser._get_name_id()[1], "bad")
|
||||
self.assertEqual(parser._get_name_id()[1], "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7")
|
||||
|
||||
@freeze_time("2022-10-14T14:15:00")
|
||||
def test_name_id_comment(self):
|
||||
"""Test comment in name ID"""
|
||||
fixture = load_fixture("fixtures/response_signed_assertion_dup.xml")
|
||||
fixture = fixture.replace(
|
||||
"_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7",
|
||||
"_ce3d2948b4cf20146dee0a0b3dd6f<!--x-->69b6cf86f62d7",
|
||||
)
|
||||
key = load_fixture("fixtures/signature_cert.pem")
|
||||
kp = CertificateKeyPair.objects.create(
|
||||
name=generate_id(),
|
||||
certificate_data=key,
|
||||
)
|
||||
self.source.verification_kp = kp
|
||||
self.source.signed_assertion = True
|
||||
self.source.signed_response = False
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
data={"SAMLResponse": b64encode(fixture.encode()).decode()},
|
||||
)
|
||||
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
self.assertEqual(parser._get_name_id()[1], "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7")
|
||||
|
||||
@freeze_time("2014-07-17T01:02:18Z")
|
||||
def test_verification_response(self):
|
||||
"""Test verifying signature inside response"""
|
||||
key = load_fixture("fixtures/signature_cert.pem")
|
||||
@@ -211,6 +244,7 @@ class TestResponseProcessor(TestCase):
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
|
||||
@freeze_time("2024-01-18T06:20:48Z")
|
||||
def test_verification_response_and_assertion(self):
|
||||
"""Test verifying signature inside response and assertion"""
|
||||
key = load_fixture("fixtures/signature_cert.pem")
|
||||
@@ -257,6 +291,7 @@ class TestResponseProcessor(TestCase):
|
||||
with self.assertRaisesMessage(InvalidSignature, ""):
|
||||
parser.parse()
|
||||
|
||||
@freeze_time("2022-10-14T14:15:00")
|
||||
def test_verification_no_signature(self):
|
||||
"""Test rejecting response without signature when signed_assertion is True"""
|
||||
key = load_fixture("fixtures/signature_cert.pem")
|
||||
@@ -303,6 +338,7 @@ class TestResponseProcessor(TestCase):
|
||||
with self.assertRaisesMessage(InvalidSignature, ""):
|
||||
parser.parse()
|
||||
|
||||
@freeze_time("2025-10-30T05:45:47.619Z")
|
||||
def test_signed_encrypted_response(self):
|
||||
"""Test signed & encrypted response"""
|
||||
verification_key = load_fixture("fixtures/signature_cert2.pem")
|
||||
@@ -330,6 +366,7 @@ class TestResponseProcessor(TestCase):
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
|
||||
@freeze_time("2026-01-21T14:23")
|
||||
def test_transient(self):
|
||||
"""Test SAML transient NameID"""
|
||||
verification_key = load_fixture("fixtures/signature_cert2.pem")
|
||||
|
||||
@@ -4,6 +4,7 @@ from base64 import b64encode
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from freezegun import freeze_time
|
||||
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.flows.planner import PLAN_CONTEXT_REDIRECT, FlowPlan
|
||||
@@ -26,6 +27,7 @@ class TestViews(TestCase):
|
||||
pre_authentication_flow=create_test_flow(),
|
||||
)
|
||||
|
||||
@freeze_time("2022-10-14T14:15:00")
|
||||
def test_enroll(self):
|
||||
"""Enroll"""
|
||||
flow = create_test_flow()
|
||||
@@ -52,6 +54,7 @@ class TestViews(TestCase):
|
||||
plan: FlowPlan = self.client.session.get(SESSION_KEY_PLAN)
|
||||
self.assertIsNotNone(plan)
|
||||
|
||||
@freeze_time("2022-10-14T14:15:00")
|
||||
def test_enroll_redirect(self):
|
||||
"""Enroll when attempting to access a provider"""
|
||||
initial_redirect = f"http://{generate_id()}"
|
||||
|
||||
@@ -36,14 +36,10 @@ entries:
|
||||
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()
|
||||
actor_uuid = str(getattr(http_request.user, "pk", ""))
|
||||
pending_user = user if getattr(user, "is_authenticated", False) else None
|
||||
target_uuid = str(getattr(pending_user, "pk", ""))
|
||||
is_self_service = not target_uuid or target_uuid == actor_uuid
|
||||
if is_self_service:
|
||||
return (
|
||||
"<p><strong>You are about to lock down your own account.</strong></p>"
|
||||
@@ -63,14 +59,15 @@ entries:
|
||||
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>"
|
||||
detail = pending_user.email or pending_user.name
|
||||
user_html = f"<code>{escape(pending_user.username)}</code>"
|
||||
if detail and detail != pending_user.username:
|
||||
user_html = f"{user_html} ({escape(detail)})"
|
||||
else:
|
||||
user_html = "<p>the account selected when this one-time lockdown link was created</p>"
|
||||
user_html = "the account selected when this one-time lockdown link was created"
|
||||
|
||||
return (
|
||||
"<p><strong>You are about to lock down the following account:</strong></p>"
|
||||
f"{user_html}"
|
||||
f"<p><strong>You are about to lock down the following account:</strong> {user_html}</p>"
|
||||
"<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>"
|
||||
@@ -99,9 +96,9 @@ entries:
|
||||
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
|
||||
actor_uuid = str(getattr(http_request.user, "pk", ""))
|
||||
target_uuid = str(getattr(user, "pk", ""))
|
||||
is_self_service = not target_uuid or target_uuid == actor_uuid
|
||||
if is_self_service:
|
||||
info = (
|
||||
"Use this if you no longer trust your current password or sessions. "
|
||||
@@ -134,9 +131,9 @@ entries:
|
||||
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
|
||||
actor_uuid = str(getattr(http_request.user, "pk", ""))
|
||||
target_uuid = str(getattr(user, "pk", ""))
|
||||
is_self_service = not target_uuid or target_uuid == actor_uuid
|
||||
if is_self_service:
|
||||
return "Describe why you are locking your account..."
|
||||
return "Describe why this account is being locked down..."
|
||||
@@ -184,14 +181,10 @@ entries:
|
||||
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>"
|
||||
if getattr(user, "is_authenticated", False):
|
||||
return f"<p><code>{escape(user.username)}</code> has been locked down.</p>"
|
||||
|
||||
return "<p>The selected account has been locked down.</p>"
|
||||
initial_value_expression: true
|
||||
@@ -221,9 +214,9 @@ entries:
|
||||
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
|
||||
actor_uuid = str(getattr(request.http_request.user, "pk", ""))
|
||||
target_uuid = str(getattr(request.user, "pk", ""))
|
||||
return bool(target_uuid) and target_uuid != actor_uuid
|
||||
identifiers:
|
||||
name: default-account-lockdown-admin-policy
|
||||
id: admin-policy
|
||||
|
||||
@@ -110,17 +110,6 @@ func (a *Application) getTraefikForwardUrl(r *http.Request) (*url.URL, error) {
|
||||
|
||||
// getNginxForwardUrl See https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/template/nginx.tmpl
|
||||
func (a *Application) getNginxForwardUrl(r *http.Request) (*url.URL, error) {
|
||||
ou := r.Header.Get("X-Original-URI")
|
||||
if ou != "" {
|
||||
// Turn this full URL into a relative URL
|
||||
u := &url.URL{
|
||||
Host: "",
|
||||
Scheme: "",
|
||||
Path: ou,
|
||||
}
|
||||
a.log.WithField("url", u.String()).Info("building forward URL from X-Original-URI")
|
||||
return u, nil
|
||||
}
|
||||
h := r.Header.Get("X-Original-URL")
|
||||
if len(h) < 1 {
|
||||
return nil, errors.New("no forward URL found")
|
||||
|
||||
@@ -5,10 +5,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
"goauthentik.io/internal/outpost/proxyv2/types"
|
||||
api "goauthentik.io/packages/client-go"
|
||||
)
|
||||
|
||||
@@ -47,67 +45,6 @@ func TestForwardHandleNginx_Single_Headers(t *testing.T) {
|
||||
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
|
||||
}
|
||||
|
||||
func TestForwardHandleNginx_Single_URI(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "https://foo.bar/outpost.goauthentik.io/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URI", "/app")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
||||
|
||||
s, _ := a.sessions.Get(req, a.SessionName())
|
||||
assert.Equal(t, "/app", s.Values[constants.SessionRedirect])
|
||||
}
|
||||
|
||||
func TestForwardHandleNginx_Single_Claims(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URI", "/")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
s, _ := a.sessions.Get(req, a.SessionName())
|
||||
s.ID = uuid.New().String()
|
||||
s.Options.MaxAge = 86400
|
||||
s.Values[constants.SessionClaims] = types.Claims{
|
||||
Sub: "foo",
|
||||
Proxy: &types.ProxyClaims{
|
||||
UserAttributes: map[string]any{
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"additionalHeaders": map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := a.sessions.Save(req, rr, s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
h := rr.Result().Header
|
||||
|
||||
assert.Equal(t, []string{"Basic Zm9vOmJhcg=="}, h["Authorization"])
|
||||
assert.Equal(t, []string{"bar"}, h["Foo"])
|
||||
assert.Equal(t, []string{""}, h["User-Agent"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Email"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Groups"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Jwt"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Meta-App"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Meta-Jwks"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Meta-Outpost"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Name"])
|
||||
assert.Equal(t, []string{"foo"}, h["X-Authentik-Uid"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Username"])
|
||||
}
|
||||
|
||||
func TestForwardHandleNginx_Domain_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
|
||||
@@ -101,8 +101,6 @@ RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
rustc --version && \
|
||||
cargo --version
|
||||
|
||||
RUN cat /root/.rustup/settings.toml
|
||||
|
||||
# Stage: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.11.5@sha256:555ac94f9a22e656fc5f2ce5dfee13b04e94d099e46bb8dd3a73ec7263f2e484 AS uv
|
||||
# Stage: Base python image
|
||||
|
||||
@@ -21,33 +21,45 @@ COPY web .
|
||||
RUN npm run build-proxy
|
||||
|
||||
# Stage 2: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.26.2-trixie@sha256:4a7137ea573f79c86ae451ff05817ed762ef5597fcf732259e97abeb3108d873 AS builder
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341 AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ARG GOOS=$TARGETOS
|
||||
ARG GOARCH=$TARGETARCH
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
||||
dpkg --add-architecture arm64 && \
|
||||
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends crossbuild-essential-arm64 gcc-aarch64-linux-gnu
|
||||
# Required for installing pip packages
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Build essentials
|
||||
build-essential \
|
||||
# aws-lc deps
|
||||
cmake clang golang && \
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain none && \
|
||||
rustup install && \
|
||||
rustup default "$(sed -n 's/channel = "\(.*\)"/\1/p' rust-toolchain.toml)" && \
|
||||
rustc --version && \
|
||||
cargo --version
|
||||
# See https://github.com/aws/aws-lc-rs/issues/569
|
||||
ENV AWS_LC_FIPS_SYS_CC=clang
|
||||
|
||||
RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
|
||||
--mount=type=bind,target=/go/src/goauthentik.io/go.sum,src=./go.sum \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go mod download
|
||||
|
||||
COPY . .
|
||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
||||
go build -o /go/proxy ./cmd/proxy
|
||||
RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
--mount=type=bind,target=Cargo.toml,src=Cargo.toml \
|
||||
--mount=type=bind,target=Cargo.lock,src=Cargo.lock \
|
||||
--mount=type=bind,target=.cargo/,src=.cargo/ \
|
||||
--mount=type=bind,target=src/,src=src/ \
|
||||
--mount=type=bind,target=packages/,src=packages/ \
|
||||
--mount=type=bind,target=authentik/lib/default.yml,src=authentik/lib/default.yml \
|
||||
# Required otherwise workspace discovery fails
|
||||
--mount=type=bind,target=website/scripts/docsmg/,src=website/scripts/docsmg/ \
|
||||
--mount=type=cache,id=cargo-git-db-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/git/db/ \
|
||||
--mount=type=cache,id=cargo-registry-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/registry/ \
|
||||
--mount=type=cache,id=rust-target-$TARGETARCH$TARGETVARIANT,target=/build/target/ \
|
||||
cargo build --package authentik --no-default-features --features proxy --locked --release && \
|
||||
cp ./target/release/authentik /bin/authentik
|
||||
|
||||
# Stage 3: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341
|
||||
@@ -72,13 +84,13 @@ RUN apt-get update && \
|
||||
apt-get clean && \
|
||||
rm -rf /tmp/* /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /go/proxy /
|
||||
COPY --from=builder /bin/authentik /
|
||||
COPY --from=web-builder /static/robots.txt /web/robots.txt
|
||||
COPY --from=web-builder /static/security.txt /web/security.txt
|
||||
COPY --from=web-builder /static/dist/ /web/dist/
|
||||
COPY --from=web-builder /static/authentik/ /web/authentik/
|
||||
|
||||
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/proxy", "healthcheck" ]
|
||||
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/authentik", "healthcheck" ]
|
||||
|
||||
EXPOSE 9000 9300 9443
|
||||
|
||||
@@ -87,4 +99,4 @@ USER 1000
|
||||
ENV TMPDIR=/dev/shm/ \
|
||||
GOFIPS=1
|
||||
|
||||
ENTRYPOINT ["/proxy"]
|
||||
ENTRYPOINT ["/authentik", "proxy"]
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-06 00:27+0000\n"
|
||||
"POT-Creation-Date: 2026-05-13 05:39+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -226,6 +226,10 @@ msgstr ""
|
||||
msgid "The slug '{slug}' is reserved and cannot be used for applications."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "User does not have permission to add members to this group."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/providers.py
|
||||
msgid ""
|
||||
"When not set all providers are returned. When set to true, only backchannel "
|
||||
@@ -256,6 +260,14 @@ msgstr ""
|
||||
msgid "Setting a user to internal service account is not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/users.py
|
||||
msgid "User does not have permission to add members to a superuser group."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/users.py
|
||||
msgid "User does not have permission to assign roles."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/users.py
|
||||
msgid "Can't modify internal service account users"
|
||||
msgstr ""
|
||||
|
||||
@@ -11,3 +11,4 @@ Naur
|
||||
Wärting
|
||||
Aadit
|
||||
Kilby
|
||||
Kahmen
|
||||
|
||||
@@ -164,3 +164,4 @@ yamltags
|
||||
zxcvbn
|
||||
~uuid
|
||||
~uuids
|
||||
wreply
|
||||
|
||||
@@ -22,11 +22,13 @@ axum-server.workspace = true
|
||||
config-rs.workspace = true
|
||||
console-subscriber.workspace = true
|
||||
eyre.workspace = true
|
||||
futures.workspace = true
|
||||
glob.workspace = true
|
||||
ipnet.workspace = true
|
||||
json-subscriber.workspace = true
|
||||
notify.workspace = true
|
||||
pin-project-lite.workspace = true
|
||||
rcgen.workspace = true
|
||||
reqwest.workspace = true
|
||||
reqwest-middleware.workspace = true
|
||||
rustls.workspace = true
|
||||
@@ -43,6 +45,7 @@ tracing-error.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
nix.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Utilities for working with the authentik API client.
|
||||
|
||||
use ak_client::apis::configuration::Configuration;
|
||||
use ak_client::{apis::configuration::Configuration, models::Pagination};
|
||||
use eyre::{Result, eyre};
|
||||
use url::Url;
|
||||
|
||||
@@ -60,6 +60,42 @@ pub fn make_config() -> Result<Configuration> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch all pages from a paginated API endpoint, returning all results combined.
|
||||
///
|
||||
/// - `fetch`: a function that takes a page number and returns a future resolving to a paginated
|
||||
/// response.
|
||||
/// - `get_pagination`: a function that extracts the [`Pagination`] metadata from a response.
|
||||
/// - `get_results`: a function that extracts the result items from a response.
|
||||
pub async fn fetch_all<T, R, E, F, Fut>(
|
||||
fetch: F,
|
||||
get_pagination: impl Fn(&R) -> &Pagination,
|
||||
get_results: impl Fn(R) -> Vec<T>,
|
||||
) -> std::result::Result<Vec<T>, E>
|
||||
where
|
||||
F: Fn(i32) -> Fut,
|
||||
Fut: Future<Output = std::result::Result<R, E>>,
|
||||
{
|
||||
let mut page = 1;
|
||||
let mut results = Vec::with_capacity(0);
|
||||
|
||||
loop {
|
||||
let response = fetch(page).await?;
|
||||
let next = get_pagination(&response).next;
|
||||
if page == 1 {
|
||||
let count = get_pagination(&response).count as usize;
|
||||
results.reserve(count);
|
||||
}
|
||||
results.extend(get_results(response));
|
||||
if next > 0.0 {
|
||||
page += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
@@ -3,8 +3,9 @@ use std::{collections::HashMap, net::SocketAddr, num::NonZeroUsize};
|
||||
use ipnet::IpNet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub(super) const KEYS_TO_PARSE_AS_LIST: [&str; 4] = [
|
||||
pub(super) const KEYS_TO_PARSE_AS_LIST: [&str; 5] = [
|
||||
"listen.http",
|
||||
"listen.https",
|
||||
"listen.metrics",
|
||||
"listen.trusted_proxy_cidrs",
|
||||
"log.http_headers",
|
||||
@@ -59,6 +60,7 @@ pub struct PostgreSQLConfig {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListenConfig {
|
||||
pub http: Vec<SocketAddr>,
|
||||
pub https: Vec<SocketAddr>,
|
||||
pub metrics: Vec<SocketAddr>,
|
||||
pub debug_tokio: SocketAddr,
|
||||
pub trusted_proxy_cidrs: Vec<IpNet>,
|
||||
|
||||
@@ -7,6 +7,9 @@ use tracing::trace;
|
||||
|
||||
use crate::config;
|
||||
|
||||
pub mod self_signed;
|
||||
pub mod store;
|
||||
|
||||
/// Dummy resolver for FIPS compliance check.
|
||||
#[derive(Debug)]
|
||||
struct EmptyCertResolver;
|
||||
52
packages/ak-common/src/tls/self_signed.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use eyre::Result;
|
||||
use rcgen::{
|
||||
Certificate, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, KeyPair,
|
||||
KeyUsagePurpose, PKCS_RSA_SHA256, SanType,
|
||||
};
|
||||
use rustls::{
|
||||
crypto::aws_lc_rs::sign::any_supported_type,
|
||||
pki_types::{CertificateDer, PrivateKeyDer},
|
||||
sign::CertifiedKey,
|
||||
};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
pub fn generate() -> Result<(Certificate, KeyPair)> {
|
||||
let signing_key = KeyPair::generate_for(&PKCS_RSA_SHA256)?;
|
||||
|
||||
let mut params = CertificateParams::default();
|
||||
params.not_before = OffsetDateTime::now_utc();
|
||||
params.not_after = OffsetDateTime::now_utc() + Duration::days(365);
|
||||
params.distinguished_name = {
|
||||
let mut dn = DistinguishedName::new();
|
||||
dn.push(DnType::OrganizationName, "authentik");
|
||||
dn.push(DnType::CommonName, "authentik default certificate");
|
||||
dn
|
||||
};
|
||||
params.subject_alt_names = vec![SanType::DnsName("*".try_into()?)];
|
||||
params.key_usages = vec![
|
||||
KeyUsagePurpose::DigitalSignature,
|
||||
KeyUsagePurpose::KeyEncipherment,
|
||||
];
|
||||
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
|
||||
|
||||
let cert = params.self_signed(&signing_key)?;
|
||||
|
||||
Ok((cert, signing_key))
|
||||
}
|
||||
|
||||
pub fn generate_certifiedkey() -> Result<CertifiedKey> {
|
||||
let (cert, keypair) = generate()?;
|
||||
|
||||
let cert_der = cert.der().to_vec();
|
||||
let key_der = keypair.serialize_der();
|
||||
|
||||
let private_key =
|
||||
PrivateKeyDer::try_from(key_der).map_err(|_| rcgen::Error::CouldNotParseKeyPair)?;
|
||||
let signing_key =
|
||||
any_supported_type(&private_key).map_err(|_| rcgen::Error::CouldNotParseKeyPair)?;
|
||||
|
||||
Ok(CertifiedKey::new(
|
||||
vec![CertificateDer::from(cert_der)],
|
||||
signing_key,
|
||||
))
|
||||
}
|
||||
92
packages/ak-common/src/tls/store.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use ak_client::apis::{
|
||||
configuration::Configuration,
|
||||
crypto_api::{
|
||||
crypto_certificatekeypairs_retrieve, crypto_certificatekeypairs_view_certificate_retrieve,
|
||||
crypto_certificatekeypairs_view_private_key_retrieve,
|
||||
},
|
||||
};
|
||||
use eyre::{Report, Result};
|
||||
use futures::FutureExt as _;
|
||||
use rustls::{
|
||||
crypto::CryptoProvider,
|
||||
pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject as _},
|
||||
sign::CertifiedKey,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Certificate {
|
||||
pub fingerprint: String,
|
||||
|
||||
pub certificate: String,
|
||||
pub key: String,
|
||||
|
||||
pub certified_key: Arc<CertifiedKey>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CertificateStore {
|
||||
certificates: Arc<Mutex<HashMap<Uuid, Arc<Certificate>>>>,
|
||||
}
|
||||
|
||||
impl CertificateStore {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub async fn ensure_keypair(
|
||||
&self,
|
||||
api_config: &Configuration,
|
||||
kp_uuid: Uuid,
|
||||
) -> Result<Arc<Certificate>> {
|
||||
let kp_uuid_s = kp_uuid.to_string();
|
||||
|
||||
let fingerprint = crypto_certificatekeypairs_retrieve(api_config, &kp_uuid_s)
|
||||
.await?
|
||||
.fingerprint_sha256;
|
||||
|
||||
if let Some(certificate) = self.certificates.lock().await.get(&kp_uuid)
|
||||
&& let Some(fingerprint) = &fingerprint
|
||||
&& &certificate.fingerprint == fingerprint
|
||||
{
|
||||
return Ok(Arc::clone(certificate));
|
||||
}
|
||||
|
||||
let (cert, key) = tokio::try_join!(
|
||||
crypto_certificatekeypairs_view_certificate_retrieve(api_config, &kp_uuid_s, None,)
|
||||
.map(|res| res.map_err(Report::from)),
|
||||
crypto_certificatekeypairs_view_private_key_retrieve(api_config, &kp_uuid_s, None,)
|
||||
.map(|res| res.map_err(Report::from)),
|
||||
)?;
|
||||
|
||||
let certified_key = {
|
||||
let cert_chain = CertificateDer::pem_reader_iter(cert.data.as_bytes())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let key_der = PrivateKeyDer::from_pem_reader(key.data.as_bytes())?;
|
||||
let provider = CryptoProvider::get_default().expect("no rustls provider installed");
|
||||
Arc::new(CertifiedKey::new(
|
||||
cert_chain,
|
||||
provider.key_provider.load_private_key(key_der)?,
|
||||
))
|
||||
};
|
||||
|
||||
let cert = Arc::new(Certificate {
|
||||
fingerprint: fingerprint.unwrap_or_default(),
|
||||
certificate: cert.data,
|
||||
key: key.data,
|
||||
certified_key,
|
||||
});
|
||||
|
||||
if !cert.fingerprint.is_empty() {
|
||||
self.certificates
|
||||
.lock()
|
||||
.await
|
||||
.insert(kp_uuid, Arc::clone(&cert));
|
||||
}
|
||||
|
||||
Ok(cert)
|
||||
}
|
||||
}
|
||||
@@ -30,12 +30,12 @@ pub fn install() -> Result<()> {
|
||||
}
|
||||
|
||||
if config.debug {
|
||||
let console_layer = console_subscriber::ConsoleLayer::builder()
|
||||
.server_addr(config.listen.debug_tokio)
|
||||
.spawn();
|
||||
// let console_layer = console_subscriber::ConsoleLayer::builder()
|
||||
// .server_addr(config.listen.debug_tokio)
|
||||
// .spawn();
|
||||
tracing_subscriber::registry()
|
||||
.with(ErrorLayer::default())
|
||||
.with(console_layer)
|
||||
// .with(console_layer)
|
||||
.with(
|
||||
fmt::layer()
|
||||
.compact()
|
||||
@@ -186,12 +186,9 @@ pub mod sentry {
|
||||
sentry_dsn: Some(config.sentry_dsn),
|
||||
environment: config.environment,
|
||||
send_pii: config.send_pii,
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
reason = "This is fine, we'll never get big values here."
|
||||
)]
|
||||
#[expect(
|
||||
clippy::as_conversions,
|
||||
clippy::cast_possible_truncation,
|
||||
reason = "This is fine, we'll never get big values here."
|
||||
)]
|
||||
sample_rate: config.traces_sample_rate as f32,
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { OAuth2Provider } from "./OAuth2Provider";
|
||||
import { OAuth2ProviderFromJSON, OAuth2ProviderToJSON } from "./OAuth2Provider";
|
||||
import type { Provider } from "./Provider";
|
||||
import { ProviderFromJSON, ProviderToJSON } from "./Provider";
|
||||
import type { User } from "./User";
|
||||
import { UserFromJSON, UserToJSON } from "./User";
|
||||
|
||||
@@ -31,10 +31,10 @@ export interface ExpiringBaseGrantModel {
|
||||
readonly pk: number;
|
||||
/**
|
||||
*
|
||||
* @type {OAuth2Provider}
|
||||
* @type {Provider}
|
||||
* @memberof ExpiringBaseGrantModel
|
||||
*/
|
||||
provider: OAuth2Provider;
|
||||
provider: Provider;
|
||||
/**
|
||||
*
|
||||
* @type {User}
|
||||
@@ -86,7 +86,7 @@ export function ExpiringBaseGrantModelFromJSONTyped(
|
||||
}
|
||||
return {
|
||||
pk: json["pk"],
|
||||
provider: OAuth2ProviderFromJSON(json["provider"]),
|
||||
provider: ProviderFromJSON(json["provider"]),
|
||||
user: UserFromJSON(json["user"]),
|
||||
isExpired: json["is_expired"],
|
||||
expires: json["expires"] == null ? undefined : new Date(json["expires"]),
|
||||
@@ -107,7 +107,7 @@ export function ExpiringBaseGrantModelToJSONTyped(
|
||||
}
|
||||
|
||||
return {
|
||||
provider: OAuth2ProviderToJSON(value["provider"]),
|
||||
provider: ProviderToJSON(value["provider"]),
|
||||
user: UserToJSON(value["user"]),
|
||||
expires: value["expires"] == null ? value["expires"] : value["expires"].toISOString(),
|
||||
scope: value["scope"],
|
||||
|
||||
12
packages/client-ts/src/models/TokenModel.ts
generated
@@ -12,8 +12,8 @@
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { OAuth2Provider } from "./OAuth2Provider";
|
||||
import { OAuth2ProviderFromJSON, OAuth2ProviderToJSON } from "./OAuth2Provider";
|
||||
import type { Provider } from "./Provider";
|
||||
import { ProviderFromJSON, ProviderToJSON } from "./Provider";
|
||||
import type { User } from "./User";
|
||||
import { UserFromJSON, UserToJSON } from "./User";
|
||||
|
||||
@@ -31,10 +31,10 @@ export interface TokenModel {
|
||||
readonly pk: number;
|
||||
/**
|
||||
*
|
||||
* @type {OAuth2Provider}
|
||||
* @type {Provider}
|
||||
* @memberof TokenModel
|
||||
*/
|
||||
provider: OAuth2Provider;
|
||||
provider: Provider;
|
||||
/**
|
||||
*
|
||||
* @type {User}
|
||||
@@ -96,7 +96,7 @@ export function TokenModelFromJSONTyped(json: any, ignoreDiscriminator: boolean)
|
||||
}
|
||||
return {
|
||||
pk: json["pk"],
|
||||
provider: OAuth2ProviderFromJSON(json["provider"]),
|
||||
provider: ProviderFromJSON(json["provider"]),
|
||||
user: UserFromJSON(json["user"]),
|
||||
isExpired: json["is_expired"],
|
||||
expires: json["expires"] == null ? undefined : new Date(json["expires"]),
|
||||
@@ -119,7 +119,7 @@ export function TokenModelToJSONTyped(
|
||||
}
|
||||
|
||||
return {
|
||||
provider: OAuth2ProviderToJSON(value["provider"]),
|
||||
provider: ProviderToJSON(value["provider"]),
|
||||
user: UserToJSON(value["user"]),
|
||||
expires: value["expires"] == null ? value["expires"] : value["expires"].toISOString(),
|
||||
scope: value["scope"],
|
||||
|
||||
@@ -60,7 +60,7 @@ export const LogLevels = /** @type {Level[]} */ (Object.keys(LogLevelLabel));
|
||||
/**
|
||||
* @callback LoggerFactory
|
||||
* @param {string | null} [prefix]
|
||||
* @param {...string} args
|
||||
* @param {...string[]} args
|
||||
* @returns {Logger}
|
||||
*/
|
||||
|
||||
@@ -207,7 +207,7 @@ export function pinoLight(options) {
|
||||
* Creates a logger with the given prefix.
|
||||
*
|
||||
* @param {string} [prefix]
|
||||
* @param {...string} args
|
||||
* @param {...string[]} args
|
||||
* @returns {Logger}
|
||||
*
|
||||
*/
|
||||
|
||||
6
packages/logger-js/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/logger-js",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/logger-js",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.3",
|
||||
@@ -68,7 +68,7 @@
|
||||
},
|
||||
"../tsconfig": {
|
||||
"name": "@goauthentik/tsconfig",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.9",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/logger-js",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"description": "Pino-based logger for authentik",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -57,7 +57,7 @@ dependencies = [
|
||||
"pyyaml==6.0.3",
|
||||
"requests-oauthlib==2.0.0",
|
||||
"scim2-filter-parser==0.7.0",
|
||||
"sentry-sdk==2.58.0",
|
||||
"sentry-sdk==2.59.0",
|
||||
"service-identity==24.2.0",
|
||||
"setproctitle==1.3.7",
|
||||
"structlog==25.5.0",
|
||||
@@ -85,7 +85,7 @@ dev = [
|
||||
"coverage[toml]==7.13.5",
|
||||
"daphne==4.2.1",
|
||||
"debugpy==1.8.20",
|
||||
"django-stubs[compatible-mypy]==6.0.3",
|
||||
"django-stubs[compatible-mypy]==6.0.4",
|
||||
"djangorestframework-stubs[compatible-mypy]==3.16.9",
|
||||
"drf-jsonschema-serializer==3.0.0",
|
||||
"freezegun==1.5.5",
|
||||
|
||||
12
schema.yml
@@ -5386,6 +5386,8 @@ paths:
|
||||
using this object
|
||||
tags:
|
||||
- endpoints
|
||||
security:
|
||||
- {}
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
@@ -5430,6 +5432,8 @@ paths:
|
||||
using this object
|
||||
tags:
|
||||
- endpoints
|
||||
security:
|
||||
- {}
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
@@ -5453,6 +5457,8 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeviceFactsRequest'
|
||||
security:
|
||||
- {}
|
||||
responses:
|
||||
'204':
|
||||
description: Successfully checked in
|
||||
@@ -5473,6 +5479,8 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EnrollRequest'
|
||||
required: true
|
||||
security:
|
||||
- {}
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
@@ -39078,7 +39086,7 @@ components:
|
||||
readOnly: true
|
||||
title: ID
|
||||
provider:
|
||||
$ref: '#/components/schemas/OAuth2Provider'
|
||||
$ref: '#/components/schemas/Provider'
|
||||
user:
|
||||
$ref: '#/components/schemas/User'
|
||||
is_expired:
|
||||
@@ -57253,7 +57261,7 @@ components:
|
||||
readOnly: true
|
||||
title: ID
|
||||
provider:
|
||||
$ref: '#/components/schemas/OAuth2Provider'
|
||||
$ref: '#/components/schemas/Provider'
|
||||
user:
|
||||
$ref: '#/components/schemas/User'
|
||||
is_expired:
|
||||
|
||||
10
src/main.rs
@@ -8,6 +8,8 @@ use eyre::{Result, eyre};
|
||||
use tracing::{error, info, trace};
|
||||
|
||||
mod metrics;
|
||||
#[cfg(feature = "proxy")]
|
||||
mod outpost;
|
||||
#[cfg(feature = "core")]
|
||||
mod server;
|
||||
#[cfg(feature = "core")]
|
||||
@@ -29,6 +31,8 @@ enum Command {
|
||||
Server(server::Cli),
|
||||
#[cfg(feature = "core")]
|
||||
Worker(worker::Cli),
|
||||
#[cfg(feature = "proxy")]
|
||||
Proxy(outpost::proxy::Cli),
|
||||
}
|
||||
|
||||
#[derive(Debug, FromArgs, PartialEq)]
|
||||
@@ -53,6 +57,8 @@ fn main() -> Result<()> {
|
||||
Command::Server(_) => Mode::set(Mode::Server)?,
|
||||
#[cfg(feature = "core")]
|
||||
Command::Worker(_) => Mode::set(Mode::Worker)?,
|
||||
#[cfg(feature = "proxy")]
|
||||
Command::Proxy(_) => Mode::set(Mode::Proxy)?,
|
||||
}
|
||||
|
||||
trace!("installing error formatting");
|
||||
@@ -108,6 +114,10 @@ fn main() -> Result<()> {
|
||||
let workers = worker::start(args, &mut tasks)?;
|
||||
metrics.workers.store(Some(workers));
|
||||
}
|
||||
#[cfg(feature = "proxy")]
|
||||
Command::Proxy(args) => {
|
||||
outpost::start::<outpost::proxy::ProxyOutpost>(args, &mut tasks).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let errors = tasks.run().await;
|
||||
|
||||
318
src/outpost/event.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
use std::{fmt::Display, sync::Arc};
|
||||
|
||||
use ak_common::{Arbiter, Tasks, VERSION, api, arbiter, authentik_build_hash};
|
||||
use axum::http::{HeaderValue, header::AUTHORIZATION};
|
||||
use eyre::{Result, eyre};
|
||||
use futures::{SinkExt as _, StreamExt as _};
|
||||
use nix::unistd::gethostname;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use time::UtcDateTime;
|
||||
use tokio::{
|
||||
signal::unix::SignalKind,
|
||||
time::{Duration, interval, sleep},
|
||||
};
|
||||
use tokio_tungstenite::tungstenite::{Message, client::IntoClientRequest as _};
|
||||
use tracing::{debug, info, instrument, trace, warn};
|
||||
use url::Url;
|
||||
|
||||
use crate::outpost::{Outpost, OutpostController};
|
||||
|
||||
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone, Copy, Eq)]
|
||||
#[repr(u8)]
|
||||
enum EventKind {
|
||||
/// Code used to acknowledge a previous message.
|
||||
Ack = 0,
|
||||
/// Code used to send a healthcheck keepalive.
|
||||
Hello = 1,
|
||||
/// Code received to trigger a config update.
|
||||
TriggerUpdate = 2,
|
||||
/// Code received to trigger some provider specific function.
|
||||
ProviderSpecific = 3,
|
||||
/// Code received to identify the end of a session.
|
||||
SessionEnd = 4,
|
||||
}
|
||||
|
||||
impl Display for EventKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Ack => write!(f, "Ack"),
|
||||
Self::Hello => write!(f, "Hello"),
|
||||
Self::TriggerUpdate => write!(f, "TriggerUpdate"),
|
||||
Self::ProviderSpecific => write!(f, "ProviderSpecific"),
|
||||
Self::SessionEnd => write!(f, "SessionEnd"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Event {
|
||||
instruction: EventKind,
|
||||
args: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct EventSessionEnd {
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
fn build_ws_url(mut url: Url, outpost_pk: &str, instance_uuid: &str, attempt: u32) -> Result<Url> {
|
||||
let ws_scheme = match url.scheme() {
|
||||
"https" => "wss",
|
||||
"http" => "ws",
|
||||
other => return Err(eyre!("Unsupported scheme for WebSocket URL: {other}")),
|
||||
};
|
||||
|
||||
url.set_scheme(ws_scheme)
|
||||
.map_err(|()| eyre!("Failed to set URL scheme to {ws_scheme}"))?;
|
||||
url.set_path(&format!("{}ws/outpost/{outpost_pk}/", url.path()));
|
||||
url.query_pairs_mut()
|
||||
.append_pair("instance_uuid", instance_uuid)
|
||||
.append_pair("attempt", &attempt.to_string());
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
fn hello_args(instance_uuid: &str) -> serde_json::Value {
|
||||
let raw_hostname = gethostname().unwrap_or_default();
|
||||
let hostname = raw_hostname.to_string_lossy();
|
||||
|
||||
serde_json::json!({
|
||||
"version": VERSION,
|
||||
"buildHash": authentik_build_hash(None),
|
||||
"uuid": instance_uuid,
|
||||
// TODO: rust version and AWS-LC versions
|
||||
"hostname": hostname,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn handle_event<O: Outpost>(
|
||||
controller: Arc<OutpostController>,
|
||||
outpost: Arc<O>,
|
||||
event: Event,
|
||||
reload_offset: Option<Duration>,
|
||||
) -> Result<()> {
|
||||
match event.instruction {
|
||||
EventKind::Ack | EventKind::Hello => {}
|
||||
EventKind::TriggerUpdate => {
|
||||
info!("received update trigger, refreshing outpost");
|
||||
if let Some(reload_offset) = reload_offset {
|
||||
sleep(reload_offset).await;
|
||||
}
|
||||
controller.refresh().await?;
|
||||
debug!("outpost controller has been refreshed");
|
||||
outpost.refresh().await?;
|
||||
debug!("outpost has been refreshed");
|
||||
#[expect(
|
||||
clippy::as_conversions,
|
||||
clippy::cast_precision_loss,
|
||||
reason = "This is fine, we'll never get big values here."
|
||||
)]
|
||||
controller
|
||||
.m_last_update
|
||||
.set(UtcDateTime::now().unix_timestamp() as f64);
|
||||
}
|
||||
EventKind::SessionEnd => {
|
||||
let event: EventSessionEnd = serde_json::from_value(event.args)?;
|
||||
outpost.end_session(event).await?;
|
||||
}
|
||||
#[expect(
|
||||
clippy::unimplemented,
|
||||
reason = "this is only relevant for the RAC provider"
|
||||
)]
|
||||
EventKind::ProviderSpecific => unimplemented!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn watch_events_inner<O: Outpost>(
|
||||
arbiter: Arbiter,
|
||||
controller: Arc<OutpostController>,
|
||||
outpost: Arc<O>,
|
||||
attempt: u32,
|
||||
) -> Result<()> {
|
||||
let server_config = api::ServerConfig::new()?;
|
||||
let ws_url = build_ws_url(
|
||||
server_config.host,
|
||||
&controller.outpost.load().pk.to_string(),
|
||||
&controller.instance_uuid.to_string(),
|
||||
attempt,
|
||||
)?;
|
||||
|
||||
debug!(url = %ws_url, "connecting to websocket");
|
||||
let mut request = ws_url.into_client_request()?;
|
||||
let token = controller
|
||||
.api_config
|
||||
.bearer_access_token
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
request.headers_mut().insert(
|
||||
AUTHORIZATION,
|
||||
HeaderValue::from_str(&format!("Bearer {token}"))?,
|
||||
);
|
||||
|
||||
let (ws_stream, _response) = tokio_tungstenite::connect_async(request).await?;
|
||||
let (mut ws_write, mut ws_read) = ws_stream.split();
|
||||
|
||||
info!(
|
||||
outpost = %controller.outpost.load().pk,
|
||||
"connected to websocket"
|
||||
);
|
||||
controller.m_connection.set(1_u8);
|
||||
|
||||
let get_refresh_interval = || {
|
||||
let mut interval = controller.outpost.load().refresh_interval_s;
|
||||
// Ensure timer interval is not negative or 0.
|
||||
// If it is, we default to 5 minutes.
|
||||
if interval <= 0_i32 {
|
||||
interval = 60_i32 * 5_i32;
|
||||
}
|
||||
// Clamp interval to be at least 30 seconds.
|
||||
if interval < 30_i32 {
|
||||
interval = 30_i32;
|
||||
}
|
||||
// infallible because we bound it to be positive above
|
||||
Duration::from_secs(interval.try_into().expect("infallible"))
|
||||
};
|
||||
let mut refresh_interval = interval(get_refresh_interval());
|
||||
let mut heartbeat_interval = interval(Duration::from_secs(10));
|
||||
|
||||
let mut events_rx = arbiter.events_subscribe();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = refresh_interval.tick() => {
|
||||
info!("refreshing outpost on interval");
|
||||
if let Err(err) = handle_event(
|
||||
Arc::clone(&controller),
|
||||
Arc::clone(&outpost),
|
||||
Event {
|
||||
instruction: EventKind::TriggerUpdate,
|
||||
args: serde_json::Value::Null
|
||||
},
|
||||
None,
|
||||
).await {
|
||||
warn!(?err, "failed to refresh");
|
||||
}
|
||||
refresh_interval = interval(get_refresh_interval());
|
||||
// Since we re-create the interval, we need to make it tick instantly to avoid
|
||||
// ending up in a never-ending tick-loop.
|
||||
refresh_interval.tick().await;
|
||||
},
|
||||
_ = heartbeat_interval.tick() => {
|
||||
let ping = Event {
|
||||
instruction: EventKind::Hello,
|
||||
args: hello_args(&controller.instance_uuid.to_string()),
|
||||
};
|
||||
ws_write.send(Message::text(serde_json::to_string(&ping)?)).await?;
|
||||
trace!("sent websocket hello (heartbeat)");
|
||||
},
|
||||
Ok(arbiter::Event::Signal(signal)) = events_rx.recv() => {
|
||||
if signal == SignalKind::user_defined1() {
|
||||
info!("refreshing outpost on signal");
|
||||
if let Err(err) = handle_event(
|
||||
Arc::clone(&controller),
|
||||
Arc::clone(&outpost),
|
||||
Event {
|
||||
instruction: EventKind::TriggerUpdate,
|
||||
args: serde_json::Value::Null
|
||||
},
|
||||
None,
|
||||
).await {
|
||||
warn!(?err, "failed to refresh");
|
||||
}
|
||||
}
|
||||
},
|
||||
msg = ws_read.next() => {
|
||||
let Some(msg) = msg else {
|
||||
break;
|
||||
};
|
||||
let msg = msg?;
|
||||
match msg {
|
||||
Message::Text(text) => {
|
||||
let Ok(event): Result<Event, _> = serde_json::from_str(&text) else {
|
||||
warn!(data = text.as_str(), "failed to parse event");
|
||||
continue;
|
||||
};
|
||||
trace!(event = %event.instruction, "received websocket event");
|
||||
if let Err(err) = handle_event(
|
||||
Arc::clone(&controller),
|
||||
Arc::clone(&outpost),
|
||||
event,
|
||||
Some(controller.reload_offset),
|
||||
).await {
|
||||
warn!(?err, "failed to handle event");
|
||||
}
|
||||
},
|
||||
Message::Ping(data) => {
|
||||
ws_write.send(Message::Pong(data)).await?;
|
||||
},
|
||||
Message::Close(_) => {
|
||||
break;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
},
|
||||
() = arbiter.shutdown() => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn watch_events<O: Outpost>(
|
||||
arbiter: Arbiter,
|
||||
controller: Arc<OutpostController>,
|
||||
outpost: Arc<O>,
|
||||
) -> Result<()> {
|
||||
const MAX_BACKOFF: Duration = Duration::from_mins(5);
|
||||
let mut backoff = Duration::from_secs(1);
|
||||
let mut attempt: u32 = 0;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
() = arbiter.shutdown() => break,
|
||||
res = watch_events_inner(
|
||||
arbiter.clone(),
|
||||
Arc::clone(&controller),
|
||||
Arc::clone(&outpost),
|
||||
attempt
|
||||
) => {
|
||||
controller.m_connection.set(0_u8);
|
||||
match res {
|
||||
Ok(()) => debug!("websocket disconnected cleanly"),
|
||||
Err(err) => warn!(?err, attempt, "websocket error"),
|
||||
}
|
||||
|
||||
info!(attempt, delay = backoff.as_secs(), "reconnecting websocket in {}s...", backoff.as_secs());
|
||||
|
||||
tokio::select! {
|
||||
() = arbiter.shutdown() => break,
|
||||
() = sleep(backoff) => {}
|
||||
}
|
||||
|
||||
backoff = (backoff * 2).min(MAX_BACKOFF);
|
||||
attempt += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("stopping event watcher");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn start<O: Outpost + 'static>(
|
||||
tasks: &mut Tasks,
|
||||
controller: Arc<OutpostController>,
|
||||
outpost: Arc<O>,
|
||||
) -> Result<()> {
|
||||
let arbiter = tasks.arbiter();
|
||||
tasks
|
||||
.build_task()
|
||||
.name(&format!("{}::watch_events", module_path!()))
|
||||
.spawn(watch_events(arbiter, controller, outpost))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
123
src/outpost/mod.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use ak_client::{
|
||||
apis::{configuration::Configuration, outposts_api::outposts_instances_list},
|
||||
models::Outpost as OutpostModel,
|
||||
};
|
||||
use ak_common::{Tasks, VERSION, api, authentik_build_hash};
|
||||
use arc_swap::ArcSwap;
|
||||
use eyre::{Result, eyre};
|
||||
use tracing::{debug, info, instrument};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) mod event;
|
||||
#[cfg(feature = "proxy")]
|
||||
pub(crate) mod proxy;
|
||||
|
||||
pub(crate) trait Outpost: Send + Sync + Sized {
|
||||
const OUTPOST_TYPE: &'static str;
|
||||
type Cli: Send + Sync;
|
||||
|
||||
async fn new(controller: Arc<OutpostController>) -> Result<Self>;
|
||||
|
||||
fn start(self: Arc<Self>, tasks: &mut Tasks) -> Result<()>;
|
||||
fn refresh(&self) -> impl Future<Output = Result<()>> + Send;
|
||||
|
||||
fn end_session(&self, event: event::EventSessionEnd)
|
||||
-> impl Future<Output = Result<()>> + Send;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct OutpostController {
|
||||
api_config: Configuration,
|
||||
outpost: ArcSwap<OutpostModel>,
|
||||
instance_uuid: Uuid,
|
||||
reload_offset: Duration,
|
||||
m_info: metrics::Gauge,
|
||||
m_last_update: metrics::Gauge,
|
||||
m_connection: metrics::Gauge,
|
||||
}
|
||||
|
||||
impl OutpostController {
|
||||
#[instrument(skip_all)]
|
||||
async fn get_outpost(api_config: &Configuration) -> Result<OutpostModel> {
|
||||
let outposts = outposts_instances_list(
|
||||
api_config, None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some(outpost) = outposts.results.into_iter().next() else {
|
||||
return Err(eyre!(
|
||||
"No outposts found with given token, ensure the given token corresponds to an \
|
||||
authentik Outpost"
|
||||
));
|
||||
};
|
||||
debug!(name = outpost.name, "fetched outpost configuration");
|
||||
|
||||
Ok(outpost)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn new<O: Outpost>() -> Result<Self> {
|
||||
let api_config = api::make_config()?;
|
||||
let outpost = Self::get_outpost(&api_config).await?;
|
||||
let instance_uuid = Uuid::new_v4();
|
||||
|
||||
let m_labels = [
|
||||
("outpost_name", outpost.name.clone()),
|
||||
("outpost_type", O::OUTPOST_TYPE.to_owned()),
|
||||
("uuid", instance_uuid.to_string()),
|
||||
("version", VERSION.to_owned()),
|
||||
("build", authentik_build_hash(None)),
|
||||
];
|
||||
metrics::describe_gauge!("authentik_outpost_info", "Outpost info");
|
||||
let m_info = metrics::gauge!("authentik_outpost_info", &m_labels);
|
||||
metrics::describe_gauge!("authentik_outpost_last_update", "Time of last update");
|
||||
let m_last_update = metrics::gauge!("authentik_outpost_last_update", &m_labels);
|
||||
metrics::describe_gauge!("authentik_outpost_connection", "Connection status");
|
||||
let m_connection = metrics::gauge!("authentik_outpost_connection", &m_labels);
|
||||
|
||||
let reload_offset = Duration::from_secs(rand::random_range(0..10));
|
||||
let controller = Self {
|
||||
api_config,
|
||||
outpost: ArcSwap::from_pointee(outpost),
|
||||
instance_uuid,
|
||||
reload_offset,
|
||||
m_info,
|
||||
m_last_update,
|
||||
m_connection,
|
||||
};
|
||||
|
||||
info!(embedded = controller.is_embedded(), "outpost mode");
|
||||
debug!(?reload_offset, "HA Reload offset");
|
||||
|
||||
Ok(controller)
|
||||
}
|
||||
|
||||
fn is_embedded(&self) -> bool {
|
||||
self.outpost
|
||||
.load()
|
||||
.managed
|
||||
.as_ref()
|
||||
.and_then(|m| m.as_deref())
|
||||
.is_some_and(|m| m == "goauthentik.io/outposts/embedded")
|
||||
}
|
||||
|
||||
async fn refresh(&self) -> Result<()> {
|
||||
let outpost = Self::get_outpost(&self.api_config).await?;
|
||||
self.outpost.swap(Arc::new(outpost));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub(crate) async fn start<O: Outpost + 'static>(_cli: O::Cli, tasks: &mut Tasks) -> Result<()> {
|
||||
let controller = Arc::new(OutpostController::new::<O>().await?);
|
||||
let outpost = Arc::new(O::new(Arc::clone(&controller)).await?);
|
||||
|
||||
event::start(tasks, Arc::clone(&controller), Arc::clone(&outpost))?;
|
||||
outpost.start(tasks)?;
|
||||
controller.m_info.set(1_u8);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
61
src/outpost/proxy/application.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ak_client::models::ProxyOutpostConfig;
|
||||
use ak_common::tls::store::Certificate;
|
||||
use axum::Router;
|
||||
use eyre::{Result, eyre};
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
|
||||
use crate::outpost::proxy::ProxyOutpost;
|
||||
|
||||
const _REDIRECT_PARAM: &str = "rd";
|
||||
const CALLBACK_SIGNATURE: &str = "X-authentik-auth-callback";
|
||||
const _LOGOUT_SIGNATURE: &str = "X-authentik-logout";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Application {
|
||||
pub(super) host: String,
|
||||
pub(super) provider: ProxyOutpostConfig,
|
||||
pub(super) router: Router,
|
||||
pub(super) cert: Option<Arc<Certificate>>,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
#[instrument(skip_all)]
|
||||
pub(super) async fn new(outpost: &ProxyOutpost, provider: ProxyOutpostConfig) -> Result<Self> {
|
||||
let external_url = Url::parse(&provider.external_host)?;
|
||||
if !external_url.has_authority() {
|
||||
return Err(eyre!("no host in external host"));
|
||||
}
|
||||
let external_host = external_url.authority();
|
||||
|
||||
let _old_app = outpost.apps.load().get(external_host);
|
||||
|
||||
let cert = if let Some(Some(kp_uuid)) = provider.certificate {
|
||||
Some(
|
||||
outpost
|
||||
.certificate_store
|
||||
.ensure_keypair(&outpost.controller.api_config, kp_uuid)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let _redirect_url = {
|
||||
let mut redirect_url = external_url.join("outpost.goauthentik.io/callback")?;
|
||||
redirect_url.set_query(Some(&format!("{CALLBACK_SIGNATURE}=true")));
|
||||
redirect_url
|
||||
};
|
||||
|
||||
let router = Router::new();
|
||||
|
||||
Ok(Self {
|
||||
host: external_host.to_owned(),
|
||||
provider,
|
||||
router,
|
||||
cert,
|
||||
})
|
||||
}
|
||||
}
|
||||
87
src/outpost/proxy/handlers.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ak_axum::{error::Result, extract::host::Host};
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::{Method, StatusCode, header::CONTENT_TYPE},
|
||||
response::{IntoResponse as _, Response},
|
||||
};
|
||||
use metrics::histogram;
|
||||
use serde_json::json;
|
||||
use tokio::time::Instant;
|
||||
use tower::util::ServiceExt as _;
|
||||
use tracing::{Instrument as _, debug, field, info_span, instrument, trace, warn};
|
||||
|
||||
use crate::outpost::proxy::ProxyOutpost;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub(super) async fn handle_ping(
|
||||
method: Method,
|
||||
Host(host): Host,
|
||||
State(outpost): State<Arc<ProxyOutpost>>,
|
||||
) -> Response {
|
||||
let start = Instant::now();
|
||||
histogram!(
|
||||
"authentik_outpost_proxy_request_duration_seconds",
|
||||
"outpost_name" => outpost.controller.outpost.load().name.clone(),
|
||||
"method" => method.to_string(),
|
||||
"host" => host,
|
||||
"type" => "ping",
|
||||
)
|
||||
.record(start.elapsed().as_secs_f64());
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub(super) async fn default(
|
||||
method: Method,
|
||||
Host(host): Host,
|
||||
State(outpost): State<Arc<ProxyOutpost>>,
|
||||
request: Request,
|
||||
) -> Result<Response> {
|
||||
let span = info_span!("proxy outpost request", user = field::Empty);
|
||||
let start = Instant::now();
|
||||
|
||||
let app = outpost.lookup_app(&host).or_else(|| {
|
||||
// If we only have a single app, host name switching doesn't matter.
|
||||
let apps = outpost.apps.load();
|
||||
if apps.len() == 1
|
||||
&& let Some(app) = apps.values().next()
|
||||
{
|
||||
debug!(app = app.provider.name, "found a single app, using it");
|
||||
return Some(Arc::clone(app));
|
||||
}
|
||||
None
|
||||
});
|
||||
let Some(app) = app else {
|
||||
trace!(headers = ?request.headers(), "tracing headers for no hostname match");
|
||||
warn!("no app for hostname");
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
json!({
|
||||
"message": "no app for hostname",
|
||||
"host": host,
|
||||
"detail": format!("check the outpost settings and make sure '{host}' is included."),
|
||||
})
|
||||
.to_string()
|
||||
.into(),
|
||||
)
|
||||
.expect("infallible"));
|
||||
};
|
||||
|
||||
trace!("passing to application");
|
||||
let response = app.router.clone().oneshot(request).instrument(span).await?;
|
||||
|
||||
histogram!(
|
||||
"authentik_outpost_proxy_request_duration_seconds",
|
||||
"outpost_name" => outpost.controller.outpost.load().name.clone(),
|
||||
"method" => method.to_string(),
|
||||
"host" => host,
|
||||
"type" => "app",
|
||||
)
|
||||
.record(start.elapsed().as_secs_f64());
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
231
src/outpost/proxy/mod.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use ak_axum::router::wrap_router;
|
||||
use ak_client::{apis::outposts_api::outposts_proxy_list, models::ProxyMode};
|
||||
use ak_common::{
|
||||
Tasks,
|
||||
api::fetch_all,
|
||||
config,
|
||||
tls::{self, store::CertificateStore},
|
||||
};
|
||||
use arc_swap::ArcSwap;
|
||||
use argh::FromArgs;
|
||||
use axum::Router;
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use eyre::Result;
|
||||
use rustls::{
|
||||
ServerConfig,
|
||||
server::{ClientHello, ResolvesServerCert},
|
||||
sign::CertifiedKey,
|
||||
};
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
use crate::outpost::{Outpost, OutpostController, proxy::application::Application};
|
||||
|
||||
mod application;
|
||||
mod handlers;
|
||||
|
||||
#[derive(Debug, Default, FromArgs, PartialEq, Eq)]
|
||||
/// Run the authentik proxy outpost.
|
||||
#[argh(subcommand, name = "proxy")]
|
||||
#[expect(
|
||||
clippy::empty_structs_with_brackets,
|
||||
reason = "argh doesn't support unit structs"
|
||||
)]
|
||||
pub(crate) struct Cli {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ProxyOutpost {
|
||||
controller: Arc<OutpostController>,
|
||||
apps: ArcSwap<HashMap<String, Arc<Application>>>,
|
||||
certificate_store: CertificateStore,
|
||||
default_cert: Arc<CertifiedKey>,
|
||||
}
|
||||
|
||||
impl Outpost for ProxyOutpost {
|
||||
type Cli = Cli;
|
||||
|
||||
const OUTPOST_TYPE: &'static str = "proxy";
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn new(controller: Arc<OutpostController>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
controller,
|
||||
apps: ArcSwap::from_pointee(HashMap::with_capacity(0)),
|
||||
certificate_store: CertificateStore::new(),
|
||||
default_cert: Arc::new(tls::self_signed::generate_certifiedkey()?),
|
||||
})
|
||||
}
|
||||
|
||||
fn start(self: Arc<Self>, tasks: &mut Tasks) -> Result<()> {
|
||||
let router = build_router(Arc::clone(&self));
|
||||
|
||||
for addr in config::get().listen.http.iter().copied() {
|
||||
ak_axum::server::start_plain(tasks, "proxy-outpost", router.clone(), addr)?;
|
||||
}
|
||||
|
||||
for addr in config::get().listen.https.iter().copied() {
|
||||
let resolver = Arc::clone(&self);
|
||||
let server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(resolver);
|
||||
let rustls_config = RustlsConfig::from_config(Arc::new(server_config));
|
||||
ak_axum::server::start_tls(
|
||||
tasks,
|
||||
"proxy-outpost",
|
||||
router.clone(),
|
||||
addr,
|
||||
rustls_config,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn refresh(&self) -> Result<()> {
|
||||
debug!(
|
||||
outpost_pk = %self.controller.outpost.load().pk,
|
||||
"requesting providers for outpost"
|
||||
);
|
||||
|
||||
let providers = fetch_all(
|
||||
|page| {
|
||||
outposts_proxy_list(
|
||||
&self.controller.api_config,
|
||||
None,
|
||||
None,
|
||||
Some(page),
|
||||
Some(100_i32),
|
||||
None,
|
||||
)
|
||||
},
|
||||
|r| &r.pagination,
|
||||
|r| r.results,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| error!(?err, "failed to fetch providers"))?;
|
||||
debug!(count = providers.len(), "fetched providers");
|
||||
|
||||
if providers.is_empty() && !self.controller.is_embedded() {
|
||||
warn!(
|
||||
"no providers assigned to this outpost, check outpost configuration in authentik"
|
||||
);
|
||||
}
|
||||
|
||||
for (i, provider) in providers.iter().enumerate() {
|
||||
debug!(
|
||||
index = i,
|
||||
name = provider.name,
|
||||
external_host = provider.external_host,
|
||||
assigned_to_app = provider.assigned_application_name,
|
||||
"provider details"
|
||||
);
|
||||
}
|
||||
|
||||
let mut apps = HashMap::with_capacity(providers.len());
|
||||
|
||||
for provider in providers {
|
||||
let name = provider.name.clone();
|
||||
let Ok(application) = Application::new(self, provider)
|
||||
.await
|
||||
.inspect_err(|err| warn!(?err, "failed to setup application, skipping provider"))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
info!(name, host = application.host, "loaded application");
|
||||
|
||||
apps.insert(application.host.clone(), Arc::new(application));
|
||||
}
|
||||
|
||||
self.apps.store(Arc::new(apps));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn end_session(&self, _event: super::event::EventSessionEnd) -> Result<()> {
|
||||
// todo!()
|
||||
warn!(?_event, "removing session");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvesServerCert for ProxyOutpost {
|
||||
fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
|
||||
if let Some(server_name) = client_hello.server_name()
|
||||
&& let Some(app) = self.apps.load().get(server_name)
|
||||
&& let Some(cert) = &app.cert
|
||||
{
|
||||
return Some(Arc::clone(&cert.certified_key));
|
||||
}
|
||||
Some(Arc::clone(&self.default_cert))
|
||||
}
|
||||
|
||||
fn only_raw_public_keys(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl ProxyOutpost {
|
||||
#[instrument(skip(self))]
|
||||
fn lookup_app(&self, host: &str) -> Option<Arc<Application>> {
|
||||
let apps = self.apps.load();
|
||||
|
||||
if apps.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(app) = apps.get(host) {
|
||||
debug!(app = app.provider.name, "found app based direct host match");
|
||||
return Some(Arc::clone(app));
|
||||
}
|
||||
|
||||
// For forward_auth_domain, we don't have a direct app to domain relationship.
|
||||
// Check through all apps, and check how much of their cookie domain matches the host.
|
||||
// Return the application that has the longest match.
|
||||
let mut longest_match = None;
|
||||
let mut longest_len = 0_usize;
|
||||
|
||||
for app in apps.values() {
|
||||
if app.provider.mode != Some(ProxyMode::ForwardDomain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(cookie_domain) = app.provider.cookie_domain.as_deref() {
|
||||
// Check if the cookie domain has a leading period for a wildcard.
|
||||
// This will decrease the weight of a wildcard domain, but a request to example.com
|
||||
// with the cookie domain set to example.com will still be routed correctly.
|
||||
let domain = cookie_domain.trim_start_matches('.');
|
||||
|
||||
if host.ends_with(domain) && domain.len() > longest_len {
|
||||
longest_len = domain.len();
|
||||
longest_match = Some(Arc::clone(app));
|
||||
}
|
||||
// For forward_auth_domain, we need to response on the external domain too.
|
||||
if app.provider.external_host == host {
|
||||
debug!(app = app.provider.name, "found app based on external_host");
|
||||
return Some(Arc::clone(app));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(app) = &longest_match {
|
||||
debug!(app = app.provider.name, "found app based on cookie domain");
|
||||
}
|
||||
|
||||
longest_match
|
||||
}
|
||||
}
|
||||
|
||||
fn build_router(outpost: Arc<ProxyOutpost>) -> Router {
|
||||
wrap_router(
|
||||
Router::new()
|
||||
.nest(
|
||||
"/outpost.goauthentik.io/ping",
|
||||
Router::new().fallback(handlers::handle_ping),
|
||||
)
|
||||
.fallback(handlers::default)
|
||||
.with_state(outpost),
|
||||
true,
|
||||
)
|
||||
}
|
||||
66
uv.lock
generated
@@ -366,7 +366,7 @@ requires-dist = [
|
||||
{ name = "pyyaml", specifier = "==6.0.3" },
|
||||
{ name = "requests-oauthlib", specifier = "==2.0.0" },
|
||||
{ name = "scim2-filter-parser", specifier = "==0.7.0" },
|
||||
{ name = "sentry-sdk", specifier = "==2.58.0" },
|
||||
{ name = "sentry-sdk", specifier = "==2.59.0" },
|
||||
{ name = "service-identity", specifier = "==24.2.0" },
|
||||
{ name = "setproctitle", specifier = "==1.3.7" },
|
||||
{ name = "structlog", specifier = "==25.5.0" },
|
||||
@@ -394,7 +394,7 @@ dev = [
|
||||
{ name = "coverage", extras = ["toml"], specifier = "==7.13.5" },
|
||||
{ name = "daphne", specifier = "==4.2.1" },
|
||||
{ name = "debugpy", specifier = "==1.8.20" },
|
||||
{ name = "django-stubs", extras = ["compatible-mypy"], specifier = "==6.0.3" },
|
||||
{ name = "django-stubs", extras = ["compatible-mypy"], specifier = "==6.0.4" },
|
||||
{ name = "djangorestframework-stubs", extras = ["compatible-mypy"], specifier = "==3.16.9" },
|
||||
{ name = "drf-jsonschema-serializer", specifier = "==3.0.0" },
|
||||
{ name = "freezegun", specifier = "==1.5.5" },
|
||||
@@ -1106,7 +1106,7 @@ requires-dist = [
|
||||
{ name = "django", specifier = ">=4.2,<6.0" },
|
||||
{ name = "django-pgtrigger", specifier = ">=4,<5" },
|
||||
{ name = "msgpack", specifier = ">=1,<2" },
|
||||
{ name = "psycopg", extras = ["pool"], specifier = ">=3.3.4,<4" },
|
||||
{ name = "psycopg", extras = ["pool"], specifier = ">=3,<4" },
|
||||
{ name = "structlog", specifier = ">=25,<26" },
|
||||
]
|
||||
|
||||
@@ -1269,7 +1269,7 @@ s3 = [
|
||||
|
||||
[[package]]
|
||||
name = "django-stubs"
|
||||
version = "6.0.3"
|
||||
version = "6.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
@@ -1277,9 +1277,9 @@ dependencies = [
|
||||
{ name = "types-pyyaml" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/86/0c/8d0d875af79bf774c1c3997c84aa118dba3a77be12086b9c14e130e8ec72/django_stubs-6.0.3.tar.gz", hash = "sha256:ee895f403c373608eeb50822f0733f9d9ec5ab12731d4ab58956053bb95fdd9e", size = 278214, upload-time = "2026-04-18T15:11:22.327Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/82/ccf2a2dc9cdb4bd9cbe91f11e887589bf2da7609506db00ccbc73bd8a6da/django_stubs-6.0.4.tar.gz", hash = "sha256:7aee77e8de9c14c0d9cf84988befe826d93cbc15a87e0ade2943f14d553451cf", size = 280019, upload-time = "2026-05-09T21:24:30.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/a3/6751b7684d20fc4f228bdd3dd8341d382ab3faaf65d3d050c0d59ab0a1b0/django_stubs-6.0.3-py3-none-any.whl", hash = "sha256:5fee22bcbbad59a78c727a820b6f4e68ff442ca76a922b7002e57c25dd7cb390", size = 541570, upload-time = "2026-04-18T15:11:20.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e7/5128914ada94dd6277626ef5a4a5680a4def7d2f9366214d26c1cd86723b/django_stubs-6.0.4-py3-none-any.whl", hash = "sha256:e991c68f77239663577a5f4fc75e99c84f867f378cafc97cbf4acc5aff378279", size = 543791, upload-time = "2026-05-09T21:24:28.218Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -3332,15 +3332,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.58.0"
|
||||
version = "2.59.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/b3/fb8291170d0e844173164709fc0fa0c221ed75a5da740c8746f2a83b4eb1/sentry_sdk-2.58.0.tar.gz", hash = "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f", size = 438764, upload-time = "2026-04-13T17:23:26.265Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/e0/9bf5e5fc7442b10880f3ec0eff0ef4208b84a099606f343ec4f5445227fb/sentry_sdk-2.59.0.tar.gz", hash = "sha256:cd265808ef8bf3f3edf69b527c0a0b2b6b1322762679e55b8987db2e9584aec1", size = 447331, upload-time = "2026-05-04T12:19:06.538Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/00/b8cc413748fb6383d1582e7cda51314f99743351c462a92dc690d5b5853b/sentry_sdk-2.59.0-py2.py3-none-any.whl", hash = "sha256:abcf65ee9a9d9cdebf9ad369782408ecca9c1c792686ef06ba34f5ab233527fe", size = 468432, upload-time = "2026-05-04T12:19:04.741Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3743,32 +3743,32 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ujson"
|
||||
version = "5.12.0"
|
||||
version = "5.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/3e/c35530c5ffc25b71c59ae0cd7b8f99df37313daa162ce1e2f7925f7c2877/ujson-5.12.0.tar.gz", hash = "sha256:14b2e1eb528d77bc0f4c5bd1a7ebc05e02b5b41beefb7e8567c9675b8b13bcf4", size = 7158451, upload-time = "2026-03-11T22:19:30.397Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/78/937198ea8708182dd1edbf0237bf255a96feab3f511691ad08b84da98e5d/ujson-5.12.1.tar.gz", hash = "sha256:5b7e96406c301a1366534479a7352ec40ec68bb327c0c119091635acd5925e35", size = 7164538, upload-time = "2026-05-05T22:05:01.354Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bd/9a8d693254bada62bfea75a507e014afcfdb6b9d047b6f8dd134bfefaf67/ujson-5.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85833bca01aa5cae326ac759276dc175c5fa3f7b3733b7d543cf27f2df12d1ef", size = 56499, upload-time = "2026-03-11T22:18:45.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/2d/285a83df8176e18dcd675d1a4cff8f7620f003f30903ea43929406e98986/ujson-5.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d22cad98c2a10bbf6aa083a8980db6ed90d4285a841c4de892890c2b28286ef9", size = 53998, upload-time = "2026-03-11T22:18:47.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/8b/e2f09e16dabfa91f6a84555df34a4329fa7621e92ed054d170b9054b9bb2/ujson-5.12.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cc80facad240b0c2fb5a633044420878aac87a8e7c348b9486450cba93f27c", size = 57783, upload-time = "2026-03-11T22:18:48.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/fb/ba1d06f3658a0c36d0ab3869ec3914f202bad0a9bde92654e41516c7bb13/ujson-5.12.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:d1831c07bd4dce53c4b666fa846c7eba4b7c414f2e641a4585b7f50b72f502dc", size = 60011, upload-time = "2026-03-11T22:18:49.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/2b/3e322bf82d926d9857206cd5820438d78392d1f523dacecb8bd899952f73/ujson-5.12.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e00cec383eab2406c9e006bd4edb55d284e94bb943fda558326048178d26961", size = 57465, upload-time = "2026-03-11T22:18:50.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/fd/af72d69603f9885e5136509a529a4f6d88bf652b457263ff96aefcd3ab7d/ujson-5.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f19b3af31d02a2e79c5f9a6deaab0fb3c116456aeb9277d11720ad433de6dfc6", size = 1037275, upload-time = "2026-03-11T22:18:51.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/a7/a2411ec81aef7872578e56304c3e41b3a544a9809e95c8e1df46923fc40b/ujson-5.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bacbd3c69862478cbe1c7ed4325caedec580d8acf31b8ee1b9a1e02a56295cad", size = 1196758, upload-time = "2026-03-11T22:18:53.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/85/aa18ae175dd03a118555aa14304d4f466f9db61b924c97c6f84388ecacb1/ujson-5.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94c5f1621cbcab83c03be46441f090b68b9f307b6c7ec44d4e3f6d5997383df4", size = 1089760, upload-time = "2026-03-11T22:18:55.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/d4/4b40b67ac7e916ebffc3041ae2320c5c0b8a045300d4c542b6e50930cca5/ujson-5.12.0-cp314-cp314-win32.whl", hash = "sha256:e6369ac293d2cc40d52577e4fa3d75a70c1aae2d01fa3580a34a4e6eff9286b9", size = 41043, upload-time = "2026-03-11T22:18:56.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/38/a1496d2a3428981f2b3a2ffbb4656c2b05be6cc406301d6b10a6445f6481/ujson-5.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:31348a0ffbfc815ce78daac569d893349d85a0b57e1cd2cdbba50b7f333784da", size = 45303, upload-time = "2026-03-11T22:18:57.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/d3/39dbd3159543d9c57ec3a82d36226152cf0d710784894ce5aa24b8220ac1/ujson-5.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:6879aed770557f0961b252648d36f6fdaab41079d37a2296b5649fd1b35608e0", size = 39860, upload-time = "2026-03-11T22:18:58.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/71/9b4dacb177d3509077e50497222d39eec04c8b41edb1471efc764d645237/ujson-5.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ddb08b3c2f9213df1f2e3eb2fbea4963d80ec0f8de21f0b59898e34f3b3d96d", size = 56845, upload-time = "2026-03-11T22:18:59.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/c2/8abffa3be1f3d605c4a62445fab232b3e7681512ce941c6b23014f404d36/ujson-5.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a3ae28f0b209be5af50b54ca3e2123a3de3a57d87b75f1e5aa3d7961e041983", size = 54463, upload-time = "2026-03-11T22:19:00.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/2e/60114a35d1d6796eb428f7affcba00a921831ff604a37d9142c3d8bbe5c5/ujson-5.12.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30ad4359413c8821cc7b3707f7ca38aa8bc852ba3b9c5a759ee2d7740157315", size = 58689, upload-time = "2026-03-11T22:19:01.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/ad/010925c2116c21ce119f9c2ff18d01f48a19ade3ff4c5795da03ce5829fc/ujson-5.12.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:02f93da7a4115e24f886b04fd56df1ee8741c2ce4ea491b7ab3152f744ad8f8e", size = 60618, upload-time = "2026-03-11T22:19:03.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/74/db7f638bf20282b1dccf454386cbd483faaaed3cdbb9cb27e06f74bb109e/ujson-5.12.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ff4ede90ed771140caa7e1890de17431763a483c54b3c1f88bd30f0cc1affc0", size = 58151, upload-time = "2026-03-11T22:19:04.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/7e/3ebaecfa70a2e8ce623db8e21bd5cb05d42a5ef943bcbb3309d71b5de68d/ujson-5.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bf9cc97f05048ac8f3e02cd58f0fe62b901453c24345bfde287f4305dcc31c", size = 1038117, upload-time = "2026-03-11T22:19:05.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/aa/e073eda7f0036c2973b28db7bb99faba17a932e7b52d801f9bb3e726271f/ujson-5.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2324d9a0502317ffc35d38e153c1b2fa9610ae03775c9d0f8d0cca7b8572b04e", size = 1197434, upload-time = "2026-03-11T22:19:06.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/01/b9a13f058fdd50c746b192c4447ca8d6352e696dcda912ccee10f032ff85/ujson-5.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:50524f4f6a1c839714dbaff5386a1afb245d2d5ec8213a01fbc99cea7307811e", size = 1090401, upload-time = "2026-03-11T22:19:08.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/37/3d1b4e0076b6e43379600b5229a5993db8a759ff2e1830ea635d876f6644/ujson-5.12.0-cp314-cp314t-win32.whl", hash = "sha256:f7a0430d765f9bda043e6aefaba5944d5f21ec43ff4774417d7e296f61917382", size = 41880, upload-time = "2026-03-11T22:19:09.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/c5/3c2a262a138b9f0014fe1134a6b5fdc2c54245030affbaac2fcbc0632138/ujson-5.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ccbfd94e59aad4a2566c71912b55f0547ac1680bfac25eb138e6703eb3dd434e", size = 46365, upload-time = "2026-03-11T22:19:10.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/40/956dc20b7e00dc0ff3259871864f18dab211837fce3478778bedb3132ac1/ujson-5.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:42d875388fbd091c7ea01edfff260f839ba303038ffb23475ef392012e4d63dd", size = 40398, upload-time = "2026-03-11T22:19:11.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ca/d88d86f90f8f237985f3e347b9a4f9fa24e8d30d19ec7d477ed18aa58393/ujson-5.12.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f19e9a407a24230df0cc1ec1c0f5999872ba526b14a780f80ad6479f5eed9bc", size = 58099, upload-time = "2026-05-05T22:04:06.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/2d/a0a88407cee3550f7ed1e49b41157ee2d410f51905ed51fb134844255280/ujson-5.12.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b657e870c77aaacdeea86cfad3e6d2ef9b52517e45988c9c367f7ee764fe4dd", size = 55631, upload-time = "2026-05-05T22:04:07.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6d/12a3b8e72132db244ae048075e71a0079b3c5f61ff45b7ca81d5193ab3e7/ujson-5.12.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984b5a99d1e0a037c2046c3c4b34cec832565d62d5017be0a035bf3cbfab72dc", size = 59469, upload-time = "2026-05-05T22:04:09.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/72/310f8c21737554f2d2b4f1883e1a71e8a6ab0d8f92f0feb8aaa85e0f4b66/ujson-5.12.1-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:f48ef8a16f1d85bd7982beac7adfd3fb704058631db84c1c61c8a1b7072b1508", size = 61611, upload-time = "2026-05-05T22:04:10.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/50/ab4b2f7bab6c7a67298c8f2aca80e2082eaf6f332cf2d099762647b5301e/ujson-5.12.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f39ba3b65cc637b59731532f7e7c807786bff1d0332ab2d5b96a04d2584d78f", size = 59122, upload-time = "2026-05-05T22:04:12.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/48/5d81cbe76fc2aa9e071aa489a3041cf0712f5e0663d60d501641f92b7bb4/ujson-5.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:07f307780f85b49cba93f291718421b6f5f3b627a323b431fad937a18f6587cb", size = 1038938, upload-time = "2026-05-05T22:04:13.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/a7/abe1acb0e5d8b8d724b35533a44c89684c88100a5fd9f2fee7f7155528d5/ujson-5.12.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c335caea51c31494e514b82d50763b9792d3960d2c7d9fdb6b6fb8ed50ebdd0", size = 1198416, upload-time = "2026-05-05T22:04:15.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6e/087067d6ee22bd01bfba9fb1f32ce98c24ae2bcbab53bd2fbf8f7a80fe9e/ujson-5.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ea07e29a45d199f926aadf93a9974128438c01b83141fba32477c0ee604b33", size = 1091425, upload-time = "2026-05-05T22:04:17.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/d2/28938574b766980f873b68962abb4c68a944d939446768982934ad3bcd93/ujson-5.12.1-cp314-cp314-win32.whl", hash = "sha256:c8e626b6bc9bdd2e8f7393b7d99f3daa2ca4022e6203662e70de7bb3604b21b9", size = 42334, upload-time = "2026-05-05T22:04:19.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b0/0af30bf65d96b73c28054b344ebbe24bc96780ae8a7f2973f5dad979510a/ujson-5.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:c6d3bdd020333688ee60559437021ed68a98a28fdd609b5af16de5dd58f90cba", size = 46586, upload-time = "2026-05-05T22:04:21.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/3b/0ee2555823724e60cc847c715c299f5792aa444bdde69c51d4aa42d885c2/ujson-5.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:e3c9c894971f4ada3ded16a804ed4640e1f2b3e5239beaeec7c48296f39f4232", size = 41178, upload-time = "2026-05-05T22:04:22.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/3d/7547835cd0b7fa22eb1122702f81b2403c38a0027a2cc0d75acc449a4a66/ujson-5.12.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:49dd9c378e1c8e676785ff2b62cb490074229f15ab54abf45b623713cb2c36b5", size = 58565, upload-time = "2026-05-05T22:04:23.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6a/1784e0b24aab50623eb47b2f7a8dc22c9d809d798854d2568a9cb7c3560f/ujson-5.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d8827904358d7da59ccf2e1fd8de59e78248036d17fecc0462e62c6721f1102", size = 56157, upload-time = "2026-05-05T22:04:25.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2d/2c1b24df24eee309047d81460c3a1acf0d047207327edc6f3cab8a614985/ujson-5.12.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc26caebea90425662ef0b979f945f6ac832651881107d6ec9a3c4d4a4ba929c", size = 60288, upload-time = "2026-05-05T22:04:26.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/14/c0c603e3dff2ef98f7deee2df7795e6055abbc5825c6ef530024b3b06a15/ujson-5.12.1-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:45022aae09ac3d45bda6fbfc631088d1aff9a0465542d40bd6d295ced378c430", size = 62302, upload-time = "2026-05-05T22:04:27.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/0d/889bbc044561d9adc9bf413620fbd9878f352c9fd36da829d319bca2f5ad/ujson-5.12.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b22aa0f644516d3d5b29464949e4b23fe784f84b4a1030ab9ac3cb42aaedabb1", size = 59784, upload-time = "2026-05-05T22:04:28.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/35/3b1d8ff8cd6dc048f5c495af6ee6ded43055562610a7e9b78b438dc6421e/ujson-5.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7dc5cf44ea42365cd1b66e6ed3fc6ca040c86587b024a6659b98e99d31cff2cd", size = 1039759, upload-time = "2026-05-05T22:04:30.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/d8/3c66cdf839420a6da2d6140a54a882c15efd135bcced103bd4473d577636/ujson-5.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8df5d984ff4ac1ef292d70f30da03417038a7e1e0bc272d28ca9d34f02f41682", size = 1199121, upload-time = "2026-05-05T22:04:31.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/51/c3d1b94a4ad27dc7532e9f7d00b869463157cede2295ba6d57566afeb8cd/ujson-5.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:485f0182a0c0b54c304061cdc826d8343ce595c4055f7a24e72772a8520e5f7b", size = 1092085, upload-time = "2026-05-05T22:04:33.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/52/4d4a6e78290a5eef3f576f6d281e6355535db903a08483fd1bb393bf8cb9/ujson-5.12.1-cp314-cp314t-win32.whl", hash = "sha256:4e12ca368b397aed7fa1eec534ea1ba8d94977b376f9df3e93ae1acfd004ec40", size = 43243, upload-time = "2026-05-05T22:04:35.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/c8/849366785de52b513e5fc89d7aea0b531e71bb5641407cbdfdf47a99ede8/ujson-5.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:cec6b9b539539affc1f01a795c99574592a635ce22331b64f2b42e0af570659e", size = 47662, upload-time = "2026-05-05T22:04:37.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/46/36a67f5a531a15308124786f3e2b7b96414b9d23dbcdc2a182dd3ffa2e1d/ujson-5.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:696224d4cfb8883fa5c0285dff31e5ce924704dd9ccd38e9ea8b5bf4a42b12fc", size = 41680, upload-time = "2026-05-05T22:04:39.083Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -3,6 +3,27 @@
|
||||
This is the default UI for the authentik server. The documentation is going to be a little sparse
|
||||
for awhile, but at least let's get started.
|
||||
|
||||
# Setup
|
||||
|
||||
Install dependencies from the repo root with `make node-install` (or `make install` for the full
|
||||
Python + web + docs bootstrap). This wraps `npm ci` and explicitly rebuilds the small set of
|
||||
packages whose install scripts are required for the toolchain to function — currently `esbuild`,
|
||||
`chromedriver`, `tree-sitter`, and `tree-sitter-json`.
|
||||
|
||||
The repo-root `.npmrc` sets `ignore-scripts=true` to neutralize the dominant npm supply-chain
|
||||
attack vector. As a side effect, running `npm ci` directly in this directory will install
|
||||
dependencies but skip those rebuilds, leaving `esbuild` and `chromedriver` in a non-functional
|
||||
state. If you bypass `make`, run the rebuild step yourself:
|
||||
|
||||
```bash
|
||||
npm rebuild --ignore-scripts=false --foreground-scripts \
|
||||
esbuild chromedriver tree-sitter tree-sitter-json
|
||||
```
|
||||
|
||||
New dependencies that ship install scripts must be audited and added to `TRUSTED_INSTALL_SCRIPTS`
|
||||
in the repo-root `Makefile`. Each entry is arbitrary code that runs at install time, so the list
|
||||
is intentionally small.
|
||||
|
||||
# The Theory of the authentik UI
|
||||
|
||||
In Peter Naur's 1985 essay [Programming as Theory
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 12 KiB |
@@ -1 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 144.29"><defs><style>.cls-1{fill:#fd4b2d;}</style></defs><path class="cls-1" d="M106,41.08h25.39v101.2H106v-10.7a50,50,0,0,1-14.92,10.19,41.84,41.84,0,0,1-16.21,3.11q-19.61,0-33.91-15.21T26.64,91.86q0-23.43,13.85-38.41t33.63-15a42.78,42.78,0,0,1,17.09,3.44A46.82,46.82,0,0,1,106,52.24ZM79.29,61.91a25.65,25.65,0,0,0-19.56,8.33q-7.78,8.33-7.79,21.34t7.93,21.58a25.66,25.66,0,0,0,19.51,8.47,26.15,26.15,0,0,0,19.84-8.33q7.88-8.33,7.88-21.81,0-13.2-7.88-21.39T79.29,61.91Z"/><path class="cls-1" d="M168.39,41.08h25.67V89.82q0,14.22,2,19.76a17.24,17.24,0,0,0,6.29,8.61A18.06,18.06,0,0,0,213,121.26a18.6,18.6,0,0,0,10.77-3,17.7,17.7,0,0,0,6.57-8.88q1.59-4.36,1.59-18.7V41.08h25.39V84q0,26.51-4.18,36.27a39.6,39.6,0,0,1-15.07,18.28q-10,6.38-25.3,6.37-16.65,0-26.93-7.44T171.36,116.7q-3-9.21-3-33.49Z"/><path class="cls-1" d="M297.3,3.78h25.39v37.3h15.07V62.93H322.69v79.35H297.3V62.93h-13V41.08h13Z"/><path class="cls-1" d="M362.86,2h25.21v49.3a57.74,57.74,0,0,1,15-9.63,38.56,38.56,0,0,1,15.25-3.21,34.36,34.36,0,0,1,25.39,10.42q8.83,9,8.84,26.51v66.88h-25V97.91q0-17.58-1.68-23.81t-5.71-9.3a16.07,16.07,0,0,0-10-3.07,18.85,18.85,0,0,0-13.26,5.11q-5.53,5.11-7.67,14-1.12,4.56-1.12,20.84v40.65H362.86Z"/><path class="cls-1" d="M589.91,99H508.33q1.77,10.78,9.44,17.16t19.58,6.37a33.86,33.86,0,0,0,24.46-10l21.4,10a50.54,50.54,0,0,1-19.16,16.79q-11.16,5.44-26.51,5.44-23.82,0-38.79-15t-15-37.63q0-23.16,14.93-38.46t37.44-15.3q23.91,0,38.88,15.3t15,40.42Zm-25.4-20a25.48,25.48,0,0,0-9.92-13.77A28.81,28.81,0,0,0,537.4,60a30.42,30.42,0,0,0-18.64,5.95q-5,3.72-9.31,13.12Z"/><path class="cls-1" d="M621.89,41.08h25.39V51.45q8.64-7.29,15.65-10.13a37.82,37.82,0,0,1,14.35-2.85A34.77,34.77,0,0,1,702.83,49q8.82,8.94,8.82,26.42v66.88H686.54V98q0-18.12-1.63-24.06a16.44,16.44,0,0,0-5.66-9.06,15.8,15.8,0,0,0-10-3.11,18.73,18.73,0,0,0-13.23,5.15Q650.53,72,648.4,81.14q-1.12,4.74-1.12,20.54v40.6H621.89Z"/><path class="cls-1" d="M750.71,3.78H776.1v37.3h15.07V62.93H776.1v79.35H750.71V62.93h-13V41.08h13Z"/><path class="cls-1" d="M826.09-.6a15.55,15.55,0,0,1,11.45,4.84A16.08,16.08,0,0,1,842.31,16a15.87,15.87,0,0,1-4.72,11.58,15.34,15.34,0,0,1-11.32,4.79,15.6,15.6,0,0,1-11.55-4.88A16.35,16.35,0,0,1,810,15.59a15.57,15.57,0,0,1,4.73-11.44A15.53,15.53,0,0,1,826.09-.6Z"/><rect class="cls-1" x="813.39" y="41.08" width="25.39" height="101.2"/><path class="cls-1" d="M873.47,2h25.39V82.8l37.39-41.72h31.89l-43.59,48.5,48.81,52.7H941.83l-43-46.64v46.64H873.47Z"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3064.87 487.37"><defs><symbol id="a" viewBox="0 0 2865.3 437.72"><g style="isolation:isolate;"><path d="M238.73,125.38h76.4v304.5h-76.4v-32.18c-14.91,14.18-29.87,24.4-44.87,30.65-15,6.25-31.26,9.37-48.78,9.37-39.32,0-73.33-15.25-102.04-45.76C14.35,361.45,0,323.53,0,278.19s13.89-85.54,41.65-115.58c27.77-30.04,61.5-45.06,101.19-45.06,18.26,0,35.4,3.45,51.43,10.35,16.03,6.91,30.84,17.26,44.45,31.07v-33.58ZM158.41,188.07c-23.62,0-43.24,8.35-58.86,25.05-15.62,16.7-23.43,38.11-23.43,64.23s7.95,47.96,23.84,64.93c15.9,16.98,35.47,25.47,58.72,25.47s43.89-8.35,59.69-25.05c15.8-16.7,23.71-38.57,23.71-65.63s-7.9-47.95-23.71-64.37c-15.81-16.42-35.8-24.63-59.97-24.63Z" style="fill:#fd4b2d;"/><path d="M403.16,125.38h77.24v146.65c0,28.55,1.96,48.37,5.89,59.47,3.93,11.1,10.24,19.73,18.94,25.89,8.69,6.16,19.4,9.24,32.12,9.24s23.52-3.03,32.4-9.1c8.88-6.06,15.47-14.97,19.78-26.73,3.18-8.77,4.77-27.52,4.77-56.25V125.38h76.41v129.02c0,53.18-4.2,89.56-12.59,109.15-10.26,23.88-25.38,42.22-45.34,54.99-19.97,12.78-45.34,19.17-76.13,19.17-33.4,0-60.41-7.46-81.02-22.39-20.62-14.92-35.13-35.73-43.52-62.41-5.97-18.47-8.96-52.06-8.96-100.75v-126.78Z" style="fill:#fd4b2d;"/><path d="M796.76,13.15h76.41v112.23h45.34v65.77h-45.34v238.73h-76.41v-238.73h-39.18v-65.77h39.18V13.15Z" style="fill:#fd4b2d;"/><path d="M999.76,7.84h75.85v148.33c14.93-12.88,29.95-22.53,45.06-28.97,15.11-6.44,30.41-9.65,45.9-9.65,30.23,0,55.7,10.45,76.41,31.34,17.73,18.1,26.59,44.69,26.59,79.76v201.23h-75.29v-133.5c0-35.27-1.68-59.15-5.04-71.65-3.36-12.5-9.09-21.83-17.21-27.99-8.12-6.15-18.15-9.23-30.09-9.23-15.49,0-28.78,5.13-39.88,15.39-11.11,10.26-18.8,24.26-23.09,41.98-2.24,9.14-3.36,30.04-3.36,62.69v122.3h-75.85V7.84Z" style="fill:#fd4b2d;"/><path d="M1688.63,299.74h-245.45c3.54,21.65,13.01,38.86,28.41,51.64,15.39,12.78,35.03,19.17,58.91,19.17,28.55,0,53.08-9.98,73.6-29.95l64.37,30.23c-16.05,22.77-35.26,39.6-57.65,50.52-22.39,10.91-48.98,16.37-79.76,16.37-47.77,0-86.67-15.06-116.71-45.2-30.04-30.13-45.06-67.87-45.06-113.21s14.97-85.03,44.92-115.73c29.95-30.69,67.49-46.04,112.65-46.04,47.95,0,86.95,15.35,116.99,46.04,30.04,30.69,45.06,71.23,45.06,121.61l-.28,14.55ZM1612.22,239.57c-5.05-16.98-15-30.79-29.86-41.42-14.86-10.63-32.1-15.95-51.72-15.95-21.3,0-40,5.98-56.07,17.91-10.09,7.47-19.44,20.62-28.03,39.46h165.68Z" style="fill:#fd4b2d;"/><path d="M1790.6,125.38h76.41v31.21c17.33-14.61,33.02-24.77,47.09-30.48,14.06-5.71,28.46-8.57,43.18-8.57,30.18,0,55.8,10.54,76.85,31.62,17.7,17.91,26.55,44.41,26.55,79.48v201.23h-75.57v-133.35c0-36.34-1.63-60.47-4.89-72.4-3.26-11.93-8.93-21.01-17.03-27.26-8.1-6.24-18.1-9.36-30.01-9.36-15.45,0-28.71,5.17-39.78,15.51-11.08,10.35-18.76,24.65-23.04,42.91-2.24,9.5-3.35,30.1-3.35,61.78v122.16h-76.41V125.38Z" style="fill:#fd4b2d;"/><path d="M2183.92,13.15h76.41v112.23h45.34v65.77h-45.34v238.73h-76.41v-238.73h-39.18v-65.77h39.18V13.15Z" style="fill:#fd4b2d;"/><path d="M2416.46,0c13.39,0,24.88,4.85,34.46,14.55,9.58,9.7,14.38,21.46,14.38,35.27s-4.75,25.24-14.24,34.84c-9.49,9.61-20.84,14.41-34.04,14.41s-25.16-4.9-34.75-14.69c-9.58-9.79-14.37-21.69-14.37-35.68s4.74-24.91,14.23-34.43c9.49-9.51,20.93-14.27,34.33-14.27ZM2378.26,125.38h76.41v304.5h-76.41V125.38Z" style="fill:#fd4b2d;"/><path d="M2564.75,7.84h76.41v243.09l112.51-125.54h95.96l-131.17,145.94,146.86,158.56h-94.85l-129.3-140.34v140.34h-76.41V7.84Z" style="fill:#fd4b2d;"/></g></symbol></defs><use width="2865.3" height="437.72" transform="translate(99.78 24.83)" xlink:href="#a"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 6.0 KiB |
@@ -1 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><defs><style>.cls-1{fill:#fd4b2d;}</style></defs><rect class="cls-1" x="546.66" y="275.34" width="34.99" height="99.97"/><rect class="cls-1" x="637.66" y="271.13" width="34.99" height="78.19"/><path class="cls-1" d="M127.64,385.31a127.57,127.57,0,0,0-112.13,66.9H74.82c26.27-22.67,64.42-29.28,92,0h62.8C205.11,419.06,168.36,385.31,127.64,385.31Z"/><path class="cls-1" d="M212.39,512.53C130.55,683.65-12.89,537.81,74.82,452.21H15.51C-31,533.33,33.3,642.73,127.64,640.24c73,0,133.2-108.3,133.2-127.46,0-8.47-11.78-34.33-31.2-60.57h-62.8C187.65,471.08,205.81,498.56,212.39,512.53Zm2.17-5h0Z"/><path class="cls-1" d="M999.94,274.11V725.89c0,86.58-70.42,157.06-157.05,157.06H776.22V729.12H457.88V883H391.22c-86.64,0-157.06-70.48-157.06-157.06V583.81H738.87V312.11H495.24V464.76H234.16V274.11a151.29,151.29,0,0,1,1.06-18,154.4,154.4,0,0,1,3.88-21.15c.58-2.23,1.23-4.46,1.88-6.64a13.66,13.66,0,0,1,.52-1.64c.36-1.12.71-2.17,1.06-3.23s.76-2.17,1.18-3.23c.47-1.23.88-2.41,1.35-3.58s1-2.35,1.47-3.53a159,159,0,0,1,14.27-26.49c.06-.06.12-.17.17-.23,1.41-2.06,2.88-4.11,4.41-6.17,1.29-1.7,2.58-3.35,3.88-5,1.52-1.82,3.11-3.7,4.69-5.46s3.12-3.47,4.76-5.11l.18-.18a36.53,36.53,0,0,1,2.64-2.64,159.75,159.75,0,0,1,18.68-15.63c1.76-1.29,3.64-2.52,5.52-3.76,2.11-1.35,4.23-2.64,6.4-3.93,4.11-2.41,8.28-4.64,12.63-6.64,1.35-.64,2.76-1.29,4.11-1.88a152.81,152.81,0,0,1,18.38-6.63c2.41-.71,4.82-1.35,7.29-1.94,1.17-.3,2.35-.59,3.58-.82a158.5,158.5,0,0,1,21.26-3.12l3.12-.17c.52,0,1-.06,1.52-.06,2.35-.12,4.76-.18,7.17-.18H842.89c2.4,0,4.81.06,7.16.18.53,0,1,.06,1.53.06l3.11.17A158.26,158.26,0,0,1,876,120.58c1.24.23,2.41.52,3.59.82,2.46.59,4.87,1.23,7.28,1.94A152.81,152.81,0,0,1,905.2,130c1.35.59,2.76,1.24,4.11,1.88,4.35,2,8.52,4.23,12.63,6.64,2.18,1.29,4.29,2.58,6.4,3.93,1.88,1.24,3.76,2.47,5.52,3.76a157.53,157.53,0,0,1,21.5,18.45c1.65,1.64,3.23,3.34,4.76,5.11s3.17,3.64,4.7,5.46c1.29,1.64,2.58,3.29,3.87,5,1.53,2.06,3,4.11,4.41,6.17.06.06.12.17.18.23a159.71,159.71,0,0,1,14.27,26.49c.47,1.18,1,2.35,1.47,3.53s.88,2.35,1.35,3.58c.41,1.06.82,2.11,1.17,3.23s.71,2.11,1.06,3.23a15.74,15.74,0,0,1,.53,1.64c.64,2.18,1.29,4.41,1.88,6.64a155.92,155.92,0,0,1,3.87,21.15A151.29,151.29,0,0,1,999.94,274.11Z"/><path class="cls-1" d="M973.27,186.59H260.84A157.05,157.05,0,0,1,391.2,117.07H842.9A157.08,157.08,0,0,1,973.27,186.59Z"/><path class="cls-1" d="M998.94,256.1H235.16a155.35,155.35,0,0,1,25.68-69.51H973.27A155.34,155.34,0,0,1,998.94,256.1Z"/><path class="cls-1" d="M1000,274.11v51.51H738.87V312.11H495.24v13.51H234.1V274.11a153.41,153.41,0,0,1,1.06-18H998.94A151.29,151.29,0,0,1,1000,274.11Z"/><rect class="cls-1" x="234.1" y="325.62" width="261.13" height="69.54"/><rect class="cls-1" x="738.87" y="325.62" width="261.13" height="69.54"/><rect class="cls-1" x="234.1" y="395.16" width="261.13" height="69.48"/><rect class="cls-1" x="738.87" y="395.16" width="261.13" height="69.48"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="c" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1000 1000"><defs><symbol id="a" viewBox="0 0 998.94 763.82"><path d="M829.67,0h-425.28c-93.1,0-169.27,76.17-169.27,169.27v425.28c0,93.1,76.17,169.27,169.27,169.27h50.18v-165.68h324.96v165.68h50.14c93.1,0,169.27-76.17,169.27-169.27V169.27C998.94,76.17,922.77,0,829.67,0ZM755.98,463.53H235.4v-114.49h268.96v-158.97h43.68v94.7h25.61v-94.7h30.88v69.64h25.61v-69.64h30.88v116.35h25.61v-116.35h43.68v158.97h25.69v114.49Z" style="fill:#fd4b2d;"/><g id="b"><path d="M237.36,342.19h-.02c-25.34-34.27-63.32-69.15-105.42-69.15-48.4.03-92.89,26.58-115.91,69.15-48.08,83.85,18.39,196.94,115.91,194.36,75.46,0,137.69-111.95,137.69-131.75,0-8.76-12.18-35.49-32.25-62.61ZM77.32,342.19c27.16-23.43,66.59-30.27,95.1,0h.02c21.51,19.51,40.28,47.91,47.08,62.35-84.6,176.88-232.87,26.13-142.2-62.35Z" style="fill:#fd4b2d;"/></g></symbol></defs><use width="998.94" height="763.82" transform="translate(1 117.03)" xlink:href="#a"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 17 KiB |
@@ -1 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 994.71 151.65"><defs><style>.cls-1{fill:#fd4b2d;}</style></defs><path class="cls-1" d="M284.72,50.4H305.5v82.84H284.72v-8.76a40.79,40.79,0,0,1-12.21,8.34,34.14,34.14,0,0,1-13.27,2.55q-16.05,0-27.76-12.45T219.77,92q0-19.18,11.33-31.45t27.53-12.26a34.94,34.94,0,0,1,14,2.82,38.32,38.32,0,0,1,12.1,8.45ZM262.87,67.45a21,21,0,0,0-16,6.82q-6.37,6.81-6.38,17.47T247,109.4a21,21,0,0,0,16,6.93,21.42,21.42,0,0,0,16.24-6.81q6.45-6.81,6.45-17.86,0-10.8-6.45-17.51A21.71,21.71,0,0,0,262.87,67.45Z"/><path class="cls-1" d="M335.8,50.4h21V90.29q0,11.65,1.6,16.18a14.16,14.16,0,0,0,5.16,7,14.76,14.76,0,0,0,8.74,2.51,15.25,15.25,0,0,0,8.81-2.48,14.49,14.49,0,0,0,5.38-7.27q1.31-3.57,1.3-15.3V50.4h20.79V85.5q0,21.69-3.43,29.69a32.32,32.32,0,0,1-12.33,15q-8.16,5.22-20.71,5.22-13.64,0-22.05-6.09a32.2,32.2,0,0,1-11.84-17q-2.43-7.55-2.43-27.41Z"/><path class="cls-1" d="M441.32,19.86H462.1V50.4h12.34V68.29H462.1v65H441.32V68.29H430.66V50.4h10.66Z"/><path class="cls-1" d="M495,18.42h20.63V58.77a47.41,47.41,0,0,1,12.26-7.88,31.62,31.62,0,0,1,12.49-2.63,28.13,28.13,0,0,1,20.78,8.53q7.23,7.4,7.24,21.7v54.75H547.9V96.92q0-14.4-1.37-19.49a13.6,13.6,0,0,0-4.68-7.62,13.19,13.19,0,0,0-8.18-2.51,15.43,15.43,0,0,0-10.85,4.19,22.14,22.14,0,0,0-6.28,11.42q-.91,3.72-.92,17v33.28H495Z"/><path class="cls-1" d="M680.84,97.83H614.06a22.25,22.25,0,0,0,7.73,14q6.29,5.22,16,5.21a27.7,27.7,0,0,0,20-8.14l17.51,8.22a41.31,41.31,0,0,1-15.68,13.74q-9.13,4.46-21.7,4.46-19.5,0-31.75-12.3T594,92.27q0-19,12.22-31.48t30.65-12.53q19.56,0,31.82,12.53t12.26,33.08ZM660.05,81.46a20.87,20.87,0,0,0-8.12-11.27,23.61,23.61,0,0,0-14.08-4.34,24.88,24.88,0,0,0-15.25,4.88q-4.11,3-7.62,10.73Z"/><path class="cls-1" d="M707,50.4H727.8v8.49a50.15,50.15,0,0,1,12.81-8.3,31.08,31.08,0,0,1,11.75-2.33,28.44,28.44,0,0,1,20.91,8.61q7.22,7.31,7.22,21.62v54.75H759.93V97q0-14.83-1.33-19.7A13.48,13.48,0,0,0,754,69.85a13,13,0,0,0-8.16-2.55A15.32,15.32,0,0,0,735,71.52a22.6,22.6,0,0,0-6.27,11.67q-.9,3.89-.91,16.81v33.24H707Z"/><path class="cls-1" d="M812.46,19.86h20.79V50.4h12.33V68.29H833.25v65H812.46V68.29H801.8V50.4h10.66Z"/><path class="cls-1" d="M874.16,16.29a12.74,12.74,0,0,1,9.38,3.95,13.18,13.18,0,0,1,3.91,9.6,13,13,0,0,1-3.87,9.48,12.6,12.6,0,0,1-9.27,3.92,12.73,12.73,0,0,1-9.45-4A13.39,13.39,0,0,1,861,29.53a12.78,12.78,0,0,1,3.87-9.36A12.71,12.71,0,0,1,874.16,16.29Z"/><rect class="cls-1" x="863.77" y="50.4" width="20.79" height="82.84"/><path class="cls-1" d="M913,18.42h20.78V84.55L964.34,50.4h26.11L954.76,90.1l40,43.14h-25.8L933.73,95.06v38.18H913Z"/><rect class="cls-1" x="107.1" y="34.93" width="6.37" height="18.2"/><rect class="cls-1" x="123.67" y="34.16" width="6.37" height="14.23"/><path class="cls-1" d="M30.83,55A23.23,23.23,0,0,0,10.41,67.13h10.8C26,63,32.94,61.8,38,67.13H49.39C44.93,61.09,38.24,55,30.83,55Z"/><path class="cls-1" d="M46.25,78.11c-14.89,31.15-41,4.6-25-11H10.41c-8.47,14.76,3.24,34.68,20.42,34.23,13.28,0,24.24-19.72,24.24-23.21,0-1.54-2.14-6.25-5.68-11H38A40.52,40.52,0,0,1,46.25,78.11Zm.4-.91Z"/><path class="cls-1" d="M189.62,34.71V117A28.62,28.62,0,0,1,161,145.54H148.89v-28H90.94v28H78.81A28.62,28.62,0,0,1,50.22,117V91.08h91.87V41.62H97.74V69.41H50.22V34.71a27.43,27.43,0,0,1,.19-3.29,27.09,27.09,0,0,1,.71-3.84c.1-.41.22-.82.34-1.21a2.13,2.13,0,0,1,.09-.3c.07-.21.13-.4.2-.59s.14-.4.21-.59.16-.44.25-.65.18-.43.26-.64a29.35,29.35,0,0,1,2.6-4.82l0-.05c.26-.37.53-.75.81-1.12s.47-.61.7-.91.57-.67.86-1,.56-.63.86-.93l0,0a4.53,4.53,0,0,1,.49-.49,29.23,29.23,0,0,1,3.4-2.84c.32-.24.66-.46,1-.68s.77-.49,1.17-.72a23.78,23.78,0,0,1,2.29-1.21l.75-.34a27.84,27.84,0,0,1,3.35-1.21c.44-.13.88-.24,1.33-.35a6.19,6.19,0,0,1,.65-.15,28.86,28.86,0,0,1,3.87-.57l.56,0h.28c.43,0,.87,0,1.31,0H161c.43,0,.87,0,1.3,0h.28l.56,0a29.25,29.25,0,0,1,3.88.57c.22,0,.43.09.65.15.45.11.88.22,1.32.35a27.23,27.23,0,0,1,3.35,1.21l.75.34a25.19,25.19,0,0,1,2.3,1.21c.39.23.78.47,1.16.72s.69.44,1,.68a29.23,29.23,0,0,1,3.91,3.36q.45.45.87.93c.29.32.57.66.85,1l.71.91c.28.37.54.75.8,1.12l0,.05a28.61,28.61,0,0,1,2.6,4.82l.27.64.24.65c.08.19.15.39.22.59l.19.59c0,.09.06.19.1.3.11.39.23.8.34,1.21a28.56,28.56,0,0,1,.7,3.84A27.42,27.42,0,0,1,189.62,34.71Z"/><path class="cls-1" d="M184.76,18.78H55.07A28.59,28.59,0,0,1,78.8,6.12H161A28.59,28.59,0,0,1,184.76,18.78Z"/><path class="cls-1" d="M189.43,31.43H50.4a28.29,28.29,0,0,1,4.67-12.65H184.76A28.17,28.17,0,0,1,189.43,31.43Z"/><path class="cls-1" d="M189.63,34.71v9.37H142.09V41.62H97.74v2.46H50.21V34.71a27.43,27.43,0,0,1,.19-3.29h139A27.42,27.42,0,0,1,189.63,34.71Z"/><rect class="cls-1" x="50.21" y="44.08" width="47.54" height="12.66"/><rect class="cls-1" x="142.09" y="44.08" width="47.54" height="12.66"/><rect class="cls-1" x="50.21" y="56.74" width="47.54" height="12.65"/><rect class="cls-1" x="142.09" y="56.74" width="47.54" height="12.65"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="i" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3767.3 592.89"><defs><symbol id="a" viewBox="0 0 998.94 763.82"><path d="M829.67,0h-425.28c-93.1,0-169.27,76.17-169.27,169.27v425.28c0,93.1,76.17,169.27,169.27,169.27h50.18v-165.68h324.96v165.68h50.14c93.1,0,169.27-76.17,169.27-169.27V169.27C998.94,76.17,922.77,0,829.67,0ZM755.98,463.53H235.4v-114.49h268.96v-158.97h43.68v94.7h25.61v-94.7h30.88v69.64h25.61v-69.64h30.88v116.35h25.61v-116.35h43.68v158.97h25.69v114.49Z" style="fill:#fd4b2d;"/><g id="b"><path d="M237.36,342.19h-.02c-25.34-34.27-63.32-69.15-105.42-69.15-48.4.03-92.89,26.58-115.91,69.15-48.08,83.85,18.39,196.94,115.91,194.36,75.46,0,137.69-111.95,137.69-131.75,0-8.76-12.18-35.49-32.25-62.61ZM77.32,342.19c27.16-23.43,66.59-30.27,95.1,0h.02c21.51,19.51,40.28,47.91,47.08,62.35-84.6,176.88-232.87,26.13-142.2-62.35Z" style="fill:#fd4b2d;"/></g></symbol><symbol id="c" viewBox="0 0 2865.3 437.72"><g style="isolation:isolate;"><path d="M238.73,125.38h76.4v304.5h-76.4v-32.18c-14.91,14.18-29.87,24.4-44.87,30.65-15,6.25-31.26,9.37-48.78,9.37-39.32,0-73.33-15.25-102.04-45.76C14.35,361.45,0,323.53,0,278.19s13.89-85.54,41.65-115.58c27.77-30.04,61.5-45.06,101.19-45.06,18.26,0,35.4,3.45,51.43,10.35,16.03,6.91,30.84,17.26,44.45,31.07v-33.58ZM158.41,188.07c-23.62,0-43.24,8.35-58.86,25.05-15.62,16.7-23.43,38.11-23.43,64.23s7.95,47.96,23.84,64.93c15.9,16.98,35.47,25.47,58.72,25.47s43.89-8.35,59.69-25.05c15.8-16.7,23.71-38.57,23.71-65.63s-7.9-47.95-23.71-64.37c-15.81-16.42-35.8-24.63-59.97-24.63Z" style="fill:#fd4b2d;"/><path d="M403.16,125.38h77.24v146.65c0,28.55,1.96,48.37,5.89,59.47,3.93,11.1,10.24,19.73,18.94,25.89,8.69,6.16,19.4,9.24,32.12,9.24s23.52-3.03,32.4-9.1c8.88-6.06,15.47-14.97,19.78-26.73,3.18-8.77,4.77-27.52,4.77-56.25V125.38h76.41v129.02c0,53.18-4.2,89.56-12.59,109.15-10.26,23.88-25.38,42.22-45.34,54.99-19.97,12.78-45.34,19.17-76.13,19.17-33.4,0-60.41-7.46-81.02-22.39-20.62-14.92-35.13-35.73-43.52-62.41-5.97-18.47-8.96-52.06-8.96-100.75v-126.78Z" style="fill:#fd4b2d;"/><path d="M796.76,13.15h76.41v112.23h45.34v65.77h-45.34v238.73h-76.41v-238.73h-39.18v-65.77h39.18V13.15Z" style="fill:#fd4b2d;"/><path d="M999.76,7.84h75.85v148.33c14.93-12.88,29.95-22.53,45.06-28.97,15.11-6.44,30.41-9.65,45.9-9.65,30.23,0,55.7,10.45,76.41,31.34,17.73,18.1,26.59,44.69,26.59,79.76v201.23h-75.29v-133.5c0-35.27-1.68-59.15-5.04-71.65-3.36-12.5-9.09-21.83-17.21-27.99-8.12-6.15-18.15-9.23-30.09-9.23-15.49,0-28.78,5.13-39.88,15.39-11.11,10.26-18.8,24.26-23.09,41.98-2.24,9.14-3.36,30.04-3.36,62.69v122.3h-75.85V7.84Z" style="fill:#fd4b2d;"/><path d="M1688.63,299.74h-245.45c3.54,21.65,13.01,38.86,28.41,51.64,15.39,12.78,35.03,19.17,58.91,19.17,28.55,0,53.08-9.98,73.6-29.95l64.37,30.23c-16.05,22.77-35.26,39.6-57.65,50.52-22.39,10.91-48.98,16.37-79.76,16.37-47.77,0-86.67-15.06-116.71-45.2-30.04-30.13-45.06-67.87-45.06-113.21s14.97-85.03,44.92-115.73c29.95-30.69,67.49-46.04,112.65-46.04,47.95,0,86.95,15.35,116.99,46.04,30.04,30.69,45.06,71.23,45.06,121.61l-.28,14.55ZM1612.22,239.57c-5.05-16.98-15-30.79-29.86-41.42-14.86-10.63-32.1-15.95-51.72-15.95-21.3,0-40,5.98-56.07,17.91-10.09,7.47-19.44,20.62-28.03,39.46h165.68Z" style="fill:#fd4b2d;"/><path d="M1790.6,125.38h76.41v31.21c17.33-14.61,33.02-24.77,47.09-30.48,14.06-5.71,28.46-8.57,43.18-8.57,30.18,0,55.8,10.54,76.85,31.62,17.7,17.91,26.55,44.41,26.55,79.48v201.23h-75.57v-133.35c0-36.34-1.63-60.47-4.89-72.4-3.26-11.93-8.93-21.01-17.03-27.26-8.1-6.24-18.1-9.36-30.01-9.36-15.45,0-28.71,5.17-39.78,15.51-11.08,10.35-18.76,24.65-23.04,42.91-2.24,9.5-3.35,30.1-3.35,61.78v122.16h-76.41V125.38Z" style="fill:#fd4b2d;"/><path d="M2183.92,13.15h76.41v112.23h45.34v65.77h-45.34v238.73h-76.41v-238.73h-39.18v-65.77h39.18V13.15Z" style="fill:#fd4b2d;"/><path d="M2416.46,0c13.39,0,24.88,4.85,34.46,14.55,9.58,9.7,14.38,21.46,14.38,35.27s-4.75,25.24-14.24,34.84c-9.49,9.61-20.84,14.41-34.04,14.41s-25.16-4.9-34.75-14.69c-9.58-9.79-14.37-21.69-14.37-35.68s4.74-24.91,14.23-34.43c9.49-9.51,20.93-14.27,34.33-14.27ZM2378.26,125.38h76.41v304.5h-76.41V125.38Z" style="fill:#fd4b2d;"/><path d="M2564.75,7.84h76.41v243.09l112.51-125.54h95.96l-131.17,145.94,146.86,158.56h-94.85l-129.3-140.34v140.34h-76.41V7.84Z" style="fill:#fd4b2d;"/></g></symbol></defs><use width="998.94" height="763.82" transform="translate(28.54 36.14) scale(.68)" xlink:href="#a"/><use width="2865.3" height="437.72" transform="translate(802.22 67.81)" xlink:href="#c"/></svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 3.8 KiB |
@@ -63,7 +63,7 @@ const LogLevelColors = /** @type {const} */ ({
|
||||
* Creates a logger with the given prefix.
|
||||
*
|
||||
* @param {string} [prefix]
|
||||
* @param {...string} args
|
||||
* @param {...string[]} args
|
||||
* @returns {Logger}
|
||||
*
|
||||
*/
|
||||
|
||||
2008
web/package-lock.json
generated
@@ -127,14 +127,15 @@
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.5",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/node": "^25.6.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
||||
"@typescript-eslint/parser": "^8.57.2",
|
||||
"@typescript-eslint/utils": "^8.57.2",
|
||||
"@vitest/browser": "^4.1.5",
|
||||
"@vitest/browser-playwright": "^4.0.15",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260421.2",
|
||||
"@vitest/browser": "^4.1.6",
|
||||
"@vitest/browser-playwright": "^4.1.6",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"change-case": "^5.4.4",
|
||||
@@ -151,7 +152,7 @@
|
||||
"eslint-plugin-lit": "^2.2.1",
|
||||
"eslint-plugin-wc": "^3.1.0",
|
||||
"fuse.js": "^7.3.0",
|
||||
"globals": "^17.5.0",
|
||||
"globals": "^17.6.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^6.11.0",
|
||||
@@ -161,12 +162,12 @@
|
||||
"lit-element": "^4.2.2",
|
||||
"lit-html": "^3.3.2",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.14.0",
|
||||
"mermaid": "^11.15.0",
|
||||
"node-domexception": "^2025.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.2",
|
||||
"playwright": "^1.58.2",
|
||||
"playwright": "^1.60.0",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"pseudolocale": "^2.2.0",
|
||||
@@ -183,6 +184,7 @@
|
||||
"remark-mdx-frontmatter": "^5.2.0",
|
||||
"storybook": "^10.2.1",
|
||||
"style-mod": "^4.1.3",
|
||||
"stylelint": "^17.11.0",
|
||||
"trusted-types": "^2.0.0",
|
||||
"ts-pattern": "^5.9.0",
|
||||
"turnstile-types": "^1.2.3",
|
||||
@@ -190,8 +192,8 @@
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.1",
|
||||
"vite": "^8.0.12",
|
||||
"vitest": "^4.1.6",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
"yaml": "^2.8.4"
|
||||
@@ -202,8 +204,7 @@
|
||||
"@esbuild/linux-x64": "^0.28.0",
|
||||
"@rollup/rollup-darwin-arm64": "^4.57.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.57.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.57.1",
|
||||
"chromedriver": "^147.0.4"
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.57.1"
|
||||
},
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
@@ -260,10 +261,7 @@
|
||||
"command": "lit-analyzer src"
|
||||
},
|
||||
"lint:types": {
|
||||
"command": "tsc -p .",
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--max_old_space_size=8192"
|
||||
},
|
||||
"command": "tsgo -p .",
|
||||
"dependencies": [
|
||||
"build-locales"
|
||||
]
|
||||
@@ -292,10 +290,7 @@
|
||||
}
|
||||
},
|
||||
"tsc": {
|
||||
"command": "tsc -p .",
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--max_old_space_size=8192"
|
||||
},
|
||||
"command": "tsgo -p .",
|
||||
"dependencies": [
|
||||
"build-locales"
|
||||
]
|
||||
@@ -332,7 +327,8 @@
|
||||
"typescript": "$typescript"
|
||||
},
|
||||
"@mrmarble/djangoql-completion": {
|
||||
"lex": "$lex"
|
||||
"lex": "$lex",
|
||||
"lodash": "^4.18.1"
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"typescript": "$typescript"
|
||||
@@ -340,6 +336,9 @@
|
||||
"@typescript-eslint/parser": {
|
||||
"typescript": "$typescript"
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"typescript": "$typescript"
|
||||
},
|
||||
"@typescript-eslint/utils": {
|
||||
"typescript": "$typescript"
|
||||
},
|
||||
@@ -354,6 +353,9 @@
|
||||
},
|
||||
"typescript-eslint": {
|
||||
"typescript": "$typescript"
|
||||
},
|
||||
"wireit": {
|
||||
"brace-expansion": "^1.1.14"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@goauthentik/tsconfig": "^1.0.9",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/node": "^25.6.2",
|
||||
"@types/semver": "^7.7.1",
|
||||
"semver": "^7.7.4",
|
||||
"typescript": "^6.0.3"
|
||||
|
||||
2
web/packages/lex/index.js
vendored
@@ -25,7 +25,7 @@
|
||||
*
|
||||
* @callback LexerAction
|
||||
* @this {Lexer}
|
||||
* @param {...string} match
|
||||
* @param {...string[]} match
|
||||
* @returns {Token | Token[] | null | void}
|
||||
*/
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ import { ToggleUserActivationButton } from "#admin/users/UserActiveForm";
|
||||
import { UserForm } from "#admin/users/UserForm";
|
||||
import { UserImpersonateForm } from "#admin/users/UserImpersonateForm";
|
||||
|
||||
import { CapabilitiesEnum, CoreApi, ModelEnum, User } from "@goauthentik/api";
|
||||
import { CapabilitiesEnum, CoreApi, ModelEnum, User, UserTypeEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, html, PropertyValues, TemplateResult } from "lit";
|
||||
@@ -192,7 +192,10 @@ export class UserViewPage extends WithLicenseSummary(
|
||||
protected renderActionButtons(user: User) {
|
||||
const showImpersonate =
|
||||
this.can(CapabilitiesEnum.CanImpersonate) && user.pk !== this.currentUser?.pk;
|
||||
const showLockdown = this.hasEnterpriseLicense && user.pk !== this.currentUser?.pk;
|
||||
const showLockdown =
|
||||
this.hasEnterpriseLicense &&
|
||||
user.pk !== this.currentUser?.pk &&
|
||||
user.type !== UserTypeEnum.InternalServiceAccount;
|
||||
|
||||
const displayName = formatUserDisplayName(user);
|
||||
|
||||
|
||||
@@ -1,32 +1,73 @@
|
||||
import Style from "./ak-drawer.css";
|
||||
import AKDrawer from "./ak-drawer.styles";
|
||||
import { DrawerResizeController } from "./drawerResizeController";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { classList } from "#elements/directives/class-list";
|
||||
|
||||
import { html } from "lit";
|
||||
import { html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
||||
export class DrawerExpandRequest extends Event {
|
||||
static readonly eventName = "ak-drawer-expand-request";
|
||||
expanded: boolean | null = null;
|
||||
|
||||
export class Drawer extends AKElement {
|
||||
static readonly styles = [PFDrawer, Style];
|
||||
constructor(expanded: boolean | null = null) {
|
||||
super(DrawerExpandRequest.eventName, { bubbles: true, composed: true });
|
||||
this.expanded = expanded;
|
||||
}
|
||||
}
|
||||
|
||||
export class AkDrawer extends LitElement {
|
||||
static readonly styles = [AKDrawer];
|
||||
|
||||
@property({ type: Boolean })
|
||||
public resizable = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
public expanded = false;
|
||||
|
||||
render() {
|
||||
const open = [(this.open && "pf-m-expanded") || "pf-m-collapsed"];
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public resizing = false;
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
public width = "33";
|
||||
|
||||
private resize = new DrawerResizeController(this);
|
||||
|
||||
onDrawerRequest = (ev: DrawerExpandRequest) => {
|
||||
ev.stopPropagation();
|
||||
this.expanded = ev.expanded === null ? !this.expanded : ev.expanded;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(DrawerExpandRequest.eventName, this.onDrawerRequest);
|
||||
}
|
||||
|
||||
public override render() {
|
||||
return html`
|
||||
<div class="pf-c-page__drawer">
|
||||
<div class="pf-c-drawer ${classList(open)}" id="flow-drawer">
|
||||
<div class="pf-c-drawer__main">
|
||||
<div class="pf-c-drawer__content">
|
||||
<div class="pf-c-drawer__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="ak-v2-c-drawer" part="drawer">
|
||||
<div class="ak-v2-c-drawer__main" part="drawer-main">
|
||||
<div class="ak-v2-c-drawer__content" part="drawer-content">
|
||||
<div class="ak-v2-c-drawer__body" part="drawer-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="pf-c-drawer__panel pf-m-width-33">
|
||||
</div>
|
||||
<div class="ak-v2-c-drawer__panel" part="drawer-panel">
|
||||
${this.resizable
|
||||
? html` <div
|
||||
class="ak-v2-c-drawer__splitter"
|
||||
part="drawer-splitter"
|
||||
@mousedown=${this.resize.handleMouseDown}
|
||||
@keydown=${this.resize.handleKeyDown}
|
||||
@touchstart=${this.resize.handleTouchStart}
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ak-v2-c-drawer__splitter-handle"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="ak-v2-c-drawer__panel-main" part="drawer-panel-main">
|
||||
<slot name="panel"></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,4 +75,26 @@ export class Drawer extends AKElement {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public override updated(changed: PropertyValues<this>) {
|
||||
super.updated(changed);
|
||||
|
||||
// Simulate the behavior of summary/details, another disclosure pattern.
|
||||
const expanded = changed.get("expanded");
|
||||
if (expanded !== undefined) {
|
||||
const expandedMsg = (i: boolean) => (i ? "open" : "closed");
|
||||
this.dispatchEvent(
|
||||
new ToggleEvent("toggle", {
|
||||
newState: expandedMsg(this.expanded),
|
||||
oldState: expandedMsg(expanded),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
[DrawerExpandRequest.eventName]: DrawerExpandRequest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
slot {
|
||||
display: content;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--pf-c-drawer__panel--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
.pf-c-drawer {
|
||||
/* TODO: Revisit this after native <dialog> modals are implemented. */
|
||||
--pf-c-drawer__content--ZIndex: auto;
|
||||
}
|
||||
|
||||
.pf-c-drawer__body {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.pf-c-drawer__content {
|
||||
--pf-c-drawer__content--BackgroundColor: transparent;
|
||||
}
|
||||
|
||||
.pf-c-drawer {
|
||||
.pf-c-drawer__panel {
|
||||
background-color: var(--pf-c-drawer__panel--BackgroundColor);
|
||||
|
||||
transition-behavior: allow-discrete;
|
||||
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
|
||||
@media (width > 768px) {
|
||||
flex-flow: row;
|
||||
|
||||
.pf-c-drawer__panel_content {
|
||||
flex: 1 1 auto;
|
||||
max-width: 33dvw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
web/src/elements/ak-drawer/ak-drawer.root.css
Normal file
@@ -0,0 +1,141 @@
|
||||
/* ----------- CSS Custom Properties for DRAWER --------------------------- */
|
||||
|
||||
:root {
|
||||
--ak-v2-c-drawer__content--FlexBasis: 100%;
|
||||
--ak-v2-c-drawer__content--BackgroundColor: var(--ak-v2-global--ContentSurface);
|
||||
--ak-v2-c-drawer__content--ZIndex: var(--ak-v2-global--ZIndex--xs, auto);
|
||||
--ak-v2-c-drawer__panel--MinWidth: 50%;
|
||||
--ak-v2-c-drawer__panel--MaxHeight: auto;
|
||||
--ak-v2-c-drawer__panel--ZIndex: var(--ak-v2-global--ZIndex--sm);
|
||||
--ak-v2-c-drawer__panel--BackgroundColor: var(--ak-v2-global--ContentSurface);
|
||||
--ak-v2-c-drawer__panel--TransitionDuration: var(--ak-v2-global--TransitionDuration);
|
||||
--ak-v2-c-drawer__panel--TransitionProperty: margin, transform, box-shadow, flex-basis;
|
||||
--ak-v2-c-drawer__panel--FlexBasis: 100%;
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis--min: 1.5rem;
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 50%;
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis--max: 100%;
|
||||
--ak-v2-c-drawer__panel--xl--MinWidth: 28.125rem;
|
||||
--ak-v2-c-drawer__panel--xl--FlexBasis: 28.125rem;
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--md--MinHeight: 50%;
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--xl--MinHeight: 18.75rem;
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--xl--FlexBasis: 18.75rem;
|
||||
--ak-v2-c-drawer__panel--m-resizable--FlexDirection: row;
|
||||
--ak-v2-c-drawer__panel--m-resizable--md--FlexBasis--min: var(
|
||||
--ak-v2-c-drawer__splitter--m-vertical--Width
|
||||
);
|
||||
--ak-v2-c-drawer__panel--m-resizable--MinWidth: 1.5rem;
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--FlexDirection: column;
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--md--FlexBasis--min: 1.5rem;
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--MinHeight: 1.5rem;
|
||||
--ak-v2-c-drawer__splitter--Height: 0.5625rem;
|
||||
--ak-v2-c-drawer__splitter--Width: 100%;
|
||||
--ak-v2-c-drawer__splitter--BackgroundColor: var(--ak-v2-global--ContentSurface);
|
||||
--ak-v2-c-drawer__splitter--Cursor: row-resize;
|
||||
--ak-v2-c-drawer__splitter--m-vertical--Height: 100%;
|
||||
--ak-v2-c-drawer__splitter--m-vertical--Width: 0.5625rem;
|
||||
--ak-v2-c-drawer__splitter--m-vertical--Cursor: col-resize;
|
||||
--ak-v2-c-drawer--m-inline__splitter--focus--OutlineOffset: -0.0625rem;
|
||||
--ak-v2-c-drawer__splitter--after--BorderColor: var(--ak-v2-global--BorderColor--100);
|
||||
--ak-v2-c-drawer__splitter--after--border-width--base: var(--ak-v2-global--BorderWidth--sm);
|
||||
--ak-v2-c-drawer__splitter--after--BorderTopWidth: 0;
|
||||
--ak-v2-c-drawer__splitter--after--BorderRightWidth: var(
|
||||
--ak-v2-c-drawer__splitter--after--border-width--base
|
||||
);
|
||||
--ak-v2-c-drawer__splitter--after--BorderBottomWidth: 0;
|
||||
--ak-v2-c-drawer__splitter--after--BorderLeftWidth: 0;
|
||||
--ak-v2-c-drawer--m-panel-left__splitter--after--BorderLeftWidth: var(
|
||||
--ak-v2-c-drawer__splitter--after--border-width--base
|
||||
);
|
||||
--ak-v2-c-drawer--m-panel-bottom__splitter--after--BorderBottomWidth: var(
|
||||
--ak-v2-c-drawer__splitter--after--border-width--base
|
||||
);
|
||||
--ak-v2-c-drawer--m-inline__splitter--m-vertical--Width: 0.625rem;
|
||||
--ak-v2-c-drawer--m-inline__splitter-handle--Left: 50%;
|
||||
--ak-v2-c-drawer--m-inline__splitter--after--BorderRightWidth: var(
|
||||
--ak-v2-c-drawer__splitter--after--border-width--base
|
||||
);
|
||||
--ak-v2-c-drawer--m-inline__splitter--after--BorderLeftWidth: var(
|
||||
--ak-v2-c-drawer__splitter--after--border-width--base
|
||||
);
|
||||
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter--Height: 0.625rem;
|
||||
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter-handle--Top: 50%;
|
||||
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter--after--BorderTopWidth: var(
|
||||
--ak-v2-c-drawer__splitter--after--border-width--base
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--Top: 50%;
|
||||
--ak-v2-c-drawer__splitter-handle--Left: calc(
|
||||
50% - var(--ak-v2-c-drawer__splitter--after--border-width--base)
|
||||
);
|
||||
--ak-v2-c-drawer--m-panel-left__splitter-handle--Left: 50%;
|
||||
--ak-v2-c-drawer--m-panel-bottom__splitter-handle--Top: calc(
|
||||
50% - var(--ak-v2-c-drawer__splitter--after--border-width--base)
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderColor: var(--ak-v2-global--Color--200);
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderTopWidth: var(--ak-v2-global--BorderWidth--sm);
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderRightWidth: 0;
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderBottomWidth: var(
|
||||
--ak-v2-global--BorderWidth--sm
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderLeftWidth: 0;
|
||||
--ak-v2-c-drawer__splitter--hover__splitter-handle--after--BorderColor: var(
|
||||
--ak-v2-global--Color--100
|
||||
);
|
||||
--ak-v2-c-drawer__splitter--focus__splitter-handle--after--BorderColor: var(
|
||||
--ak-v2-global--Color--100
|
||||
);
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderTopWidth: 0;
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderRightWidth: var(
|
||||
--ak-v2-global--BorderWidth--sm
|
||||
);
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderBottomWidth: 0;
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderLeftWidth: var(
|
||||
--ak-v2-global--BorderWidth--sm
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--after--Width: 0.75rem;
|
||||
--ak-v2-c-drawer__splitter-handle--after--Height: 0.25rem;
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--Width: 0.25rem;
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--Height: 0.75rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
:root {
|
||||
--ak-v2-c-drawer__panel--MinWidth: var(--ak-v2-c-drawer__panel--xl--MinWidth);
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--ak-v2-c-drawer__panel--BoxShadow: none;
|
||||
--ak-v2-c-drawer--m-expanded--m-panel-bottom__panel--BoxShadow: var(
|
||||
--ak-v2-global--BoxShadow--lg-top
|
||||
);
|
||||
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: var(--ak-v2-global--BoxShadow--lg-left);
|
||||
}
|
||||
|
||||
:root {
|
||||
--ak-v2-c-drawer--m-expanded--m-panel-left__panel--BoxShadow: var(
|
||||
--ak-v2-global--BoxShadow--lg-right
|
||||
);
|
||||
}
|
||||
|
||||
:root {
|
||||
--ak-v2-c-drawer__panel--after--Width: var(--ak-v2-global--BorderWidth--sm);
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--after--Height: var(--ak-v2-global--BorderWidth--sm);
|
||||
--ak-v2-c-drawer__panel--after--BackgroundColor: transparent;
|
||||
--ak-v2-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor: var(
|
||||
--ak-v2-global--BorderColor--100
|
||||
);
|
||||
--ak-v2-c-drawer--m-inline__panel--PaddingLeft: var(--ak-v2-c-drawer__panel--after--Width);
|
||||
--ak-v2-c-drawer--m-panel-left--m-inline__panel--PaddingRight: var(
|
||||
--ak-v2-c-drawer__panel--after--Width
|
||||
);
|
||||
--ak-v2-c-drawer--m-panel-bottom--m-inline__panel--PaddingTop: var(
|
||||
--ak-v2-c-drawer__panel--after--Width
|
||||
);
|
||||
}
|
||||
|
||||
html[data-theme="dark"],
|
||||
.ak-t-dark,
|
||||
.pf-t-dark {
|
||||
--ak-v2-c-drawer__panel--BackgroundColor: var(--ak-v2-global--ContentSurface);
|
||||
--ak-v2-c-drawer__splitter--BackgroundColor: transparent;
|
||||
}
|
||||
151
web/src/elements/ak-drawer/ak-drawer.stories.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import "./ak-drawer";
|
||||
|
||||
import { DrawerExpandRequest } from "./ak-drawer.component";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/web-components-vite";
|
||||
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
const toggle = (e: Event) => {
|
||||
const button = e.target as HTMLButtonElement;
|
||||
button.dispatchEvent(new DrawerExpandRequest());
|
||||
};
|
||||
|
||||
const contentBlock = html`
|
||||
<div style="padding: 1rem;">
|
||||
<h2>Main Content</h2>
|
||||
<p><button @click=${toggle}>Toggle Drawer</button></p>
|
||||
<p>
|
||||
This is the drawer's main: fill it by inserting slotted content without a slot name.
|
||||
This is the part that stays visible most of the time.
|
||||
</p>
|
||||
<p>
|
||||
Macaroon lollipop croissant sweet biscuit croissant chocolate cake. Cake cake pastry
|
||||
soufflé pudding. Tiramisu lollipop chocolate cake toffee oat cake muffin topping tootsie
|
||||
roll. Carrot cake bonbon chupa chups sugar plum fruitcake. Brownie sweet halvah oat cake
|
||||
cheesecake topping chocolate. Wafer macaroon topping lollipop powder cupcake sugar plum
|
||||
donut. Muffin wafer icing danish jelly-o bonbon. Powder shortbread brownie caramels
|
||||
tootsie roll dragée liquorice. Cake lemon drops powder danish toffee.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const panelBlock = html`
|
||||
<style>
|
||||
[slot="panel"] {
|
||||
padding: 1rem;
|
||||
background-color: var(--pf-v5-global--BackgroundColor--200, #f0f0f0);
|
||||
}
|
||||
</style>
|
||||
<div slot="panel">
|
||||
<h3>Panel Content</h3>
|
||||
<p>This is the side panel. This is where you put the secondary information.</p>
|
||||
<ul>
|
||||
<li>
|
||||
Seasonal, steamed, con panna and rich ut aged cup decaffeinated single origin con
|
||||
panna bar
|
||||
</li>
|
||||
<li>Skinny mazagran whipped, black iced beans carajillo eu cream</li>
|
||||
<li>Americano pumpkin spice milk ristretto caffeine single shot</li>
|
||||
</ul>
|
||||
<p><button @click=${toggle}>Toggle Drawer</button></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
interface DrawerProps {
|
||||
expanded?: boolean;
|
||||
inline?: boolean;
|
||||
static?: boolean;
|
||||
resizable?: boolean;
|
||||
width?: string;
|
||||
position?: string;
|
||||
content?: TemplateResult;
|
||||
panel?: TemplateResult;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Components/Drawer",
|
||||
component: "ak-drawer",
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(story) =>
|
||||
html`<div style="min-height: 400px; border: 1px solid #d2d2d2; overflow: hidden;">
|
||||
${story()}
|
||||
</div>`,
|
||||
],
|
||||
argTypes: {
|
||||
expanded: { control: "boolean" },
|
||||
position: {
|
||||
control: { type: "select" },
|
||||
options: ["right", "left", "bottom"],
|
||||
},
|
||||
inline: { control: "boolean" },
|
||||
static: { control: "boolean" },
|
||||
resizable: { control: "boolean" },
|
||||
width: {
|
||||
control: { type: "select" },
|
||||
options: ["25", "33", "50", "66", "75", "100"],
|
||||
},
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj;
|
||||
|
||||
const Template: Story = {
|
||||
args: {
|
||||
expanded: false,
|
||||
inline: false,
|
||||
static: false,
|
||||
resizable: false,
|
||||
width: undefined,
|
||||
position: undefined,
|
||||
content: contentBlock,
|
||||
panel: panelBlock,
|
||||
},
|
||||
render: (args) => {
|
||||
return html` <ak-drawer
|
||||
?expanded=${args.expanded}
|
||||
?inline=${args.inline}
|
||||
?resizable=${args.resizable}
|
||||
position=${ifDefined(args.position)}
|
||||
width=${ifDefined(args.width)}
|
||||
>
|
||||
${args.content} ${args.panel}
|
||||
</ak-drawer>`;
|
||||
},
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => html` <ak-drawer> ${contentBlock} ${panelBlock} </ak-drawer> `,
|
||||
};
|
||||
|
||||
export const story = (args: DrawerProps = {}, name?: string): Story => ({
|
||||
...Template,
|
||||
...(name ? { name } : {}),
|
||||
args: {
|
||||
...Template.args,
|
||||
...args,
|
||||
},
|
||||
});
|
||||
|
||||
export const Expanded: Story = story({ expanded: true });
|
||||
|
||||
export const PanelLeft: Story = story({ expanded: true, position: "left" });
|
||||
|
||||
export const PanelBottom = story({ expanded: true, position: "bottom" });
|
||||
|
||||
export const Inline = story({ expanded: true, inline: true });
|
||||
|
||||
export const Static = story({ expanded: true, static: true });
|
||||
|
||||
export const Resizable = story({ expanded: true, resizable: true });
|
||||
|
||||
export const ResizableLeft = story({ expanded: true, resizable: true, position: "left" });
|
||||
|
||||
export const ResizableBottom = story({ expanded: true, resizable: true, position: "bottom" });
|
||||
|
||||
export const CustomWidth = story({ expanded: true, width: "33" });
|
||||
|
||||
export const ResponsiveWidth = story({ expanded: true, width: "75-on-xl" });
|
||||
914
web/src/elements/ak-drawer/ak-drawer.styles.ts
Normal file
@@ -0,0 +1,914 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:host([position="bottom"]) .ak-v2-c-drawer {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
slot {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
:host([inline]:not([no-border])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
|
||||
:host([inline]:not([resizable])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
|
||||
:host([static]:not([no-border])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
|
||||
:host([static]:not([resizable])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
padding-inline-start: var(--ak-v2-c-drawer--m-inline__panel--PaddingLeft);
|
||||
}
|
||||
|
||||
:host([position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
order: 0;
|
||||
margin-inline-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host([position="left"])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
:host([position="bottom"]) .ak-v2-c-drawer__main {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host(:not([inline], [static])) .ak-v2-c-drawer__main {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host(:not([inline], [static])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-block-end: 0;
|
||||
inset-inline-end: 0;
|
||||
max-width: var(--ak-v2-c-drawer__panel--FlexBasis);
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host(:not([inline], [static]))
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([expanded]:not([inline], [static])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([position="left"]:not([inline], [static]))
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
inset-inline-end: auto;
|
||||
inset-inline-start: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host([position="left"]:not([inline], [static]))
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([expanded][position="left"]:not([inline], [static]))
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([position="bottom"]:not([inline], [static]))
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: auto;
|
||||
inset-block-end: 0;
|
||||
max-width: none;
|
||||
max-height: var(--ak-v2-c-drawer__panel--FlexBasis);
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
:host([position="bottom"][expanded]:not([inline], [static]))
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
:host([class*="pf-m-resizing"]) {
|
||||
--ak-v2-c-drawer__panel--TransitionProperty: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host([class*="pf-m-resizing"]) .ak-v2-c-drawer__splitter {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__content,
|
||||
.ak-v2-c-drawer__panel,
|
||||
.ak-v2-c-drawer__panel-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
overflow: auto;
|
||||
--ak-v2-c-drawer__content--BackgroundColor: transparent;
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__content {
|
||||
z-index: var(--ak-v2-c-drawer__content--ZIndex);
|
||||
flex-basis: var(--ak-v2-c-drawer__content--FlexBasis);
|
||||
order: 0;
|
||||
background-color: var(--ak-v2-c-drawer__content--BackgroundColor);
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__panel {
|
||||
position: relative;
|
||||
z-index: var(--ak-v2-c-drawer__panel--ZIndex);
|
||||
flex-basis: var(--ak-v2-c-drawer__panel--FlexBasis);
|
||||
order: 1;
|
||||
max-height: var(--ak-v2-c-drawer__panel--MaxHeight);
|
||||
gap: var(--ak-v2-global--spacer--sm);
|
||||
overflow: auto;
|
||||
background-color: var(--ak-v2-c-drawer__panel--BackgroundColor);
|
||||
box-shadow: var(--ak-v2-c-drawer__panel--BoxShadow);
|
||||
transition-duration: var(--ak-v2-c-drawer__panel--TransitionDuration);
|
||||
transition-property: var(--ak-v2-c-drawer__panel--TransitionProperty);
|
||||
transition-behavior: allow-discrete;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__panel::after {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
width: var(--ak-v2-c-drawer__panel--after--Width);
|
||||
height: 100%;
|
||||
content: "";
|
||||
background-color: var(--ak-v2-c-drawer__panel--after--BackgroundColor);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.ak-v2-c-drawer__panel {
|
||||
--ak-v2-c-drawer__panel--FlexBasis: max(
|
||||
var(--ak-v2-c-drawer__panel--md--FlexBasis--min),
|
||||
min(
|
||||
var(--ak-v2-c-drawer__panel--md--FlexBasis),
|
||||
var(--ak-v2-c-drawer__panel--md--FlexBasis--max)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
:host(:not([width])) .ak-v2-c-drawer__panel {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: var(--ak-v2-c-drawer__panel--xl--FlexBasis);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
:host([position="bottom"]) .ak-v2-c-drawer__panel {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: var(
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--xl--FlexBasis
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
:where(
|
||||
:host(:not([position])),
|
||||
:host([position="left"]),
|
||||
:host([position="right"]),
|
||||
:host([position="start"]),
|
||||
:host([position="end"])
|
||||
)
|
||||
.ak-v2-c-drawer__splitter {
|
||||
--ak-v2-c-drawer__splitter--Height: var(--ak-v2-c-drawer__splitter--m-vertical--Height);
|
||||
--ak-v2-c-drawer__splitter--Width: var(--ak-v2-c-drawer__splitter--m-vertical--Width);
|
||||
--ak-v2-c-drawer__splitter--Cursor: var(--ak-v2-c-drawer__splitter--m-vertical--Cursor);
|
||||
--ak-v2-c-drawer__splitter-handle--after--Width: var(
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--Width
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--after--Height: var(
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--Height
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderTopWidth: var(
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderTopWidth
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderRightWidth: var(
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderRightWidth
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderBottomWidth: var(
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderBottomWidth
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderLeftWidth: var(
|
||||
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderLeftWidth
|
||||
);
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__splitter {
|
||||
position: relative;
|
||||
display: none;
|
||||
width: var(--ak-v2-c-drawer__splitter--Width);
|
||||
height: var(--ak-v2-c-drawer__splitter--Height);
|
||||
cursor: var(--ak-v2-c-drawer__splitter--Cursor);
|
||||
background-color: var(--ak-v2-c-drawer__splitter--BackgroundColor);
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__splitter:hover {
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderColor: var(
|
||||
--ak-v2-c-drawer__splitter--hover__splitter-handle--after--BorderColor
|
||||
);
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__splitter:focus {
|
||||
--ak-v2-c-drawer__splitter-handle--after--BorderColor: var(
|
||||
--ak-v2-c-drawer__splitter--focus__splitter-handle--after--BorderColor
|
||||
);
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__splitter::after {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-block-end: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
content: "";
|
||||
border: solid var(--ak-v2-c-drawer__splitter--after--BorderColor);
|
||||
border-block-start-width: var(--ak-v2-c-drawer__splitter--after--BorderTopWidth);
|
||||
border-block-end-width: var(--ak-v2-c-drawer__splitter--after--BorderBottomWidth);
|
||||
border-inline-start-width: var(--ak-v2-c-drawer__splitter--after--BorderLeftWidth);
|
||||
border-inline-end-width: var(--ak-v2-c-drawer__splitter--after--BorderRightWidth);
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__splitter-handle {
|
||||
position: absolute;
|
||||
inset-block-start: var(--ak-v2-c-drawer__splitter-handle--Top);
|
||||
inset-inline-start: var(--ak-v2-c-drawer__splitter-handle--Left);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"]) .ak-v2-c-drawer__splitter-handle {
|
||||
transform: translate(calc(-50% * var(--ak-v2-global--inverse--multiplier)), -50%);
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__splitter-handle::after {
|
||||
display: block;
|
||||
width: var(--ak-v2-c-drawer__splitter-handle--after--Width);
|
||||
height: var(--ak-v2-c-drawer__splitter-handle--after--Height);
|
||||
content: "";
|
||||
border-color: var(--ak-v2-c-drawer__splitter-handle--after--BorderColor);
|
||||
border-style: solid;
|
||||
border-block-start-width: var(--ak-v2-c-drawer__splitter-handle--after--BorderTopWidth);
|
||||
border-block-end-width: var(--ak-v2-c-drawer__splitter-handle--after--BorderBottomWidth);
|
||||
border-inline-start-width: var(--ak-v2-c-drawer__splitter-handle--after--BorderLeftWidth);
|
||||
border-inline-end-width: var(--ak-v2-c-drawer__splitter-handle--after--BorderRightWidth);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
:host {
|
||||
min-width: var(--ak-v2-c-drawer__panel--MinWidth);
|
||||
}
|
||||
|
||||
:host([expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
box-shadow: var(--ak-v2-c-drawer--m-expanded__panel--BoxShadow);
|
||||
}
|
||||
|
||||
:host([expanded][resizable]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis--min: var(
|
||||
--ak-v2-c-drawer__panel--m-resizable--md--FlexBasis--min
|
||||
);
|
||||
flex-direction: var(--ak-v2-c-drawer__panel--m-resizable--FlexDirection);
|
||||
min-width: var(--ak-v2-c-drawer__panel--m-resizable--MinWidth);
|
||||
}
|
||||
|
||||
:host([expanded][resizable]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel::after {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
:host([expanded][resizable])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel
|
||||
> .ak-v2-c-drawer__splitter {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:host([expanded][resizable])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel
|
||||
> .ak-v2-c-drawer__panel-main {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
:host([position="left"]) {
|
||||
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: var(
|
||||
--ak-v2-c-drawer--m-expanded--m-panel-left__panel--BoxShadow
|
||||
);
|
||||
}
|
||||
|
||||
:host([position="left"][inline])
|
||||
> .ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel:not(.pf-m-no-border, .pf-m-resizable),
|
||||
:host([position="left"][static])
|
||||
> .ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel:not(.pf-m-no-border, .pf-m-resizable) {
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: var(--ak-v2-c-drawer--m-panel-left--m-inline__panel--PaddingRight);
|
||||
}
|
||||
|
||||
:host([position="left"][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([position="left"][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel::after {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
:host([position="left"][expanded][resizable])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel
|
||||
> .ak-v2-c-drawer__splitter {
|
||||
--ak-v2-c-drawer__splitter-handle--Left: var(
|
||||
--ak-v2-c-drawer--m-panel-left__splitter-handle--Left
|
||||
);
|
||||
--ak-v2-c-drawer__splitter--after--BorderRightWidth: 0;
|
||||
--ak-v2-c-drawer__splitter--after--BorderLeftWidth: var(
|
||||
--ak-v2-c-drawer--m-panel-left__splitter--after--BorderLeftWidth
|
||||
);
|
||||
order: 1;
|
||||
}
|
||||
|
||||
:host([position="bottom"]) {
|
||||
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: var(
|
||||
--ak-v2-c-drawer--m-expanded--m-panel-bottom__panel--BoxShadow
|
||||
);
|
||||
--ak-v2-c-drawer__panel--MaxHeight: 100%;
|
||||
--ak-v2-c-drawer__panel--FlexBasis--min: var(
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--FlexBasis--min
|
||||
);
|
||||
min-width: auto;
|
||||
min-height: var(--ak-v2-c-drawer--m-panel-bottom__panel--md--MinHeight);
|
||||
}
|
||||
|
||||
:host([position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel::after {
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: auto;
|
||||
width: 100%;
|
||||
height: var(--ak-v2-c-drawer--m-panel-bottom__panel--after--Height);
|
||||
}
|
||||
|
||||
:host([position="bottom"][resizable]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis--min: var(
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--md--FlexBasis--min
|
||||
);
|
||||
--ak-v2-c-drawer__panel--m-resizable--FlexDirection: var(
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--FlexDirection
|
||||
);
|
||||
--ak-v2-c-drawer__panel--m-resizable--MinWidth: 0;
|
||||
min-height: var(--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--MinHeight);
|
||||
}
|
||||
|
||||
:host([position="bottom"][resizable])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel
|
||||
> .ak-v2-c-drawer__splitter {
|
||||
--ak-v2-c-drawer__splitter-handle--Top: var(
|
||||
--ak-v2-c-drawer--m-panel-bottom__splitter-handle--Top
|
||||
);
|
||||
--ak-v2-c-drawer__splitter--after--BorderRightWidth: 0;
|
||||
--ak-v2-c-drawer__splitter--after--BorderBottomWidth: var(
|
||||
--ak-v2-c-drawer--m-panel-bottom__splitter--after--BorderBottomWidth
|
||||
);
|
||||
}
|
||||
|
||||
:host([position="left"][inline]:not([no-border], [resizable]))
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel:not(.pf-m-no-border, .pf-m-resizable),
|
||||
:host([position="left"][static]:not([no-border], [resizable]))
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel:not(.pf-m-no-border, .pf-m-resizable) {
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: var(--ak-v2-c-drawer--m-panel-left--m-inline__panel--PaddingRight);
|
||||
}
|
||||
|
||||
:host([inline][resizable])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel
|
||||
> .ak-v2-c-drawer__splitter {
|
||||
--ak-v2-c-drawer__splitter--m-vertical--Width: var(
|
||||
--ak-v2-c-drawer--m-inline__splitter--m-vertical--Width
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--Left: var(
|
||||
--ak-v2-c-drawer--m-inline__splitter-handle--Left
|
||||
);
|
||||
--ak-v2-c-drawer__splitter--after--BorderRightWidth: var(
|
||||
--ak-v2-c-drawer--m-inline__splitter--after--BorderRightWidth
|
||||
);
|
||||
--ak-v2-c-drawer__splitter--after--BorderLeftWidth: var(
|
||||
--ak-v2-c-drawer--m-inline__splitter--after--BorderLeftWidth
|
||||
);
|
||||
outline-offset: var(--ak-v2-c-drawer--m-inline__splitter--focus--OutlineOffset);
|
||||
}
|
||||
|
||||
:host([position="bottom"][inline][resizable])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel
|
||||
> .ak-v2-c-drawer__splitter {
|
||||
--ak-v2-c-drawer__splitter--Height: var(
|
||||
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter--Height
|
||||
);
|
||||
--ak-v2-c-drawer__splitter-handle--Top: var(
|
||||
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter-handle--Top
|
||||
);
|
||||
--ak-v2-c-drawer__splitter--after--BorderTopWidth: var(
|
||||
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter--after--BorderTopWidth
|
||||
);
|
||||
--ak-v2-c-drawer__splitter--after--BorderRightWidth: 0;
|
||||
--ak-v2-c-drawer__splitter--after--BorderLeftWidth: 0;
|
||||
}
|
||||
|
||||
:host([no-panel-border]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: none;
|
||||
}
|
||||
|
||||
.ak-v2-c-drawer__splitter {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:host([width="25"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 25%;
|
||||
}
|
||||
|
||||
:host([width="33"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 33%;
|
||||
}
|
||||
|
||||
:host([width="50"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 50%;
|
||||
}
|
||||
|
||||
:host([width="66"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 66%;
|
||||
}
|
||||
|
||||
:host([width="75"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 75%;
|
||||
}
|
||||
|
||||
:host([width="100"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
:host([width="25-on-lg"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 25%;
|
||||
}
|
||||
|
||||
:host([width="33-on-lg"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 33%;
|
||||
}
|
||||
|
||||
:host([width="50-on-lg"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 50%;
|
||||
}
|
||||
|
||||
:host([width="66-on-lg"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 66%;
|
||||
}
|
||||
|
||||
:host([width="75-on-lg"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 75%;
|
||||
}
|
||||
|
||||
:host([width="100-on-lg"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
:host([width="25-on-xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 25%;
|
||||
}
|
||||
|
||||
:host([width="33-on-xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 33%;
|
||||
}
|
||||
|
||||
:host([width="50-on-xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 50%;
|
||||
}
|
||||
|
||||
:host([width="66-on-xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 66%;
|
||||
}
|
||||
|
||||
:host([width="75-on-xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 75%;
|
||||
}
|
||||
|
||||
:host([width="100-on-xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1450px) {
|
||||
:host([width="25-on-2xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 25%;
|
||||
}
|
||||
|
||||
:host([width="33-on-2xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 33%;
|
||||
}
|
||||
|
||||
:host([width="50-on-2xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 50%;
|
||||
}
|
||||
|
||||
:host([width="66-on-2xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 66%;
|
||||
}
|
||||
|
||||
:host([width="75-on-2xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 75%;
|
||||
}
|
||||
|
||||
:host([width="100-on-2xl"]) {
|
||||
--ak-v2-c-drawer__panel--md--FlexBasis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:host([inline]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content,
|
||||
:host([static]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
:host([inline]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
|
||||
:host([static]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: none;
|
||||
}
|
||||
|
||||
:host([inline]:not([no-border])),
|
||||
:host([static]:not([no-border])) {
|
||||
background-color: var(
|
||||
--ak-v2-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
:host([inline]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:host([inline]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host([inline])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([inline][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([inline][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host([inline][position="left"])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([inline][position="left"][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-end: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([inline][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-block-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
:host([inline][expanded][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-block-end: 0;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
:host([static]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([static][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-end: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([static][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
:host([inline-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content,
|
||||
:host([static-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
:host([inline-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
|
||||
:host([static-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: none;
|
||||
}
|
||||
|
||||
:host([inline-on-lg]:not([no-border])),
|
||||
:host([static-on-lg]:not([no-border])) {
|
||||
background-color: var(
|
||||
--ak-v2-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
:host([inline-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:host([inline-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host([inline-on-lg])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([inline-on-lg][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([inline-on-lg][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host([inline-on-lg][position="left"])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([inline-on-lg][position="left"][expanded])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
margin-inline-end: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([inline-on-lg][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-block-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
:host([inline-on-lg][expanded][position="bottom"])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
margin-block-end: 0;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
:host([static-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([static-on-lg][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-end: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([static-on-lg][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
:host([inline-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content,
|
||||
:host([static-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
:host([inline-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
|
||||
:host([static-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: none;
|
||||
}
|
||||
|
||||
:host([inline-on-xl]:not([no-border])),
|
||||
:host([static-on-xl]:not([no-border])) {
|
||||
background-color: var(
|
||||
--ak-v2-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
:host([inline-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:host([inline-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host([inline-on-xl])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([inline-on-xl][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([inline-on-xl][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host([inline-on-xl][position="left"])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([inline-on-xl][position="left"][expanded])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
margin-inline-end: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([inline-on-xl][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-block-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
:host([inline-on-xl][expanded][position="bottom"])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
margin-block-end: 0;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
:host([static-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([static-on-xl][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-end: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([static-on-xl][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@media (min-width: 1450px) {
|
||||
:host([inline-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content,
|
||||
:host([static-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
:host([inline-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
|
||||
:host([static-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: none;
|
||||
}
|
||||
|
||||
:host([inline-on-2xl]:not([no-border])),
|
||||
:host([static-on-2xl]:not([no-border])) {
|
||||
background-color: var(
|
||||
--ak-v2-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
:host([inline-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:host([inline-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host([inline-on-2xl])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([inline-on-2xl][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([inline-on-2xl][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
|
||||
:host([inline-on-2xl][position="left"])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
|
||||
}
|
||||
|
||||
:host([inline-on-2xl][position="left"][expanded])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
margin-inline-end: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([inline-on-2xl][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-block-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
:host([inline-on-2xl][expanded][position="bottom"])
|
||||
.ak-v2-c-drawer__main
|
||||
> .ak-v2-c-drawer__panel {
|
||||
margin-block-end: 0;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
:host([static-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([static-on-2xl][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
margin-inline-end: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
:host([static-on-2xl][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
:host([position="bottom"]) {
|
||||
--ak-v2-c-drawer__panel--MinWidth: auto;
|
||||
--ak-v2-c-drawer__panel--MinHeight: var(
|
||||
--ak-v2-c-drawer--m-panel-bottom__panel--xl--MinHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
`;
|
||||
//
|
||||
export default styles;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Drawer } from "./ak-drawer.component.js";
|
||||
import { AkDrawer } from "./ak-drawer.component.js";
|
||||
|
||||
export { Drawer };
|
||||
export { AkDrawer };
|
||||
|
||||
window.customElements.define("ak-drawer", Drawer);
|
||||
window.customElements.define("ak-drawer", AkDrawer);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-drawer": Drawer;
|
||||
"ak-drawer": AkDrawer;
|
||||
}
|
||||
}
|
||||
|
||||
195
web/src/elements/ak-drawer/drawerResizeController.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { type AkDrawer } from "./ak-drawer.component";
|
||||
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
import { ReactiveController, ReactiveControllerHost } from "lit";
|
||||
|
||||
type DrawerResizeControllerHost = ReactiveControllerHost & AkDrawer;
|
||||
type Position = "start" | "end" | "left" | "right" | "bottom";
|
||||
|
||||
const oneOf = P.union;
|
||||
|
||||
const DEFAULT_SIZE_PROPERTY_NAME = "--ak-v2-c-drawer__panel--md--FlexBasis";
|
||||
const DEFAULT_RESIZE_INCREMENT = 5;
|
||||
|
||||
interface ResizeControllerProps {
|
||||
sizeProperty?: string;
|
||||
resizeIncrement?: number;
|
||||
}
|
||||
|
||||
export class DrawerResizeController implements ReactiveController {
|
||||
#abortController: AbortController | null = null;
|
||||
|
||||
#positions: {
|
||||
start: number;
|
||||
end: number;
|
||||
bottom: number;
|
||||
} = { start: 0, end: 0, bottom: 0 };
|
||||
|
||||
public resizeIncrement: number;
|
||||
public sizeProperty: string;
|
||||
|
||||
constructor(
|
||||
private host: DrawerResizeControllerHost,
|
||||
props: ResizeControllerProps = {},
|
||||
) {
|
||||
this.resizeIncrement = props.resizeIncrement ?? DEFAULT_RESIZE_INCREMENT;
|
||||
this.sizeProperty = props.sizeProperty ?? DEFAULT_SIZE_PROPERTY_NAME;
|
||||
}
|
||||
|
||||
endController() {
|
||||
this.#abortController?.abort();
|
||||
this.#abortController = null;
|
||||
}
|
||||
|
||||
restartController() {
|
||||
this.endController();
|
||||
this.#abortController = new AbortController();
|
||||
return this.#abortController.signal;
|
||||
}
|
||||
|
||||
hostQ(part: string): HTMLElement {
|
||||
const element = this.host.renderRoot.querySelector(part);
|
||||
if (element === null || !(element instanceof HTMLElement)) {
|
||||
throw new Error(`Could not identify requested part ${element}`);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
get drawer() {
|
||||
return this.hostQ('[part="drawer"]');
|
||||
}
|
||||
|
||||
get panel() {
|
||||
return this.hostQ('[part="drawer-panel"]');
|
||||
}
|
||||
|
||||
get content() {
|
||||
return this.hostQ('[part="drawer-panel-main"]');
|
||||
}
|
||||
|
||||
get splitter() {
|
||||
return this.hostQ('[part="drawer-splitter"]');
|
||||
}
|
||||
|
||||
get inline() {
|
||||
return this.host.hasAttribute("inline");
|
||||
}
|
||||
|
||||
get position(): Position {
|
||||
return (this.host.getAttribute("position") || "end") as Position;
|
||||
}
|
||||
|
||||
initPositions() {
|
||||
const pan = this.panel.getBoundingClientRect();
|
||||
this.#positions = { start: pan.left, end: pan.right, bottom: pan.bottom };
|
||||
}
|
||||
|
||||
setResizing(resizing: boolean = true) {
|
||||
if (resizing) {
|
||||
this.host.setAttribute("resizing", "");
|
||||
} else {
|
||||
this.host.removeAttribute("resizing");
|
||||
}
|
||||
}
|
||||
|
||||
get isResizing() {
|
||||
return this.host.hasAttribute("resizing");
|
||||
}
|
||||
|
||||
handleMove(ev: MouseEvent | TouchEvent, controlPosition: number) {
|
||||
ev.stopPropagation();
|
||||
const newSize = match(this.position)
|
||||
.with(oneOf("end", "right"), () => this.#positions.end - controlPosition)
|
||||
.with(oneOf("start", "left"), () => controlPosition - this.#positions.start)
|
||||
.with("bottom", () => this.#positions.bottom - controlPosition)
|
||||
.otherwise(() => {
|
||||
throw new Error(`Do not recognize position: ${this.position}`);
|
||||
});
|
||||
if (this.position === "bottom") {
|
||||
this.panel.style.overflowAnchor = "none";
|
||||
}
|
||||
this.panel.style.setProperty(DEFAULT_SIZE_PROPERTY_NAME, `${newSize}px`);
|
||||
}
|
||||
|
||||
handleMouseMove = (ev: MouseEvent) => {
|
||||
this.handleMove(ev, this.position === "bottom" ? ev.clientY : ev.clientX);
|
||||
};
|
||||
|
||||
handleTouchMove = (ev: TouchEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopImmediatePropagation();
|
||||
const touch = ev.touches[0];
|
||||
this.handleMove(ev, this.position === "bottom" ? touch.clientY : touch.clientX);
|
||||
};
|
||||
|
||||
handleMouseUp = () => {
|
||||
this.setResizing(false);
|
||||
this.initPositions();
|
||||
this.restartController();
|
||||
};
|
||||
|
||||
handleTouchEnd = (ev: TouchEvent) => {
|
||||
ev.stopPropagation();
|
||||
this.handleMouseUp();
|
||||
};
|
||||
|
||||
handleTouchStart = (ev: TouchEvent) => {
|
||||
ev.stopPropagation();
|
||||
const signal = this.restartController();
|
||||
document.addEventListener("touchmove", this.handleTouchMove, { passive: false, signal });
|
||||
document.addEventListener("touchend", this.handleTouchEnd, { signal });
|
||||
this.initPositions();
|
||||
this.setResizing();
|
||||
};
|
||||
|
||||
handleMouseDown = (ev: MouseEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const signal = this.restartController();
|
||||
document.addEventListener("mousemove", this.handleMouseMove, { signal });
|
||||
document.addEventListener("mouseup", this.handleMouseUp, { signal });
|
||||
this.initPositions();
|
||||
this.setResizing();
|
||||
};
|
||||
|
||||
handleKeyDown = (ev: KeyboardEvent) => {
|
||||
const key = ev.key;
|
||||
const positionKeys =
|
||||
this.position === "bottom" ? ["ArrowUp", "ArrowDown"] : ["ArrowLeft", "ArrowRight"];
|
||||
const validKeys = ["Escape", "Enter", ...positionKeys];
|
||||
|
||||
// Prevent default behavior when resizing, but otherwise let it pass.
|
||||
if (!validKeys.includes(key)) {
|
||||
if (this.isResizing) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
|
||||
const delta = match([key, this.position])
|
||||
.with(["ArrowRight", oneOf("end", "right")], () => -1 * this.resizeIncrement)
|
||||
.with(["ArrowLeft", oneOf("end", "right")], () => this.resizeIncrement)
|
||||
.with(["ArrowRight", oneOf("start", "left")], () => this.resizeIncrement)
|
||||
.with(["ArrowLeft", oneOf("start", "left")], () => -1 * this.resizeIncrement)
|
||||
.with(["ArrowUp", "bottom"], () => this.resizeIncrement)
|
||||
.with(["ArrowDown", "bottom"], () => -1 * this.resizeIncrement)
|
||||
.otherwise(() => 0);
|
||||
|
||||
const { height, width } = this.panel.getBoundingClientRect();
|
||||
const newSize = (this.position === "bottom" ? height : width) + delta;
|
||||
this.panel.style.setProperty(DEFAULT_SIZE_PROPERTY_NAME, `${newSize}px`);
|
||||
};
|
||||
|
||||
hostConnected() {
|
||||
this.host.updateComplete.then(() => {
|
||||
this.initPositions();
|
||||
});
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this.#abortController?.abort();
|
||||
this.#abortController = null;
|
||||
}
|
||||
}
|
||||
@@ -73,9 +73,9 @@ export class FlowInspectorButton extends WithCapabilitiesConfig(AKElement) {
|
||||
const drawer = document.getElementById("flow-drawer");
|
||||
if (changed.has("open") && drawer) {
|
||||
if (this.open) {
|
||||
drawer.setAttribute("open", "");
|
||||
drawer.setAttribute("expanded", "");
|
||||
} else {
|
||||
drawer.removeAttribute("open");
|
||||
drawer.removeAttribute("expanded");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,90 @@
|
||||
--ak-sidebar--minimum-auto-width: 80rem;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--ak-global--BackgroundColorContrast--100: var(--pf-global--palette--black-150);
|
||||
/* #region Root globals, V2 */
|
||||
:root {
|
||||
/* ---- Background Colors ---- */
|
||||
--ak-v2-global--BackgroundColor--100: #fff;
|
||||
--ak-v2-global--BorderWidth--sm: 1px;
|
||||
|
||||
/* ---- Text Colors ---------- */
|
||||
--pf-v5-global--Color--100: #151515;
|
||||
|
||||
/* ---- Border Colors -------- */
|
||||
--ak-v2-global--BorderColor--100: #d2d2d2;
|
||||
--ak-v2-global--BorderColor--200: #8a8d90;
|
||||
|
||||
/* ---- Box Shadows ------ */
|
||||
--ak-v2-global--BoxShadow--lg:
|
||||
0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08);
|
||||
--ak-v2-global--BoxShadow--lg-top: 0 -0.75rem 0.75rem -0.5rem rgba(3, 3, 3, 0.18);
|
||||
--ak-v2-global--BoxShadow--lg-right: 0.75rem 0 0.75rem -0.5rem rgba(3, 3, 3, 0.18);
|
||||
--ak-v2-global--BoxShadow--lg-bottom: 0 0.75rem 0.75rem -0.5rem rgba(3, 3, 3, 0.18);
|
||||
--ak-v2-global--BoxShadow--lg-left: -0.75rem 0 0.75rem -0.5rem rgba(3, 3, 3, 0.18);
|
||||
|
||||
/* ---- Spacers -------------- */
|
||||
--ak-v2-global--spacer--xs: 0.25rem;
|
||||
--ak-v2-global--spacer--sm: 0.5rem;
|
||||
--ak-v2-global--spacer--md: 1rem;
|
||||
--ak-v2-global--spacer--lg: 1.5rem;
|
||||
--ak-v2-global--spacer--xl: 2rem;
|
||||
--ak-v2-global--spacer--2xl: 3rem;
|
||||
--ak-v2-global--spacer--3xl: 4rem;
|
||||
--ak-v2-global--spacer--4xl: 5rem;
|
||||
--ak-v2-global--spacer--form-element: 0.375rem;
|
||||
--ak-v2-global--gutter: 1rem;
|
||||
--ak-v2-global--gutter--md: 1.5rem;
|
||||
|
||||
/* ---- Z-Index -------------- */
|
||||
--ak-v2-global--ZIndex--xs: 100;
|
||||
--ak-v2-global--ZIndex--sm: 200;
|
||||
|
||||
/* ---- Animation ------------ */
|
||||
--ak-v2-global--TransitionDuration: 250ms;
|
||||
|
||||
/* ---- Customization Bridge - */
|
||||
--ak-v2-global--dark-background: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
/* -------- Dark Theme ------------------------------- */
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* ---- Background Colors ---- */
|
||||
--ak-v2-global--BackgroundColor--100: #18191a;
|
||||
|
||||
/* ---- Text Colors ---------- */
|
||||
--ak-v2-global--Color--100: #e0e0e0;
|
||||
|
||||
/* ---- Border Colors -------- */
|
||||
--ak-v2-global--BorderColor--100: #444548;
|
||||
--ak-v2-global--BorderColor--200: #444548;
|
||||
|
||||
/* ---- Box Shadows ------ */
|
||||
--ak-v2-global--BoxShadow--lg:
|
||||
0 0.5rem 1rem 0 rgba(3, 3, 3, 0.64), 0 0 0.375rem 0 rgba(3, 3, 3, 0.32);
|
||||
--ak-v2-global--BoxShadow--lg-top: 0 -0.75rem 0.75rem -0.5rem rgba(3, 3, 3, 0.72);
|
||||
--ak-v2-global--BoxShadow--lg-right: 0.75rem 0 0.75rem -0.5rem rgba(3, 3, 3, 0.72);
|
||||
--ak-v2-global--BoxShadow--lg-bottom: 0 0.75rem 0.75rem -0.5rem rgba(3, 3, 3, 0.72);
|
||||
--ak-v2-global--BoxShadow--lg-left: -0.75rem 0 0.75rem -0.5rem rgba(3, 3, 3, 0.72);
|
||||
}
|
||||
|
||||
/* -------- Semantic Names -------------------------- */
|
||||
|
||||
:root {
|
||||
/* ---- Background Colors ---- */
|
||||
--ak-v2-global--ContentSurface: var(--ak-v2-global--BackgroundColor--100);
|
||||
--ak-v2-global--SecondaryContentSurface: var(--ak-v2-global--BackgroundColor--200);
|
||||
/* Not sure what to call this next one; this is the background color Patternfly uses when you hover
|
||||
over something and it changes color to indicate it's interactive in some way. It's the same
|
||||
color as the one above in their default theme. */
|
||||
--ak-v2-global--AffordanceIndicatedSurface: var(--ak-v2-global--BackgroundColor--200);
|
||||
|
||||
/* ---- Text Colors ---- */
|
||||
--ak-v2-global--PrimaryText: var(--ak-v2-global--Color--100);
|
||||
|
||||
/* ---- Border Colors ---- */
|
||||
--ak-v2-global--StandardBorder: var(--pf-v5-global--BorderColor--100);
|
||||
--ak-v2-global--InputAccentBorder: var(--pf-v5-global--BorderColor--200);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
@import "./base/globals.css";
|
||||
@import "./base/common.css";
|
||||
@import "./base/placeholder.css";
|
||||
@import "#elements/ak-drawer/ak-drawer.root.css";
|
||||
|
||||
@import "#styles/locales/ja/globals.css";
|
||||
@import "#styles/locales/ko/globals.css";
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@import "./components/Fieldset/fieldset.css";
|
||||
@import "./components/Login/login.css";
|
||||
@import "./components/Icon/icon.css";
|
||||
@import "#elements/ak-drawer/ak-drawer.root.css";
|
||||
@import "#elements/locale/ak-locale-select.css";
|
||||
@import "#elements/locale/ak-locale-select.css";
|
||||
@import "#flow/FlowExecutor.css";
|
||||
|
||||
21
web/stylelint.config.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
/** @type { import("stylelint").Config } */
|
||||
export default {
|
||||
extends: "stylelint-config-standard",
|
||||
rules: {
|
||||
"custom-property-pattern": [
|
||||
"^([A-Za-z][A-Za-z0-9]*)((__|--?)[A-Za-z0-9]+)*$",
|
||||
{
|
||||
message: "Expected custom property name to be kebab-case",
|
||||
},
|
||||
],
|
||||
"selector-class-pattern": [
|
||||
"^([a-z][a-z0-9]*)((__?|-)[A-Za-z0-9]+)*$",
|
||||
{
|
||||
message: (/** @type {string} */ selector) =>
|
||||
`Expected class selector "${selector}" to be kebab-case`,
|
||||
},
|
||||
],
|
||||
"declaration-empty-line-before": null,
|
||||
"media-feature-range-notation": null,
|
||||
},
|
||||
};
|
||||
@@ -3,6 +3,10 @@
|
||||
"extends": "@goauthentik/tsconfig",
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
// Nothing references the web package, so it does not need to act as a
|
||||
// composite project. Disabling composite lets us exclude `packages/`
|
||||
// (each subpackage owns its own tsconfig + build) without TS6307.
|
||||
"composite": false,
|
||||
"types": ["node"],
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
@@ -68,6 +72,10 @@
|
||||
"storybook-static",
|
||||
"src/**/*.test.ts",
|
||||
"./tests",
|
||||
// Workspace subpackages each own their tsconfig and build. Including
|
||||
// them here pulls ~1k files (notably the OpenAPI client in
|
||||
// packages/client-ts) into every `tsc -p .` for no benefit.
|
||||
"packages",
|
||||
// TODO: @lit/localize-tools v0.8.0 has a nullish coalescing typing error.
|
||||
// Remove when we upgrade past that.
|
||||
"scripts/pseudolocalize.mjs",
|
||||
|
||||
@@ -10,7 +10,15 @@ To add an application to authentik and have it display on users' **My applicatio
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
|
||||
2. Navigate to **Applications** > **Applications** and click **New Application** to open the application wizard. Alternatively, you can first create a provider separately, then create the application and connect it to the provider.
|
||||
2. Navigate to **Applications** > **Applications** and click **New Application** to open the application wizard.
|
||||
|
||||
:::info
|
||||
By default, if you click **New Application**, you are prompted to create the new application and a new provider. However, you can use the drop-down menu beside **New Application** to explicitly select either:
|
||||
|
||||
- **with New Provider...** to create the application and configure a new provider at the same time.
|
||||
- **with Existing Provider...** if you have already created a provider separately, and now you want to create the application and connect it to the provider.
|
||||
|
||||
:::
|
||||
|
||||
3. In the **New application** box, define the application details, the provider type and configuration settings, and bindings for the application.
|
||||
- **Application**: provide a name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
|
||||
@@ -167,24 +167,24 @@ Now that you can access the authentik Admin interface, and you have added an app
|
||||
|
||||
### 1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
|
||||
**A.** Navigate to **Directory > Users**, and click **New User**.
|
||||
**A.** Navigate to **Directory > Users**, click **New User**, and then select **Internal User**..
|
||||
|
||||
**B.** Fill in the **_required_** fields:
|
||||
**B.** Fill in all **_required_** fields:
|
||||
|
||||
- **Username**: This value must be unique across all users.
|
||||
- <strong className="tip">TIP</strong>: With OAuth2, front-channel logout is considered the default because most applications
|
||||
(including Grafana) do not support back-channel logout.
|
||||
- **Path**: The path where the user will be created. By default the new user is created in the `users` directory, but you can change that later by editing the user.
|
||||
- <strong className="tip">TIP</strong>: Paths are basically directories that are used to organize your users (for example HR vs Sales, etc.). Paths do not impact access; they are purely organizational. Note that the top-level **users** directory displays all users in that directory and all sub-directories.
|
||||
- **Display Name** (_optional_): The display name of the user.
|
||||
- **Email** (_optional_): The email address of the user. Email addresses are used in [email stages](../../add-secure-apps/flows-stages/stages/email/index.md), as an alternative method to log in, by services to request an email address, and if configured, to receive [notifications](../../sys-mgmt/events/notifications.md).
|
||||
- **Active** (_optional_): Define if the newly created user account is active. Selected by default.
|
||||
- **Path**: The path where the user will be created. By default the new user is created in the `users` folder, but you can change that later by editing the user.
|
||||
- <strong className="tip">TIP</strong>: Paths are directories that are used to organize your
|
||||
users (for example HR vs Sales, etc.). Paths do not impact access; they are purely
|
||||
organizational. Note that the top-level **users** folder displays all users in that folder
|
||||
and all sub-folders.
|
||||
- **Attributes** (_optional_): [Custom attributes](../../users-sources/user/user_ref.mdx) for the user, in YAML or JSON format. These attributes can be used to enforce additional prompts on authentication stages or define conditions to enforce specific policies if the current implementation does not fit your use case. The value is an empty dictionary by default.
|
||||
|
||||
For information about the **_optional_** fields below, refer to our [documentation on managing users](../../users-sources/user/user_basic_operations.md#create-a-user).
|
||||
C. Click **Create**.
|
||||
|
||||
- **Name**: The display name of the user.
|
||||
- **Email**: The email address of the user. This is required for many integrations.
|
||||
- **Is active**: Define the newly created user account as active.
|
||||
- **Attributes**: You can leave this empty for this tutorial. This field can be used to store custom attributes for the user, in YAML or JSON format. These attributes can then be used within property mappings and policies.
|
||||
|
||||
**C.** Click **Create User**.
|
||||
For more information refer to our [documentation on managing users](../../users-sources/user/user_basic_operations.md#create-a-user).
|
||||
|
||||
### 2. Verify that the new user was created
|
||||
|
||||
|
||||
@@ -428,6 +428,53 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2025.12
|
||||
- sources/oauth: Fix InvalidAudienceError in id_token fallback (cherry-pick #20096 to version-2025.12) (#20122)
|
||||
- web/admin: fix default binding order (cherry-pick #19943 to version-2025.12) (#19945)
|
||||
|
||||
## Fixed in 2025.12.5
|
||||
|
||||
- core: bump cbor2 from 5.8.0 to 5.9.0 (cherry-pick #21094 to version-2025.12) (#21095)
|
||||
- core: bump django from 5.2.11 to 5.2.12 (cherry-pick #20719 to version-2025.12) (#20737)
|
||||
- core: bump django from v5.2.12 to 5.2.13 (cherry-pick #21520 to version-2025.12) (#21525)
|
||||
- docs: Add note on skipping object syncing (cherry-pick #20882 to version-2025.12) (#20893)
|
||||
- endpoints: fix infinite recursion in stage with unsupported connector (cherry-pick #20485 to version-2025.12) (#20513)
|
||||
- enterprise: add `ES384` to enterprise license algorithms (cherry-pick #20507 to version-2025.12) (#20509)
|
||||
- events: avoid implicitly setting context from login_failed event (cherry-pick #21045 to version-2025.12) (#21049)
|
||||
- flows: continuous login debug 2025.12 (#21044)
|
||||
- security: [CVE-2026-40165](../../security/cves/CVE-2026-40165) (#22275)
|
||||
- security: [CVE-2026-40166](../../security/cves/CVE-2026-40166) (#22276)
|
||||
- security: [CVE-2026-40172](https://github.com/goauthentik/authentik/security/advisories/GHSA-h6x7-hjjc-wjc9) (#22277)
|
||||
- security: [CVE-2026-41577](https://github.com/goauthentik/authentik/security/advisories/GHSA-4v4x-x5pr-8gp2) (#22278)
|
||||
- security: [CVE-2026-42849](../../security/cves/CVE-2026-42849) (#22279)
|
||||
- internal: Automated internal backport: GHSA-5wcc-hf24-rf5h.sec.patch to authentik-2025.12 (#22280)
|
||||
- internal: Automated internal backport: GHSA-973w-j457-rp2m.sec.patch to authentik-2025.12 (#22281)
|
||||
- internal: fix lint (cherry-pick #22263 to version-2025.12) (#22306)
|
||||
- internal: make http timeouts configurable (cherry-pick #20472 to version-2025.12) (#20566)
|
||||
- policies: fix PolicyEngineMode ALL with static binding optimization (cherry-pick #20430 to version-2025.12) (#20523)
|
||||
- policies: measure policy process from manager (cherry-pick #20477 to version-2025.12) (#20480)
|
||||
- providers/oauth2: allow cross provider token introspection for federated providers (cherry-pick #21513 to version-2025.12) (#21747)
|
||||
- providers/oauth2: clip device authorization scope against the provider's ScopeMapping set (cherry-pick #21701 to version-2025.12) (#21798)
|
||||
- providers/oauth2: deactivate locale after testing (cherry-pick #20518 to version-2025.12) (#20525)
|
||||
- providers/oauth2: device code flow client id via auth header (cherry-pick #20457 to version-2025.12) (#21803)
|
||||
- providers/oauth2: don't auto-set redirect_uri (cherry-pick #21746 to version-2025.12) (#21749)
|
||||
- providers/proxy: move search path to query instead of runtime parameter (cherry-pick #20662 to version-2025.12) (#20692)
|
||||
- providers/radius: fix message authenticator validation (cherry-pick #21824 to version-2025.12) (#21827)
|
||||
- providers/saml: Fix redirect for saml slo (cherry-pick #21258 to version-2025.12) (#21283)
|
||||
- proviers/ldap: avoid concurrent header writes in API Client (cherry-pick #21223 to version-2025.12) (#21227)
|
||||
- root: do not rely on npm cli for version bump (cherry-pick #20276 to version-2025.12) (#20320)
|
||||
- root: fix compose generation for patch releases release candidates (cherry-pick #21353 to version-2025.12) (#21354)
|
||||
- root: update django to 5.2.14 (cherry-pick #22064 to version-2025.12) (#22065)
|
||||
- sources/ldap: fix exception in ldap debug endpoint (cherry-pick #21219 to version-2025.12) (#21220)
|
||||
- sources/saml: update handling statusmessage (cherry-pick #19739 to version-2025.12) (#20066)
|
||||
- stages/user_login: log correct user when session binding is broken (cherry-pick #20094 to version-2025.12) (#20452)
|
||||
- web: Fix duplicate Turnstile widgets after extended idle (cherry-pick #21380 to version-2025.12) (#21472)
|
||||
- web: Fix locale selector in compatibility mode. (cherry-pick #19946 to version-2025.12) (#20088)
|
||||
- web: re-update package-lock.json to include missing tree-sitter references
|
||||
- web/admin: fix missing OSM referrerPolicy header (cherry-pick #20984 to version-2025.12) (#20989)
|
||||
- web/admin: Fix SCIM page_size UI issue (cherry-pick #20890 to version-2025.12) (#20928)
|
||||
- web/admin: handle non-string values in formatUUID to prevent Event Log crash (cherry-pick #20804 to version-2025.12) (#21051)
|
||||
- web/flows: add continuous flow 2025.12 (#20362)
|
||||
- web/flows: prevent leader tab deadlock in continuous login flow (cherry-pick #21583 to version-2025.12) (#21626)
|
||||
- web/packages: Rework SFE rendering (cherry-pick #21833 to version-2025.12) (#21851)
|
||||
- web/sfe: bug: polyfill needed to supply Object.assign() to IE11. (cherry-pick #20126 to version-2025.12) (#20136)
|
||||
|
||||
## API Changes
|
||||
|
||||
### authentik (v 2025.12.0-rc1)
|
||||
|
||||
@@ -359,6 +359,39 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2026.2
|
||||
- web/flows: continuous login (cherry-pick #19862 to version-2026.2) (#20712)
|
||||
- web/rbac: disambiguate duplicate permission names in initial permissions (cherry-pick #20786 to version-2026.2) (#20805)
|
||||
|
||||
## Fixed in 2026.2.3
|
||||
|
||||
- blueprints: fix reconcile calling @property (cherry-pick #21576 to version-2026.2) (#21616)
|
||||
- core: bump django from v5.2.12 to 5.2.13 (cherry-pick #21520 to version-2026.2) (#21526)
|
||||
- core: fix policy binding objects not being nullable (cherry-pick #21421 to version-2026.2) (#21481)
|
||||
- core: fix search for app entitlements failing (cherry-pick #21944 to version-2026.2) (#21988)
|
||||
- endpoints: fix tasks failing (cherry-pick #20904 to version-2026.2) (#21538)
|
||||
- events: fix `destination_group_obj` not being nullable (cherry-pick #22161 to version-2026.2) (#22165)
|
||||
- security: [CVE-2026-40165](../../security/cves/CVE-2026-40165) (#22282)
|
||||
- security: [CVE-2026-40166](../../security/cves/CVE-2026-40166) (#22283)
|
||||
- security: [CVE-2026-40172](https://github.com/goauthentik/authentik/security/advisories/GHSA-h6x7-hjjc-wjc9) (#22284)
|
||||
- security: [CVE-2026-41569](../../security/cves/CVE-2026-41569) (#22285)
|
||||
- security: [CVE-2026-41577](https://github.com/goauthentik/authentik/security/advisories/GHSA-4v4x-x5pr-8gp2) (#22286)
|
||||
- security: [CVE-2026-42849](../../security/cves/CVE-2026-42849) (#22287)
|
||||
- internal: Automated internal backport: GHSA-5wcc-hf24-rf5h.sec.patch to authentik-2026.2 (#22288)
|
||||
- internal: Automated internal backport: GHSA-973w-j457-rp2m.sec.patch to authentik-2026.2 (#22289)
|
||||
- internal: fix lint (#22263)
|
||||
- lib/sync/outgoing: avoid expensive query to get number of sync pages (cherry-pick #21575 to version-2026.2) (#21581)
|
||||
- packages/django-dramatiq-postgres: reset db connections in raise_connection_error (cherry-pick #21577 to version-2026.2) (#21599)
|
||||
- packages/django-dramatiq-postgres/broker: avoid task processing stopping on decode error (cherry-pick #22110 to version-2026.2) (#22127)
|
||||
- providers/oauth2: allow cross provider token introspection for federated providers (cherry-pick #21513 to version-2026.2) (#21748)
|
||||
- providers/oauth2: clip device authorization scope against the provider's ScopeMapping set (cherry-pick #21701 to version-2026.2) (#21799)
|
||||
- providers/oauth2: don't auto-set redirect_uri (cherry-pick #21746 to version-2026.2) (#21750)
|
||||
- providers/oauth2: fix time logic in refresh_token_threshold (cherry-pick #21537 to version-2026.2) (#21598)
|
||||
- providers/radius: fix message authenticator validation (cherry-pick #21824 to version-2026.2) (#21828)
|
||||
- rbac: ensure migration 0056 runs before 0010 removes group field (cherry-pick #21964 to version-2026.2) (#22033)
|
||||
- release: 2026.2.3-rc1
|
||||
- root: update django to 5.2.14 (cherry-pick #22064 to version-2026.2) (#22066)
|
||||
- tenants/settings: present unset flags as `False` (cherry-pick #22162 to version-2026.2) (#22164)
|
||||
- web: Fix duplicate Turnstile widgets after extended idle (cherry-pick #21380 to version-2026.2) (#21473)
|
||||
- web/flows: prevent leader tab deadlock in continuous login flow (cherry-pick #21583 to version-2026.2) (#21627)
|
||||
- web/packages: Rework SFE rendering (cherry-pick #21833 to version-2026.2) (#21850)
|
||||
|
||||
## API Changes
|
||||
|
||||
### authentik (v2026.2.0)
|
||||
|
||||
@@ -10,7 +10,7 @@ beta: true
|
||||
- **Fleet Conditional Access**: :ak-enterprise authentik can now verify user devices using Fleet certificates via the Fleet Connector and an mTLS stage, without the authentik agent.
|
||||
- **`AKQL` is now open source**: The `AKQL` search query language for logs and users, previously enterprise-only, is now free for everyone to use.
|
||||
- **Command Palette and wizard upgrades**: A new `Cmd + K` command palette to search the authentik UI, alongside reworked wizards including a new user creation wizard, improved binding wizard, and new invitation wizard.
|
||||
- **Performance improvements**: The new Rust worker entrypoint drops memory usage by approximately 200 MB per worker container, opens one fewer PostgreSQL connection per worker, and makes the Admin interface less resource-intensive through lazy-loaded modals.
|
||||
- **Performance improvements**: The new Rust worker entrypoint drops memory usage by approximately 200 MB per worker container, and opens one fewer PostgreSQL connection per worker. The Admin interface is less resource-intensive through lazy-loaded modals.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
@@ -114,6 +114,10 @@ The worker status reporting change also uses one fewer PostgreSQL connection per
|
||||
|
||||
The Admin interface is also less resource-intensive in the browser due to lazy-loaded modals.
|
||||
|
||||
### Fewer packages, smaller attack surface
|
||||
|
||||
We’ve removed 17 packages, trimming bloat and tightening security in one move. Fewer components mean fewer potential vulnerabilities, helping keep your authentik deployments faster, lighter, and more resilient.
|
||||
|
||||
### OAuth2 configurable grant types
|
||||
|
||||
[OAuth2 providers](../../add-secure-apps/providers/oauth2/index.mdx#oauth-20-flows-and-grant-types) now have a **Grant Types** setting that lets admins explicitly choose which grant types a given provider may use. The available options are Authorization Code, Implicit, Hybrid, Refresh token, Client credentials, Password, and Device-code. Existing providers default to having all grant types enabled to preserve current behavior, but you can now disable any grant types you don't want a particular client to use — useful for tightening security on individual integrations and disabling legacy flows like Implicit or Password where they aren't needed.
|
||||
|
||||
@@ -12,7 +12,7 @@ draft: true
|
||||
|
||||
## Upgrading
|
||||
|
||||
This release does not introduce any new requirements. You can follow the upgrade instructions below; for more detailed information about upgrading authentik, refer to our [Upgrade documentation](../install-config/upgrade.mdx).
|
||||
This release does not introduce any new requirements. You can follow the upgrade instructions below; for more detailed information about upgrading authentik, refer to our [Upgrade documentation](../../install-config/upgrade.mdx).
|
||||
|
||||
:::warning
|
||||
When you upgrade, be aware that the version of the authentik instance and of any outposts must be the same. We recommend that you always upgrade any outposts at the same time you upgrade your authentik instance.
|
||||
|
||||
33
website/docs/security/cves/CVE-2026-40165.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# CVE-2026-40165
|
||||
|
||||
_Reported by [@kodareef5](https://github.com/kodareef5), [@Android-Login-Analysis](https://github.com/Android-Login-Analysis), and [@AyushParkara](https://github.com/AyushParkara)_
|
||||
|
||||
## SAML Source Fails to Validate Assertion Conditions
|
||||
|
||||
### Summary
|
||||
|
||||
Due to how authentik used to extract the NameID value from a SAML assertion, it was possible for an attacker to trick authentik into only seeing a part of the NameID value, potentially allowing an attacker to get access to other accounts.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2025.12.5 and 2026.2.3 fix this issue, for other versions the workaround below can be used.
|
||||
|
||||
### Impact
|
||||
|
||||
This issue can be exploited given an authentik instance with a SAML Source, where the attacker has an account on the SAML Source and the ability to modify their NameID value (commonly username or E-mail), and XML Signing is enabled. The attacker can modify the SAML assertion given to authentik by injecting a comment within the NameID value, which effectively truncates the NameID value to the snippet before the comment, and give the attack access to any user account.
|
||||
|
||||
### Workarounds
|
||||
|
||||
Create a SAML Source property mapping with the following expression and add it to all SAML Sources:
|
||||
|
||||
```python
|
||||
if name_id.text != "".join(name_id.itertext()):
|
||||
raise ValueError("Mismatched NameID")
|
||||
return {}
|
||||
```
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io).
|
||||
27
website/docs/security/cves/CVE-2026-40166.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# CVE-2026-40166
|
||||
|
||||
_Reported by [@Colbascov](https://github.com/Colbascov)_
|
||||
|
||||
## Non-admin users can read confidential OAuth provider client secrets via the access token endpoint
|
||||
|
||||
### Summary
|
||||
|
||||
Authenticated non-admin users with at least one OAuth2 access token can retrieve the `client_secret` of confidential OAuth2 providers they have previously authenticated against, via `GET /api/v3/oauth2/access_tokens/`. The API response includes a nested `provider` object containing `client_id` and `client_secret` for providers configured with `client_type: confidential`, which should not be accessible to low-privilege users.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2025.12.5 and 2026.2.3 fix this issue; for other versions the workaround can be used.
|
||||
|
||||
### Impact
|
||||
|
||||
Any authenticated non-admin user who has previously completed an OAuth2 flow against a confidential provider — and therefore has an access token object returned by `/api/v3/oauth2/access_tokens/` — can read that provider's `client_secret`. Exposure is limited to providers the user has access to and has logged into at least once; users cannot read secrets for providers they have never authenticated against. This could allow unauthorized reuse of confidential client credentials depending on the provider configuration.
|
||||
|
||||
### Workarounds
|
||||
|
||||
Restrict API access to `/api/v3/oauth2/access_tokens/` for non-admin users, or review and limit which users are permitted to complete OAuth2 flows against confidential providers until a patched version can be applied.
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [[security@goauthentik.io](mailto:security@goauthentik.io)](mailto:security@goauthentik.io)
|
||||
33
website/docs/security/cves/CVE-2026-41569.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# CVE-2026-41569
|
||||
|
||||
_Reported by [@jmecom](https://github.com/jmecom) and [@AyushParkara](https://github.com/AyushParkara)_
|
||||
|
||||
## WS-Federation wreply Origin Bypass (CVE-2026-41569)
|
||||
|
||||
### Summary
|
||||
|
||||
The WS-Federation provider validates the user-supplied `wreply` parameter using a raw string prefix check rather than proper URL parsing. An attacker who can craft a login link can supply a `wreply` value on a different origin that passes the check (e.g. `https://portal.example.com.evil.tld/`), causing the victim's browser to POST the signed WS-Federation login response to attacker-controlled infrastructure.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2025.12.5 and 2026.2.3 fix this issue.
|
||||
|
||||
### Impact
|
||||
|
||||
The WS-Federation sign-in processor accepted any `wreply` whose string value started with the configured Reply URL, not correctly comparing the domain.
|
||||
|
||||
Once accepted, the attacker-controlled `wreply` is used as the autosubmit destination, and the victim's browser immediately POSTs the signed WS-Federation response (`wresult`) to that URL. The response is a valid signed authentication artifact; in many relying-party configurations it is replayable to the legitimate ACS endpoint, enabling victim impersonation in the target application.
|
||||
|
||||
The fix replaces the string prefix check with proper URL parsing, comparing scheme, host, and path independently:
|
||||
|
||||
Only WS-Federation providers (an enterprise feature) with a prefix-ambiguous Reply URL are affected. If the Reply URL is already path-specific (e.g. `https://portal.example.com/wsfed/acs`), the host-extension bypass does not apply.
|
||||
|
||||
### Workarounds
|
||||
|
||||
Configure the WS-Federation provider's Reply URL with a specific path (e.g. `https://portal.example.com/wsfed/acs`) rather than a bare hostname. This prevents the host-extension bypass without patching, though upgrading is strongly preferred.
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||
27
website/docs/security/cves/CVE-2026-42849.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# CVE-2026-42849
|
||||
|
||||
_Reported by Jan Kahmen, [turingpoint GmbH](https://turingpoint.de/en/)_
|
||||
|
||||
## Reflected XSS in SFE
|
||||
|
||||
### Summary
|
||||
|
||||
Due to the implementation of stages in the SFE (Simple Flow Executor) in order to make the interface more compatible with legacy browsers, it was possible to use an XSS exploit in the AutosubmitStage.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2025.12.5 and 2026.2.3 fix this issue.
|
||||
|
||||
### Impact
|
||||
|
||||
The SFE (Simple Flow Executor) was susceptible to an XSS exploit. This could allow an attacker to redirect web requests containing tokens, hijack the session or take other malicious actions.
|
||||
|
||||
This is possible when an OAuth2 provider is configured, either through the redirect_uri when a very broad regex is used, or through the state value.
|
||||
|
||||
The SFE previously used jQuery without explicit sanitization, which, compared to the rest of our interfaces, did not sufficiently protect from malicious input values.
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io).
|
||||
33
website/docs/security/cves/GHSA-5wcc-hf24-rf5h.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# GHSA-5wcc-hf24-rf5h
|
||||
|
||||
_Reported by [@bugbunny-research](https://github.com/bugbunny-research)_
|
||||
|
||||
## Unauthenticated Access via Client-Controlled X-Original-URI Header in Nginx Forward-Auth Mode
|
||||
|
||||
### Summary
|
||||
|
||||
In nginx forward-auth mode, the authentik outpost reads the forwarded request URL from a header that nginx does not set, but that a client can freely inject. By crafting this header to point at an internal outpost path, an unauthenticated attacker can cause the outpost to return HTTP 200 — causing nginx to forward the original request to the protected backend without authentication.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2025.12.5 and 2026.2.3 fix this issue.
|
||||
|
||||
### Impact
|
||||
|
||||
This vulnerability only affects deployments using authentik's nginx forward-auth integration. Traefik, Caddy, and proxy mode deployments are not affected.
|
||||
|
||||
In nginx forward-auth mode, the outpost builds the URL it evaluates from a header that nginx never sets but clients can freely inject. Because nginx's `auth_request` module forwards all client headers to the authentication subrequest, an attacker-supplied value reaches the outpost unmodified.
|
||||
|
||||
The outpost unconditionally allows requests whose forwarded URL path begins with `/outpost.goauthentik.io/` (to permit internal endpoints such as the OAuth callback). An attacker can set the injected header to any path under that prefix, causing the outpost to return 200 and nginx to proxy the original request to the backend as if authenticated.
|
||||
|
||||
The attack requires only a single HTTP header — no account, session, or prior knowledge of the target application. Any resource behind the nginx gateway is accessible: reads, writes, and deletes depending on what the backend exposes. The CVSS 3.1 score is 10.0 (Critical).
|
||||
|
||||
### Workarounds
|
||||
|
||||
Operators can mitigate this immediately at the nginx layer by explicitly clearing the `X-Original-URI` header in the nginx location block that proxies traffic to the outpost. This prevents the attacker-supplied value from reaching the outpost regardless of the application-level logic. Refer to the nginx documentation for `proxy_set_header` to clear individual headers.
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||
@@ -29,15 +29,15 @@ Service accounts have certain limitations compared to regular user accounts:
|
||||
To create a service account:
|
||||
|
||||
1. In the authentik **Admin interface**, navigate to **Directory** > **Users**.
|
||||
2. Click the **Create Service Account** button.
|
||||
2. Click **New User**, and then select **Service Account**.
|
||||
3. Configure the following settings:
|
||||
- **Username**: The user's primary identifier (150 characters or fewer).
|
||||
- **Create Group**: Enabling this toggle will create a group named after the user, with the user as a member.
|
||||
- **Username**: The account's primary identifier (150 characters or fewer).
|
||||
- **Create Group** (_optional_): Enabling this toggle will create a group named after the account, with the user as a member.
|
||||
- **Expiring**: If selected, the token will expire and be automatically rotated upon expiration.
|
||||
- **Expires on**: Sets the expiration date (defaults to 1 year from the creation date).
|
||||
4. Click **Create Service Account**.
|
||||
|
||||
After creating the service account, you'll see a confirmation screen that shows the username and generated password (token). Make sure to copy this information somewhere secure because you'll need it for authentication.
|
||||
4. Click **Next**.
|
||||
View the confirmation screen that shows the username and generated password (token). Make sure to copy this information somewhere secure as you'll need it for authentication. If you need the token later, navigate to the **Directory -> Tokens and App passwords** and copy the one for your service account.
|
||||
5. Click **Close**.
|
||||
|
||||
## Token properties
|
||||
|
||||
|
||||
@@ -8,11 +8,64 @@ Invitations are another way to create a user, by inviting someone to join your a
|
||||
|
||||
You can configure invitations either by:
|
||||
|
||||
- using [pre-built blueprints](#use-pre-built-blueprints-to-configure-invitations) (recommended for quick setup).
|
||||
- using the [invitation wizard](#use-the-invitation-wizard) (recommended; creates the enrollment flow and the invitation in one guided process).
|
||||
- using [pre-built blueprints](#use-pre-built-blueprints-to-configure-invitations) (good for showcasing multiple flow variations).
|
||||
- [manually creating flows and stages](#manual-setup-without-blueprints) (for custom configurations).
|
||||
|
||||
:::info
|
||||
You can also create a [policy](../../../customize/policies/) to see if the invitation was ever used.
|
||||
You can also create a [policy](../../../customize/policies/) to check whether the invitation was ever used.
|
||||
:::
|
||||
|
||||
## Use the invitation wizard
|
||||
|
||||
The invitation wizard, available from the **Directory** > **Invitations** page in the Admin interface, walks you through creating an invitation and (optionally) the enrollment flow it binds to in a single guided process.
|
||||
|
||||
### Step 1. Open the wizard
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Directory** > **Invitations**.
|
||||
3. Click the caret (>) next to the **New Invitation** button and choose how the wizard should handle the invitation:
|
||||
- **with Existing Enrollment Flow...**: bind the new invitation to an existing enrollment flow. Only enrollment flows that have an invitation stage bound to them are listed. This is also what the **New Invitation** button does by default.
|
||||
- **with New Enrollment Flow and Invitation Stage...**: create a new minimal enrollment flow, including an invitation stage, then bind the invitation to it. Use this option when you do not yet have an enrollment flow set up, or when you want a separate enrollment flow for an invitation.
|
||||
|
||||
:::info Automatic flow selection
|
||||
If you choose **with Existing Enrollment Flow...** and only one eligible flow exists, the wizard skips the flow selection step and takes you directly to the invitation details.
|
||||
:::
|
||||
|
||||
### Step 2. Configure the enrollment flow
|
||||
|
||||
- If you picked an existing flow, select it from the **Enrollment flow** drop-down and click **Next**.
|
||||
- If you are creating a new flow, fill in:
|
||||
- **Flow name**: display name of the new enrollment flow.
|
||||
- **Flow slug**: the slug for the flow which is included in the URL.
|
||||
- **Invitation stage name**: name of the invitation stage that will be bound to the new flow.
|
||||
- **User type**: the user type for users enrolled via this flow.
|
||||
- **Continue flow without invitation**: when enabled, the flow proceeds to the next stage even when no invitation token is supplied. When disabled, the flow is cancelled if a valid invitation is not provided.
|
||||
|
||||
### Step 3. Configure the invitation details
|
||||
|
||||
- **Name**: provide a slug-style name for your invitation object (lowercase letters, numbers, and hyphens only).
|
||||
- **Expires**: select a date and time for when the invitation should expire. Defaults to 48 hours from now.
|
||||
- **Flow**: read-only; reflects the flow chosen in the previous step.
|
||||
- **Custom attributes**: (_optional_) YAML or JSON that is loaded into the flow's `prompt_data` context to pre-fill user information. Field keys must match the keys configured in the flow's [prompt stage](../../add-secure-apps/flows-stages/stages/prompt/index.md). See the [example custom attributes](#step-3-create-the-invitation-object) below for sample payloads.
|
||||
- **Single use**: when enabled, the invitation is deleted after the first successful enrollment.
|
||||
|
||||
Click **Next** to create the invitation. If you chose **with New Enrollment Flow and Invitation Stage...**, the supporting blueprint is imported at this point as well.
|
||||
|
||||
### Step 4. Share the invitation
|
||||
|
||||
After the invitation is created, the wizard's final step shows the **Link to use the invitation**. From there you can:
|
||||
|
||||
- Click **Copy Link** to copy the invitation URL to your clipboard.
|
||||
- Click **Send via Email** to open the email step inside the wizard. Enter:
|
||||
- **To**: one email per line, or comma/semicolon separated. Each recipient receives a separate email.
|
||||
- **CC** / **BCC**: (_optional_) recipients for carbon and blind carbon copies.
|
||||
- **Template**: the email template to use (the default `Invitation` template is recommended).
|
||||
|
||||
Click **Send** to queue the emails. They are sent asynchronously by the background worker. Check **System Tasks** for delivery status.
|
||||
|
||||
:::note Email configuration required
|
||||
To send invitation emails, you must have configured email in authentik. Refer to the [Email configuration](../../install-config/email.mdx) documentation for details.
|
||||
:::
|
||||
|
||||
## Use pre-built blueprints to configure invitations
|
||||
|
||||