mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 22:52:42 +02:00
Compare commits
185 Commits
core/objec
...
cherry-pic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
548aef46d2 | ||
|
|
7af9e98079 | ||
|
|
51901c82ba | ||
|
|
ff653005e4 | ||
|
|
9b64d05e35 | ||
|
|
99a93fa8a2 | ||
|
|
bd2a0e1d7d | ||
|
|
c4d455dd3a | ||
|
|
508dba6a04 | ||
|
|
aa921dcdca | ||
|
|
e5d873c129 | ||
|
|
f0a14d380f | ||
|
|
1da15a549e | ||
|
|
eaf1c45ea6 | ||
|
|
f0f42668c4 | ||
|
|
123fbd26bb | ||
|
|
b94d93b6c4 | ||
|
|
d0b25bf648 | ||
|
|
d4db4e50b4 | ||
|
|
c5e726d7eb | ||
|
|
203a7e0c61 | ||
|
|
2feaeff5db | ||
|
|
8fcc47e047 | ||
|
|
7a6408cc67 | ||
|
|
2da88028da | ||
|
|
fa91404895 | ||
|
|
460fce7279 | ||
|
|
995128955c | ||
|
|
85536abbcf | ||
|
|
5249546862 | ||
|
|
bf91348c05 | ||
|
|
63136f0180 | ||
|
|
faffabf938 | ||
|
|
0b180b15a2 | ||
|
|
07af6de74f | ||
|
|
ddfef91ea5 | ||
|
|
cefbf5e6ae | ||
|
|
e53d3d2486 | ||
|
|
32a3eed521 | ||
|
|
f05cc6e75a | ||
|
|
c68c36fdeb | ||
|
|
888f969fc7 | ||
|
|
82535e4671 | ||
|
|
ed2957e4e6 | ||
|
|
a5abe85148 | ||
|
|
8d2c31fa25 | ||
|
|
2637ce2474 | ||
|
|
319008dec8 | ||
|
|
8beb2fac18 | ||
|
|
ac7b28d0b0 | ||
|
|
073acf92c2 | ||
|
|
ad107c19af | ||
|
|
d285fcd8a7 | ||
|
|
84066cab48 | ||
|
|
e623d93ff5 | ||
|
|
1d0628dfbe | ||
|
|
996645105c | ||
|
|
63d7ca6ef0 | ||
|
|
5b24f4ad80 | ||
|
|
ed2e6cfb9c | ||
|
|
a1431ea48e | ||
|
|
b30e77b363 | ||
|
|
2f50cdd9fe | ||
|
|
494bdcaa09 | ||
|
|
e36ce1789e | ||
|
|
5a72ed83e0 | ||
|
|
f72d257e43 | ||
|
|
cbedb16cc4 | ||
|
|
6fc1b5ce90 | ||
|
|
57b0fa48c1 | ||
|
|
84a344ed87 | ||
|
|
f864cb56ab | ||
|
|
692735f9e1 | ||
|
|
e24fb300b1 | ||
|
|
f0e90d6873 | ||
|
|
0cf45835a0 | ||
|
|
69d35c1d26 | ||
|
|
ac803b210d | ||
|
|
c9728b4607 | ||
|
|
6e45584563 | ||
|
|
59a2e84b35 | ||
|
|
6025dbb9c9 | ||
|
|
d07bcd5025 | ||
|
|
e80655d285 | ||
|
|
e0d3d4d38c | ||
|
|
62112404ee | ||
|
|
1c9e12fcd9 | ||
|
|
42c6c257ec | ||
|
|
41bd9d7913 | ||
|
|
2c84935732 | ||
|
|
819c13a9bc | ||
|
|
0d8f366af8 | ||
|
|
093e60c753 | ||
|
|
af646f32d2 | ||
|
|
de4afc7322 | ||
|
|
bc1983106f | ||
|
|
8c2c1474f1 | ||
|
|
0dccbd4193 | ||
|
|
6a70894e01 | ||
|
|
2f5eb9b2e4 | ||
|
|
12aedb3a9e | ||
|
|
303dc93514 | ||
|
|
fbb217db57 | ||
|
|
4de253653f | ||
|
|
4154c06831 | ||
|
|
4750ed5e2a | ||
|
|
361017127d | ||
|
|
0ca5a54307 | ||
|
|
ef1aad5dbb | ||
|
|
29d880920e | ||
|
|
fc6f8374e6 | ||
|
|
a8668bbac4 | ||
|
|
d686932166 | ||
|
|
feceb220b1 | ||
|
|
937df6e07f | ||
|
|
48e6b968a6 | ||
|
|
cd89c45e75 | ||
|
|
e53995e2c1 | ||
|
|
33d5f11f0e | ||
|
|
565e16eca7 | ||
|
|
9a0164b722 | ||
|
|
8af491630b | ||
|
|
8e25e7a213 | ||
|
|
4d183657da | ||
|
|
be89b6052d | ||
|
|
ad5d2bb611 | ||
|
|
8d30fb3d25 | ||
|
|
cea3fbfa9b | ||
|
|
151d889ff4 | ||
|
|
58ca3ecbd5 | ||
|
|
1a6c7082a3 | ||
|
|
1dc60276f9 | ||
|
|
de045c6d7b | ||
|
|
850728e9bb | ||
|
|
84a605a4ba | ||
|
|
1780bb0cf0 | ||
|
|
cd75fe235d | ||
|
|
e6e62e9de1 | ||
|
|
ac7a4f8a22 | ||
|
|
0290ed3342 | ||
|
|
e367525794 | ||
|
|
93c319baee | ||
|
|
1d02ee7d74 | ||
|
|
93439b5742 | ||
|
|
6682a6664e | ||
|
|
0b5bac74e9 | ||
|
|
062823f1b2 | ||
|
|
a17fe58971 | ||
|
|
422ea893b1 | ||
|
|
15c9f93851 | ||
|
|
e2202d498b | ||
|
|
9ea9a86ad3 | ||
|
|
4bac1edd61 | ||
|
|
24726be3c9 | ||
|
|
411f06756f | ||
|
|
4bdcab48c3 | ||
|
|
00dbd377a7 | ||
|
|
a01c0575db | ||
|
|
6e51d044bb | ||
|
|
6d1b168dc4 | ||
|
|
43675c2b22 | ||
|
|
8645273eaf | ||
|
|
eb6f4712fe | ||
|
|
7b9505242e | ||
|
|
3dda20ebc7 | ||
|
|
dfd2bc5c3c | ||
|
|
06a270913c | ||
|
|
430507fc72 | ||
|
|
847af7f9ea | ||
|
|
8f1cb636e8 | ||
|
|
e61c876002 | ||
|
|
33c0d3df0a | ||
|
|
3a03e1ebfd | ||
|
|
1e41b77761 | ||
|
|
6c1662f99f | ||
|
|
bb5bc5c8da | ||
|
|
30670c9070 | ||
|
|
fdbf9ffedc | ||
|
|
2ec433d724 | ||
|
|
55297b9e6a | ||
|
|
f9dda6582c | ||
|
|
3394c17bfd | ||
|
|
a37d101b10 | ||
|
|
4774b4db87 | ||
|
|
fdb52c9394 |
@@ -1,5 +0,0 @@
|
||||
[alias]
|
||||
t = ["nextest", "run"]
|
||||
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
@@ -1,46 +0,0 @@
|
||||
[licenses]
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-3-Clause",
|
||||
"CC0-1.0",
|
||||
"CDLA-Permissive-2.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[licenses.private]
|
||||
ignore = true
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
wildcards = "deny"
|
||||
[bans.workspace-dependencies]
|
||||
duplicates = "deny"
|
||||
include-path-dependencies = true
|
||||
unused = "deny"
|
||||
|
||||
# No non-FIPS compliant dependencies
|
||||
[[bans.deny]]
|
||||
name = "native-tls"
|
||||
[[bans.deny]]
|
||||
name = "openssl"
|
||||
[[bans.deny]]
|
||||
name = "openssl-sys"
|
||||
[[bans.deny]]
|
||||
name = "ring"
|
||||
[[bans.features]]
|
||||
allow = [
|
||||
"alloc",
|
||||
"aws-lc-sys",
|
||||
"default",
|
||||
"fips",
|
||||
"prebuilt-nasm",
|
||||
"ring-io",
|
||||
"ring-sig-verify",
|
||||
]
|
||||
name = "aws-lc-rs"
|
||||
exact = true
|
||||
@@ -1,15 +0,0 @@
|
||||
comment_width = 100
|
||||
format_code_in_doc_comments = true
|
||||
format_strings = true
|
||||
group_imports = "StdExternalCrate"
|
||||
hex_literal_case = "Lower"
|
||||
imports_granularity = "Crate"
|
||||
max_width = 100
|
||||
newline_style = "Unix"
|
||||
normalize_comments = true
|
||||
normalize_doc_attributes = true
|
||||
reorder_impl_items = true
|
||||
style_edition = "2024"
|
||||
use_field_init_shorthand = true
|
||||
use_try_shorthand = true
|
||||
wrap_comments = true
|
||||
@@ -9,5 +9,7 @@ build_docs/**
|
||||
**/*Dockerfile
|
||||
blueprints/local
|
||||
.git
|
||||
!gen-ts-api/node_modules
|
||||
!gen-ts-api/dist/**
|
||||
!gen-go-api/
|
||||
.venv
|
||||
target
|
||||
|
||||
9
.gitattributes
vendored
9
.gitattributes
vendored
@@ -1,9 +0,0 @@
|
||||
packages/client-*/** linguist-generated
|
||||
web/packages/lex/* linguist-vendored
|
||||
web/packages/node-domexception/* linguist-vendored
|
||||
web/packages/formdata-polyfill/* linguist-vendored
|
||||
web/packages/sfe/vendored/* linguist-vendored
|
||||
website/vendored/* linguist-vendored
|
||||
website/docs/** linguist-documentation
|
||||
website/integrations/** linguist-documentation
|
||||
website/api/** linguist-documentation
|
||||
@@ -54,6 +54,10 @@ outputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: "python"
|
||||
- name: Generate config
|
||||
id: ev
|
||||
shell: bash
|
||||
@@ -64,4 +68,4 @@ runs:
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
python3 ${{ github.action_path }}/push_vars.py
|
||||
uv run python3 ${{ github.action_path }}/push_vars.py
|
||||
|
||||
@@ -2,19 +2,10 @@
|
||||
|
||||
import os
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
from sys import exit as sysexit
|
||||
from time import time
|
||||
from typing import Any
|
||||
|
||||
|
||||
def authentik_version() -> str:
|
||||
init = Path(__file__).parent.parent.parent.parent / "authentik" / "__init__.py"
|
||||
with open(init) as f:
|
||||
content = f.read()
|
||||
locals: dict[str, Any] = {}
|
||||
exec(content, None, locals) # nosec
|
||||
return str(locals["VERSION"])
|
||||
from authentik import authentik_version
|
||||
|
||||
|
||||
def must_or_fail(input: str | None, error: str) -> str:
|
||||
@@ -98,6 +89,8 @@ if should_push:
|
||||
_cache_tag = "buildcache"
|
||||
if image_arch:
|
||||
_cache_tag += f"-{image_arch}"
|
||||
if is_release:
|
||||
_cache_tag += f"-{version_family}"
|
||||
cache_to = f"type=registry,ref={get_attest_image_names(image_tags)}:{_cache_tag},mode=max"
|
||||
|
||||
|
||||
@@ -106,7 +99,6 @@ if os.getenv("RELEASE", "false").lower() == "true":
|
||||
image_build_args = [f"VERSION={os.getenv('REF')}"]
|
||||
else:
|
||||
image_build_args = [f"GIT_BUILD_HASH={sha}"]
|
||||
image_build_args_str = "\n".join(image_build_args)
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||
print(f"shouldPush={str(should_push).lower()}", file=_output)
|
||||
@@ -119,4 +111,4 @@ with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||
print(f"imageMainTag={image_main_tag}", file=_output)
|
||||
print(f"imageMainName={image_tags[0]}", file=_output)
|
||||
print(f"cacheTo={cache_to}", file=_output)
|
||||
print(f"imageBuildArgs={image_build_args_str}", file=_output)
|
||||
print(f"imageBuildArgs={"\n".join(image_build_args)}", file=_output)
|
||||
|
||||
46
.github/actions/setup/action.yml
vendored
46
.github/actions/setup/action.yml
vendored
@@ -4,7 +4,7 @@ description: "Setup authentik testing environment"
|
||||
inputs:
|
||||
dependencies:
|
||||
description: "List of dependencies to setup"
|
||||
default: "system,python,rust,node,go,runtime"
|
||||
default: "system,python,node,go,runtime"
|
||||
postgresql_version:
|
||||
description: "Optional postgresql image tag"
|
||||
default: "16"
|
||||
@@ -37,7 +37,7 @@ runs:
|
||||
sudo rsync -a --delete /tmp/empty/ /usr/local/lib/android/
|
||||
- name: Install uv
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v5
|
||||
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v5
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Setup python
|
||||
@@ -50,47 +50,17 @@ runs:
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: uv sync --all-extras --dev --frozen
|
||||
- name: Setup rust (stable)
|
||||
if: ${{ contains(inputs.dependencies, 'rust') && !contains(inputs.dependencies, 'rust-nightly') }}
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1
|
||||
with:
|
||||
rustflags: ""
|
||||
- name: Setup rust (nightly)
|
||||
if: ${{ contains(inputs.dependencies, 'rust-nightly') }}
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@5939f3337e40968c39aa70f5ecb1417a92fb25a0 # v2
|
||||
with:
|
||||
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
||||
- name: Setup node (web)
|
||||
- name: Setup node
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v4
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4
|
||||
with:
|
||||
node-version-file: "${{ inputs.working-directory }}web/package.json"
|
||||
node-version-file: ${{ inputs.working-directory }}web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: "${{ inputs.working-directory }}web/package-lock.json"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Setup node (root)
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v4
|
||||
with:
|
||||
node-version-file: "${{ inputs.working-directory }}package.json"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "${{ inputs.working-directory }}package-lock.json"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Install Node deps
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: npm ci
|
||||
cache-dependency-path: ${{ inputs.working-directory }}web/package-lock.json
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Setup go
|
||||
if: ${{ contains(inputs.dependencies, 'go') }}
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v5
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v5
|
||||
with:
|
||||
go-version-file: "${{ inputs.working-directory }}go.mod"
|
||||
- name: Setup docker cache
|
||||
|
||||
2
.github/actions/setup/compose.yml
vendored
2
.github/actions/setup/compose.yml
vendored
@@ -2,7 +2,7 @@ services:
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:${PSQL_TAG:-16}
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
- db-data:/var/lib/postgresql
|
||||
command: "-c log_statement=all"
|
||||
environment:
|
||||
POSTGRES_USER: authentik
|
||||
|
||||
8
.github/actions/test-results/action.yml
vendored
8
.github/actions/test-results/action.yml
vendored
@@ -2,22 +2,18 @@ name: "Process test results"
|
||||
description: Convert test results to JUnit, add them to GitHub Actions and codecov
|
||||
|
||||
inputs:
|
||||
files:
|
||||
description: Comma-separated explicit list of files to upload
|
||||
flags:
|
||||
description: Codecov flags
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
with:
|
||||
files: ${{ inputs.files }}
|
||||
flags: ${{ inputs.flags }}
|
||||
use_oidc: true
|
||||
- uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
with:
|
||||
files: ${{ inputs.files }}
|
||||
flags: ${{ inputs.flags }}
|
||||
use_oidc: true
|
||||
report_type: test_results
|
||||
|
||||
3
.github/codecov.yml
vendored
3
.github/codecov.yml
vendored
@@ -8,6 +8,3 @@ coverage:
|
||||
threshold: 1%
|
||||
comment:
|
||||
after_n_builds: 3
|
||||
ignore:
|
||||
- packages/client-rust
|
||||
- packages/client-ts
|
||||
|
||||
1
.github/codespell-dictionary.txt
vendored
Normal file
1
.github/codespell-dictionary.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
authentic->authentik
|
||||
32
.github/codespell-words.txt
vendored
Normal file
32
.github/codespell-words.txt
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
akadmin
|
||||
asgi
|
||||
assertIn
|
||||
authentik
|
||||
authn
|
||||
crate
|
||||
docstrings
|
||||
entra
|
||||
goauthentik
|
||||
gunicorn
|
||||
hass
|
||||
jwe
|
||||
jwks
|
||||
keypair
|
||||
keypairs
|
||||
kubernetes
|
||||
oidc
|
||||
ontext
|
||||
openid
|
||||
passwordless
|
||||
plex
|
||||
saml
|
||||
scim
|
||||
singed
|
||||
slo
|
||||
sso
|
||||
totp
|
||||
traefik
|
||||
# https://github.com/codespell-project/codespell/issues/1224
|
||||
upToDate
|
||||
warmup
|
||||
webauthn
|
||||
82
.github/dependabot.yml
vendored
82
.github/dependabot.yml
vendored
@@ -20,8 +20,6 @@ updates:
|
||||
prefix: "ci:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 3
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -37,48 +35,6 @@ updates:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
exclude:
|
||||
- "golang.org/x/crypto"
|
||||
- "golang.org/x/net"
|
||||
- "github.com/golang-jwt/jwt/*"
|
||||
- "github.com/coreos/go-oidc/*"
|
||||
- "github.com/go-ldap/ldap/*"
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rust
|
||||
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
|
||||
- package-ecosystem: rust-toolchain
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 3
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -97,10 +53,6 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "web:"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
groups:
|
||||
sentry:
|
||||
patterns:
|
||||
@@ -164,10 +116,6 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "core, web:"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
groups:
|
||||
sentry:
|
||||
patterns:
|
||||
@@ -226,10 +174,6 @@ updates:
|
||||
prefix: "website:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
groups:
|
||||
docusaurus:
|
||||
patterns:
|
||||
@@ -268,10 +212,6 @@ updates:
|
||||
prefix: "lifecycle/aws:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -287,18 +227,6 @@ updates:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
exclude:
|
||||
- "django"
|
||||
- "cryptography"
|
||||
- "pyjwt"
|
||||
- "xmlsec"
|
||||
- "lxml"
|
||||
- "psycopg"
|
||||
- "pyopenssl"
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -306,7 +234,7 @@ updates:
|
||||
|
||||
- package-ecosystem: docker
|
||||
directories:
|
||||
- /lifecycle/container
|
||||
- /
|
||||
- /website
|
||||
schedule:
|
||||
interval: daily
|
||||
@@ -316,14 +244,10 @@ updates:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 3
|
||||
- package-ecosystem: docker-compose
|
||||
directories:
|
||||
- /packages/client-go
|
||||
- /packages/client-rust
|
||||
- /packages/client-ts
|
||||
# - /scripts # Maybe
|
||||
- /scripts/api
|
||||
- /tests/e2e
|
||||
schedule:
|
||||
interval: daily
|
||||
@@ -333,7 +257,5 @@ updates:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 3
|
||||
|
||||
#endregion
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -26,7 +26,7 @@ REPLACE ME
|
||||
|
||||
If an API change has been made
|
||||
|
||||
- [ ] The API schema and clients have been updated (`make gen`)
|
||||
- [ ] The API schema has been updated (`make gen-build`)
|
||||
|
||||
If changes to the frontend have been made
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -56,19 +56,31 @@ jobs:
|
||||
release: ${{ inputs.release }}
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.registry_ghcr }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Generate API Clients
|
||||
run: |
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
id: push
|
||||
with:
|
||||
context: .
|
||||
@@ -83,7 +95,7 @@ jobs:
|
||||
platforms: linux/${{ inputs.image_arch }}
|
||||
cache-from: type=registry,ref=${{ steps.ev.outputs.attestImageNames }}:buildcache-${{ inputs.image_arch }}
|
||||
cache-to: ${{ steps.ev.outputs.cacheTo }}
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
with:
|
||||
|
||||
8
.github/workflows/_reusable-docker-build.yml
vendored
8
.github/workflows/_reusable-docker-build.yml
vendored
@@ -79,25 +79,25 @@ jobs:
|
||||
image-name: ${{ inputs.image_name }}
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.registry_ghcr }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: int128/docker-manifest-create-action@44422a4b046d55dc036df622039ed3aec43c613c # v2
|
||||
- uses: int128/docker-manifest-create-action@1a059c021f1d5e9f2bd39de745d5dd3a0ef6df90 # v2
|
||||
id: build
|
||||
with:
|
||||
tags: ${{ matrix.tag }}
|
||||
sources: |
|
||||
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-amd64.outputs.image-digest }}
|
||||
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-arm64.outputs.image-digest }}
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
|
||||
66
.github/workflows/api-ts-publish.yml
vendored
Normal file
66
.github/workflows/api-ts-publish.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: API - Publish Typescript client
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "schema.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
# Required for NPM OIDC trusted publisher
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Generate API Client
|
||||
run: make gen-client-ts
|
||||
- name: Publish package
|
||||
working-directory: gen-ts-api/
|
||||
run: |
|
||||
npm i
|
||||
npm publish --tag generated
|
||||
- name: Upgrade /web
|
||||
working-directory: web
|
||||
run: |
|
||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- name: Upgrade /web/packages/sfe
|
||||
working-directory: web/packages/sfe
|
||||
run: |
|
||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: update-web-api-client
|
||||
commit-message: "web: bump API Client version"
|
||||
title: "web: bump API Client version"
|
||||
body: "web: bump API Client version"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
# ID from https://api.github.com/users/authentik-automation[bot]
|
||||
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
|
||||
labels: dependencies
|
||||
- uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
10
.github/workflows/ci-api-docs.yml
vendored
10
.github/workflows/ci-api-docs.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
- working-directory: website/
|
||||
name: Install Dependencies
|
||||
run: npm ci
|
||||
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
|
||||
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/website/api/.docusaurus
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: npm run build -w api
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4
|
||||
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
|
||||
with:
|
||||
name: api-docs
|
||||
path: website/api/build
|
||||
@@ -67,11 +67,11 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v5
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v5
|
||||
with:
|
||||
name: api-docs
|
||||
path: website/api/build
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
|
||||
2
.github/workflows/ci-aws-cfn.yml
vendored
2
.github/workflows/ci-aws-cfn.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: lifecycle/aws/package.json
|
||||
cache: "npm"
|
||||
|
||||
14
.github/workflows/ci-docs.yml
vendored
14
.github/workflows/ci-docs.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
NODE_ENV: production
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
NODE_ENV: production
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
@@ -77,9 +77,9 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -89,14 +89,14 @@ jobs:
|
||||
image-name: ghcr.io/goauthentik/dev-docs
|
||||
- name: Login to Container Registry
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: website/Dockerfile
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
context: .
|
||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache
|
||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache,mode=max' || '' }}
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
with:
|
||||
|
||||
22
.github/workflows/ci-main-daily.yml
vendored
22
.github/workflows/ci-main-daily.yml
vendored
@@ -6,10 +6,6 @@ on:
|
||||
schedule:
|
||||
# Every night at 3am
|
||||
- cron: "0 3 * * *"
|
||||
pull_request:
|
||||
paths:
|
||||
# Needs to refer to itself
|
||||
- .github/workflows/ci-main-daily.yml
|
||||
|
||||
jobs:
|
||||
test-container:
|
||||
@@ -19,20 +15,14 @@ jobs:
|
||||
matrix:
|
||||
version:
|
||||
- docs
|
||||
- version-2025-12
|
||||
- version-2026-2
|
||||
- version-2025-4
|
||||
- version-2025-2
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- run: |
|
||||
set -euo pipefail
|
||||
current="$(pwd)"
|
||||
dir="/tmp/authentik/${{ matrix.version }}"
|
||||
# 2025.12 still serves the legacy docker-compose filename; newer sites use compose.yml.
|
||||
compose_path="compose.yml"
|
||||
if [ "${{ matrix.version }}" = "version-2025-12" ]; then
|
||||
compose_path="docker-compose.yml"
|
||||
fi
|
||||
mkdir -p "${dir}/lifecycle/container"
|
||||
cd "${dir}"
|
||||
wget "https://${{ matrix.version }}.goauthentik.io/${compose_path}" -O "${dir}/lifecycle/container/compose.yml"
|
||||
"${current}/scripts/test_docker.sh"
|
||||
mkdir -p $dir
|
||||
cd $dir
|
||||
wget https://${{ matrix.version }}.goauthentik.io/compose.yml
|
||||
${current}/scripts/test_docker.sh
|
||||
|
||||
109
.github/workflows/ci-main.yml
vendored
109
.github/workflows/ci-main.yml
vendored
@@ -28,52 +28,20 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- job: bandit
|
||||
deps: python
|
||||
- job: black
|
||||
deps: python
|
||||
- job: spellcheck
|
||||
deps: node
|
||||
- job: pending-migrations
|
||||
deps: python,runtime
|
||||
- job: ruff
|
||||
deps: python
|
||||
- job: mypy
|
||||
deps: python
|
||||
- job: cargo-deny
|
||||
deps: rust
|
||||
- job: cargo-machete
|
||||
deps: rust
|
||||
- job: clippy
|
||||
deps: rust
|
||||
- job: rustfmt
|
||||
deps: rust-nightly
|
||||
job:
|
||||
- bandit
|
||||
- black
|
||||
- codespell
|
||||
- pending-migrations
|
||||
- ruff
|
||||
- mypy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: ${{ matrix.deps }}
|
||||
- name: run job
|
||||
run: make ci-lint-${{ matrix.job }}
|
||||
test-gen:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: "system,python,go,node,runtime,rust-nightly"
|
||||
- name: generate schema
|
||||
run: make migrate gen-build
|
||||
- name: generate API clients
|
||||
run: make gen-clients
|
||||
- name: ensure schema is up-to-date
|
||||
run: git diff --exit-code -- schema.yml blueprints/schema.json packages/client-go packages/client-rust packages/client-ts
|
||||
run: uv run make ci-${{ matrix.job }}
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -127,7 +95,10 @@ jobs:
|
||||
with:
|
||||
postgresql_version: ${{ matrix.psql }}
|
||||
- name: run migrations to stable
|
||||
run: uv run python -m lifecycle.migrate
|
||||
run: |
|
||||
docker ps
|
||||
docker logs setup-postgresql-1
|
||||
uv run python -m lifecycle.migrate
|
||||
- name: checkout current code
|
||||
run: |
|
||||
set -x
|
||||
@@ -192,11 +163,10 @@ jobs:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Create k8s Kind Cluster
|
||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0
|
||||
- name: run integration
|
||||
run: |
|
||||
uv run coverage run manage.py test tests/integration
|
||||
uv run coverage combine
|
||||
uv run coverage xml
|
||||
- uses: ./.github/actions/test-results
|
||||
if: ${{ always() }}
|
||||
@@ -212,60 +182,47 @@ jobs:
|
||||
job:
|
||||
- name: proxy
|
||||
glob: tests/e2e/test_provider_proxy*
|
||||
profiles: selenium
|
||||
- name: oauth
|
||||
glob: tests/e2e/test_provider_oauth2* tests/e2e/test_source_oauth*
|
||||
profiles: selenium
|
||||
- name: oauth-oidc
|
||||
glob: tests/e2e/test_provider_oidc*
|
||||
profiles: selenium
|
||||
- name: saml
|
||||
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
||||
profiles: selenium
|
||||
- name: ldap
|
||||
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
||||
- name: rac
|
||||
glob: tests/e2e/test_provider_rac*
|
||||
profiles: selenium
|
||||
- name: ws-fed
|
||||
glob: tests/e2e/test_provider_ws_fed*
|
||||
profiles: selenium
|
||||
- name: radius
|
||||
glob: tests/e2e/test_provider_radius*
|
||||
- name: scim
|
||||
glob: tests/e2e/test_source_scim*
|
||||
- name: flows
|
||||
glob: tests/e2e/test_flows*
|
||||
profiles: selenium
|
||||
- name: endpoints
|
||||
glob: tests/e2e/test_endpoints_*
|
||||
profiles: selenium
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup e2e env
|
||||
env:
|
||||
COMPOSE_PROFILES: ${{ matrix.job.profiles }}
|
||||
- name: Setup e2e env (chrome, etc)
|
||||
run: |
|
||||
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
|
||||
- id: cache-web
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
|
||||
if: contains(matrix.job.profiles, 'selenium')
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
- name: prepare web ui
|
||||
if: steps.cache-web.outputs.cache-hit != 'true' && contains(matrix.job.profiles, 'selenium')
|
||||
if: steps.cache-web.outputs.cache-hit != 'true'
|
||||
working-directory: web
|
||||
run: |
|
||||
npm ci
|
||||
make -C .. gen-client-ts
|
||||
npm run build
|
||||
npm run build:sfe
|
||||
- name: run e2e
|
||||
run: |
|
||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
uv run coverage combine
|
||||
uv run coverage xml
|
||||
- uses: ./.github/actions/test-results
|
||||
if: ${{ always() }}
|
||||
@@ -288,15 +245,13 @@ jobs:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup e2e env (chrome, etc)
|
||||
env:
|
||||
COMPOSE_PROFILES: selenium
|
||||
run: |
|
||||
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
|
||||
- name: Setup conformance suite
|
||||
run: |
|
||||
docker compose -f tests/openid_conformance/compose.yml up -d --quiet-pull
|
||||
- id: cache-web
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
@@ -305,50 +260,26 @@ jobs:
|
||||
working-directory: web
|
||||
run: |
|
||||
npm ci
|
||||
make -C .. gen-client-ts
|
||||
npm run build
|
||||
npm run build:sfe
|
||||
- name: run conformance
|
||||
run: |
|
||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
uv run coverage combine
|
||||
uv run coverage xml
|
||||
- uses: ./.github/actions/test-results
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
flags: conformance
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: conformance-certification-${{ matrix.job.name }}
|
||||
path: tests/openid_conformance/exports/
|
||||
test-rust:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: rust,runtime
|
||||
- name: run tests
|
||||
run: |
|
||||
cargo llvm-cov --no-report nextest --workspace
|
||||
cargo llvm-cov report --codecov --output-path target/llvm-cov-target/rust.json
|
||||
- uses: ./.github/actions/test-results
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
files: target/llvm-cov-target/rust.json
|
||||
flags: rust
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-rust
|
||||
path: target/llvm-cov-target/rust.json
|
||||
ci-core-mark:
|
||||
if: always()
|
||||
needs:
|
||||
- lint
|
||||
- test-gen
|
||||
- test-migrations
|
||||
- test-migrations-from-stable
|
||||
- test-unittest
|
||||
|
||||
26
.github/workflows/ci-outpost.yml
vendored
26
.github/workflows/ci-outpost.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Prepare and generate API
|
||||
@@ -31,6 +31,8 @@ jobs:
|
||||
mkdir -p web/dist
|
||||
mkdir -p website/help
|
||||
touch web/dist/test website/help/test
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v8
|
||||
with:
|
||||
@@ -41,11 +43,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: prepare database
|
||||
run: |
|
||||
uv run make migrate
|
||||
@@ -86,9 +90,9 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -98,14 +102,16 @@ jobs:
|
||||
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
|
||||
- name: Login to Container Registry
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: lifecycle/container/${{ matrix.type }}.Dockerfile
|
||||
@@ -116,7 +122,7 @@ jobs:
|
||||
context: .
|
||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
|
||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
with:
|
||||
@@ -142,14 +148,16 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
|
||||
12
.github/workflows/ci-web.yml
vendored
12
.github/workflows/ci-web.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
project: web
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: ${{ matrix.project }}/package.json
|
||||
cache: "npm"
|
||||
@@ -40,6 +40,8 @@ jobs:
|
||||
- working-directory: ${{ matrix.project }}/
|
||||
run: |
|
||||
npm ci
|
||||
- name: Generate API
|
||||
run: make gen-client-ts
|
||||
- name: Lint
|
||||
working-directory: ${{ matrix.project }}/
|
||||
run: npm run ${{ matrix.command }}
|
||||
@@ -47,13 +49,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- working-directory: web/
|
||||
run: npm ci
|
||||
- name: Generate API
|
||||
run: make gen-client-ts
|
||||
- name: build
|
||||
working-directory: web/
|
||||
run: npm run build
|
||||
@@ -73,13 +77,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- working-directory: web/
|
||||
run: npm ci
|
||||
- name: Generate API
|
||||
run: make gen-client-ts
|
||||
- name: test
|
||||
working-directory: web/
|
||||
run: npm run test || exit 0
|
||||
|
||||
6
.github/workflows/gen-image-compress.yml
vendored
6
.github/workflows/gen-image-compress.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
github.event.pull_request.head.repo.full_name == github.repository)
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
@@ -38,11 +38,11 @@ jobs:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Compress images
|
||||
id: compress
|
||||
uses: calibreapp/image-actions@4f7260f5dbd809ec86d03721c1ad71b8a841d3e0 # main
|
||||
uses: calibreapp/image-actions@d9c8ee5c3dc52ae4622c82ead88d658f4b16b65f # main
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
compressOnly: ${{ github.event_name != 'pull_request' }}
|
||||
- uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
if: "${{ github.event_name != 'pull_request' && steps.compress.outputs.markdown != '' }}"
|
||||
id: cpr
|
||||
with:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- run: uv run ak update_webauthn_mds
|
||||
- uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
2
.github/workflows/gh-cherry-pick.yml
vendored
2
.github/workflows/gh-cherry-pick.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
if: ${{ env.GH_APP_ID != '' }}
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
|
||||
2
.github/workflows/gh-ghcr-retention.yml
vendored
2
.github/workflows/gh-ghcr-retention.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
|
||||
5
.github/workflows/packages-npm-publish.yml
vendored
5
.github/workflows/packages-npm-publish.yml
vendored
@@ -29,19 +29,18 @@ jobs:
|
||||
- packages/eslint-config
|
||||
- packages/prettier-config
|
||||
- packages/docusaurus-config
|
||||
- packages/logger-js
|
||||
- packages/esbuild-plugin-live-reload
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: ${{ matrix.package }}/package.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
|
||||
uses: tj-actions/changed-files@8cba46e29c11878d930bca7870bb54394d3e8b21 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
|
||||
with:
|
||||
files: |
|
||||
${{ matrix.package }}/package.json
|
||||
|
||||
6
.github/workflows/release-branch-off.yml
vendored
6
.github/workflows/release-branch-off.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
- name: Bump version
|
||||
run: "make bump version=${{ inputs.next_version }}.0-rc1"
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: release-bump-${{ inputs.next_version }}
|
||||
|
||||
44
.github/workflows/release-publish.yml
vendored
44
.github/workflows/release-publish.yml
vendored
@@ -33,9 +33,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -44,21 +44,21 @@ jobs:
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/docs
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: website/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
|
||||
id: attest
|
||||
if: true
|
||||
with:
|
||||
@@ -84,18 +84,18 @@ jobs:
|
||||
- rac
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -103,19 +103,23 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/${{ matrix.type }},authentik/${{ matrix.type }}
|
||||
- name: Generate API Clients
|
||||
run: |
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
id: push
|
||||
with:
|
||||
push: true
|
||||
@@ -125,7 +129,7 @@ jobs:
|
||||
file: lifecycle/container/${{ matrix.type }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
@@ -148,10 +152,10 @@ jobs:
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
@@ -160,6 +164,10 @@ jobs:
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
- name: Generate API Clients
|
||||
run: |
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
@@ -172,7 +180,7 @@ jobs:
|
||||
export CGO_ENABLED=0
|
||||
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
|
||||
- name: Upload binaries to release
|
||||
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2
|
||||
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
@@ -191,7 +199,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
|
||||
- uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
|
||||
with:
|
||||
role-to-assume: "arn:aws:iam::016170277896:role/github_goauthentik_authentik"
|
||||
aws-region: ${{ env.AWS_REGION }}
|
||||
@@ -236,7 +244,7 @@ jobs:
|
||||
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
|
||||
docker cp ${container}:web/ .
|
||||
- name: Create a Sentry.io release
|
||||
uses: getsentry/action-release@5657c9e888b4e2cc85f4d29143ea4131fde4a73a # v3
|
||||
uses: getsentry/action-release@dab6548b3c03c4717878099e43782cf5be654289 # v3
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
12
.github/workflows/release-tag.yml
vendored
12
.github/workflows/release-tag.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
|
||||
git push --follow-tags
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
tag_name: "version/${{ inputs.version }}"
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
sed -E -i 's/[0-9]{4}\.[0-9]{1,2}\.[0-9]+$/${{ inputs.version }}/' charts/authentik/Chart.yaml
|
||||
./scripts/helm-docs.sh
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
branch: bump-${{ inputs.version }}
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url | .stable.reason = $reason' version.json > version.new.json
|
||||
mv version.new.json version.json
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
branch: bump-${{ inputs.version }}
|
||||
|
||||
4
.github/workflows/repo-stale.yml
vendored
4
.github/workflows/repo-stale.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
|
||||
with:
|
||||
repo-token: ${{ steps.generate_token.outputs.token }}
|
||||
days-before-stale: 60
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
- id: generate_token
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
@@ -33,6 +33,8 @@ jobs:
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Generate API
|
||||
run: make gen-client-ts
|
||||
- name: run extract
|
||||
run: |
|
||||
uv run make i18n-extract
|
||||
@@ -42,7 +44,7 @@ jobs:
|
||||
make web-check-compile
|
||||
- name: Create Pull Request
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: extract-compile-backend-translation
|
||||
|
||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -15,9 +15,6 @@ media
|
||||
|
||||
node_modules
|
||||
|
||||
.cspellcache
|
||||
cspell-report.*
|
||||
|
||||
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||
# in your Git repository. Update and uncomment the following line accordingly.
|
||||
# <django-project-name>/staticfiles/
|
||||
@@ -195,24 +192,6 @@ pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
|
||||
# End of https://www.gitignore.io/api/python,django
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/rust
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=rust
|
||||
|
||||
### Rust ###
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust
|
||||
|
||||
/static/
|
||||
local.env.yml
|
||||
|
||||
@@ -220,6 +199,7 @@ media/
|
||||
*mmdb
|
||||
|
||||
.idea/
|
||||
/gen-*/
|
||||
data/
|
||||
|
||||
# Local Netlify folder
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Prettier Ignorefile
|
||||
|
||||
## Static Files
|
||||
CODEOWNERS
|
||||
**/LICENSE
|
||||
|
||||
authentik/stages/**/*
|
||||
|
||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -17,6 +17,6 @@
|
||||
"ms-python.vscode-pylance",
|
||||
"redhat.vscode-yaml",
|
||||
"Tobermory.es6-string-html",
|
||||
"unifiedjs.vscode-mdx"
|
||||
"unifiedjs.vscode-mdx",
|
||||
]
|
||||
}
|
||||
|
||||
28
.vscode/settings.json
vendored
28
.vscode/settings.json
vendored
@@ -14,10 +14,6 @@
|
||||
"[xml]": {
|
||||
"editor.minimap.markSectionHeaderRegex": "<!--\\s*#\\bregion\\s*(?<separator>-?)\\s*(?<label>.*)\\s*-->"
|
||||
},
|
||||
"files.associations": {
|
||||
// The built-in "ignore" language gives us enough syntax highlighting to make these files readable.
|
||||
"**/dictionaries/*.txt": "ignore"
|
||||
},
|
||||
"todo-tree.tree.showCountsInTree": true,
|
||||
"todo-tree.tree.showBadges": true,
|
||||
"yaml.customTags": [
|
||||
@@ -38,10 +34,10 @@
|
||||
"!AtIndex scalar",
|
||||
"!ParseJSON scalar"
|
||||
],
|
||||
"js/ts.preferences.importModuleSpecifier": "non-relative",
|
||||
"js/ts.preferences.importModuleSpecifierEnding": "index",
|
||||
"js/ts.tsdk.path": "./node_modules/typescript/lib",
|
||||
"js/ts.tsdk.promptToUseWorkspaceVersion": true,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "index",
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"yaml.schemas": {
|
||||
"./blueprints/schema.json": "blueprints/**/*.yaml"
|
||||
},
|
||||
@@ -53,17 +49,13 @@
|
||||
"ignoreCase": false
|
||||
}
|
||||
],
|
||||
"go.testFlags": ["-count=1"],
|
||||
"go.testFlags": [
|
||||
"-count=1"
|
||||
],
|
||||
"go.testEnvVars": {
|
||||
"WORKSPACE_DIR": "${workspaceFolder}"
|
||||
},
|
||||
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"],
|
||||
"search.exclude": {
|
||||
"**/*.code-search": true,
|
||||
"**/bower_components": true,
|
||||
"**/node_modules": true,
|
||||
"**/playwright-report/**": true,
|
||||
"**/website/**/build": true,
|
||||
"**/client-*": true
|
||||
}
|
||||
"github-actions.workflows.pinned.workflows": [
|
||||
".github/workflows/ci-main.yml"
|
||||
]
|
||||
}
|
||||
|
||||
10
CODEOWNERS
10
CODEOWNERS
@@ -3,7 +3,6 @@
|
||||
# Backend
|
||||
authentik/ @goauthentik/backend
|
||||
blueprints/ @goauthentik/backend
|
||||
src/ @goauthentik/backend
|
||||
cmd/ @goauthentik/backend
|
||||
internal/ @goauthentik/backend
|
||||
lifecycle/ @goauthentik/backend
|
||||
@@ -12,12 +11,8 @@ scripts/ @goauthentik/backend
|
||||
tests/ @goauthentik/backend
|
||||
pyproject.toml @goauthentik/backend
|
||||
uv.lock @goauthentik/backend
|
||||
Cargo.toml @goauthentik/backend
|
||||
Cargo.lock @goauthentik/backend
|
||||
go.mod @goauthentik/backend
|
||||
go.sum @goauthentik/backend
|
||||
.cargo/ @goauthentik/backend
|
||||
rust-toolchain.toml @goauthentik/backend
|
||||
# Infrastructure
|
||||
.github/ @goauthentik/infrastructure
|
||||
lifecycle/aws/ @goauthentik/infrastructure
|
||||
@@ -27,23 +22,18 @@ Makefile @goauthentik/infrastructure
|
||||
.editorconfig @goauthentik/infrastructure
|
||||
CODEOWNERS @goauthentik/infrastructure
|
||||
# Backend packages
|
||||
packages/ak-* @goauthentik/backend
|
||||
packages/client-rust @goauthentik/backend
|
||||
packages/django-channels-postgres @goauthentik/backend
|
||||
packages/django-postgres-cache @goauthentik/backend
|
||||
packages/django-dramatiq-postgres @goauthentik/backend
|
||||
# Web packages
|
||||
tsconfig.json @goauthentik/frontend
|
||||
package.json @goauthentik/frontend
|
||||
package-lock.json @goauthentik/frontend
|
||||
packages/package.json @goauthentik/frontend
|
||||
packages/package-lock.json @goauthentik/frontend
|
||||
packages/client-ts @goauthentik/frontend
|
||||
packages/docusaurus-config @goauthentik/frontend
|
||||
packages/esbuild-plugin-live-reload @goauthentik/frontend
|
||||
packages/eslint-config @goauthentik/frontend
|
||||
packages/prettier-config @goauthentik/frontend
|
||||
packages/logger-js @goauthentik/frontend
|
||||
packages/tsconfig @goauthentik/frontend
|
||||
# Web
|
||||
web/ @goauthentik/frontend
|
||||
|
||||
4870
Cargo.lock
generated
4870
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
231
Cargo.toml
231
Cargo.toml
@@ -1,231 +0,0 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"packages/ak-axum",
|
||||
"packages/ak-common",
|
||||
"packages/client-rust",
|
||||
"website/scripts/docsmg",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "2026.5.0-rc1"
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
description = "Making authentication simple."
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
homepage = "https://goauthentik.io"
|
||||
repository = "https://github.com/goauthentik/authentik.git"
|
||||
license-file = "LICENSE"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
arc-swap = "= 1.9.1"
|
||||
axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] }
|
||||
aws-lc-rs = { version = "= 1.16.2", features = ["fips"] }
|
||||
axum = { version = "= 0.8.8", features = ["http2", "macros", "ws"] }
|
||||
clap = { version = "= 4.6.0", features = ["derive", "env"] }
|
||||
client-ip = { version = "0.2.1", features = ["forwarded-header"] }
|
||||
colored = "= 3.1.1"
|
||||
config-rs = { package = "config", version = "= 0.15.22", default-features = false, features = [
|
||||
"json",
|
||||
"yaml",
|
||||
] }
|
||||
console-subscriber = "= 0.5.0"
|
||||
dotenvy = "= 0.15.7"
|
||||
durstr = "= 0.5.1"
|
||||
eyre = "= 0.6.12"
|
||||
forwarded-header-value = "= 0.1.1"
|
||||
futures = "= 0.3.32"
|
||||
glob = "= 0.3.3"
|
||||
ipnet = { version = "= 2.12.0", features = ["serde"] }
|
||||
json-subscriber = "= 0.2.8"
|
||||
nix = { version = "= 0.31.2", features = ["signal"] }
|
||||
notify = "= 8.2.0"
|
||||
pin-project-lite = "= 0.2.17"
|
||||
regex = "= 1.12.3"
|
||||
reqwest = { version = "= 0.13.2", features = [
|
||||
"form",
|
||||
"json",
|
||||
"multipart",
|
||||
"query",
|
||||
"rustls",
|
||||
"stream",
|
||||
] }
|
||||
reqwest-middleware = { version = "= 0.5.1", features = [
|
||||
"form",
|
||||
"json",
|
||||
"multipart",
|
||||
"query",
|
||||
"rustls",
|
||||
] }
|
||||
rustls = { version = "= 0.23.37", features = ["fips"] }
|
||||
sentry = { version = "= 0.47.0", default-features = false, features = [
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"debug-images",
|
||||
"panic",
|
||||
"rustls",
|
||||
"reqwest",
|
||||
"tower",
|
||||
"tracing",
|
||||
] }
|
||||
serde = { version = "= 1.0.228", features = ["derive"] }
|
||||
serde_json = "= 1.0.149"
|
||||
serde_repr = "= 0.1.20"
|
||||
serde_with = { version = "= 3.18.0", default-features = false, features = [
|
||||
"base64",
|
||||
] }
|
||||
sqlx = { version = "= 0.8.6", default-features = false, features = [
|
||||
"runtime-tokio",
|
||||
"tls-rustls-aws-lc-rs",
|
||||
"postgres",
|
||||
"derive",
|
||||
"macros",
|
||||
"uuid",
|
||||
"chrono",
|
||||
"ipnet",
|
||||
"json",
|
||||
] }
|
||||
tempfile = "= 3.27.0"
|
||||
thiserror = "= 2.0.18"
|
||||
time = { version = "= 0.3.47", features = ["macros"] }
|
||||
tokio = { version = "= 1.51.1", features = ["full", "tracing"] }
|
||||
tokio-retry2 = "= 0.9.1"
|
||||
tokio-rustls = "= 0.26.4"
|
||||
tokio-util = { version = "= 0.7.18", features = ["full"] }
|
||||
tower = "= 0.5.3"
|
||||
tower-http = { version = "= 0.6.8", features = ["timeout"] }
|
||||
tracing = "= 0.1.44"
|
||||
tracing-error = "= 0.2.1"
|
||||
tracing-subscriber = { version = "= 0.3.23", features = [
|
||||
"env-filter",
|
||||
"json",
|
||||
"local-time",
|
||||
"tracing-log",
|
||||
] }
|
||||
url = "= 2.5.8"
|
||||
uuid = { version = "= 1.23.0", features = ["serde", "v4"] }
|
||||
|
||||
ak-client = { package = "authentik-client", version = "2026.5.0-rc1", path = "./packages/client-rust" }
|
||||
ak-common = { package = "authentik-common", version = "2026.5.0-rc1", path = "./packages/ak-common", default-features = false }
|
||||
|
||||
[profile.dev.package.backtrace]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
debug = 2
|
||||
|
||||
[workspace.lints.rust]
|
||||
ambiguous_negative_literals = "warn"
|
||||
closure_returning_async_block = "warn"
|
||||
macro_use_extern_crate = "deny"
|
||||
# must_not_suspend = "deny", unstable see https://github.com/rust-lang/rust/issues/83310
|
||||
non_ascii_idents = "deny"
|
||||
redundant_imports = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unit_bindings = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unsafe_code = "deny"
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_macro_rules = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
[workspace.lints.rustdoc]
|
||||
unescaped_backticks = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
### enable all lints
|
||||
cargo = { priority = -1, level = "warn" }
|
||||
complexity = { priority = -1, level = "warn" }
|
||||
correctness = { priority = -1, level = "warn" }
|
||||
nursery = { priority = -1, level = "warn" }
|
||||
pedantic = { priority = -1, level = "warn" }
|
||||
perf = { priority = -1, level = "warn" }
|
||||
# Those are too restrictive and disabled by default, however we enable some below
|
||||
# restriction = { priority = -1, level = "warn" }
|
||||
style = { priority = -1, level = "warn" }
|
||||
suspicious = { priority = -1, level = "warn" }
|
||||
### and disable the ones we don't want
|
||||
### cargo group
|
||||
multiple_crate_versions = "allow"
|
||||
### pedantic group
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
must_use_candidate = "allow"
|
||||
redundant_closure_for_method_calls = "allow"
|
||||
struct_field_names = "allow"
|
||||
too_many_lines = "allow"
|
||||
### nursery
|
||||
missing_const_for_fn = "allow"
|
||||
option_if_let_else = "allow"
|
||||
redundant_pub_crate = "allow"
|
||||
significant_drop_tightening = "allow"
|
||||
### restriction group
|
||||
allow_attributes = "warn"
|
||||
allow_attributes_without_reason = "warn"
|
||||
as_conversions = "warn"
|
||||
as_pointer_underscore = "warn"
|
||||
as_underscore = "warn"
|
||||
assertions_on_result_states = "warn"
|
||||
clone_on_ref_ptr = "warn"
|
||||
create_dir = "warn"
|
||||
dbg_macro = "warn"
|
||||
default_numeric_fallback = "warn"
|
||||
disallowed_script_idents = "warn"
|
||||
empty_drop = "warn"
|
||||
empty_enum_variants_with_brackets = "warn"
|
||||
empty_structs_with_brackets = "warn"
|
||||
error_impl_error = "warn"
|
||||
exit = "warn"
|
||||
filetype_is_file = "warn"
|
||||
float_cmp_const = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
get_unwrap = "warn"
|
||||
if_then_some_else_none = "warn"
|
||||
impl_trait_in_params = "warn"
|
||||
infinite_loop = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
map_with_unused_argument_over_ranges = "warn"
|
||||
mem_forget = "warn"
|
||||
missing_asserts_for_indexing = "warn"
|
||||
missing_trait_methods = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mutex_atomic = "warn"
|
||||
mutex_integer = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
non_zero_suggestions = "warn"
|
||||
panic_in_result_fn = "warn"
|
||||
pathbuf_init_then_push = "warn"
|
||||
print_stdout = "warn"
|
||||
rc_buffer = "warn"
|
||||
redundant_test_prefix = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
ref_patterns = "warn"
|
||||
renamed_function_params = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
return_and_then = "warn"
|
||||
same_name_method = "warn"
|
||||
semicolon_inside_block = "warn"
|
||||
str_to_string = "warn"
|
||||
string_add = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
tests_outside_test_module = "warn"
|
||||
todo = "warn"
|
||||
try_err = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unimplemented = "warn"
|
||||
unnecessary_safety_comment = "warn"
|
||||
unnecessary_safety_doc = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
unneeded_field_pattern = "warn"
|
||||
unseparated_literal_suffix = "warn"
|
||||
unused_result_ok = "warn"
|
||||
unused_trait_names = "warn"
|
||||
unwrap_in_result = "warn"
|
||||
unwrap_used = "warn"
|
||||
verbose_file_reads = "warn"
|
||||
144
Makefile
144
Makefile
@@ -15,11 +15,14 @@ else
|
||||
SED_INPLACE = sed -i
|
||||
endif
|
||||
|
||||
GEN_API_TS = gen-ts-api
|
||||
GEN_API_PY = gen-py-api
|
||||
GEN_API_GO = gen-go-api
|
||||
|
||||
BREW_LDFLAGS :=
|
||||
BREW_CPPFLAGS :=
|
||||
BREW_PKG_CONFIG_PATH :=
|
||||
|
||||
CARGO := cargo
|
||||
UV := uv
|
||||
|
||||
# For macOS users, add the libxml2 installed from brew libxmlsec1 to the build path
|
||||
@@ -66,29 +69,22 @@ help: ## Show this help
|
||||
sort
|
||||
@echo ""
|
||||
|
||||
go-test: ## Run the golang tests
|
||||
go-test:
|
||||
go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
rust-test: ## Run the Rust tests
|
||||
$(CARGO) nextest run --workspace
|
||||
|
||||
test: ## Run the server tests and produce a coverage report (locally)
|
||||
$(UV) run coverage run manage.py test --keepdb $(or $(filter-out $@,$(MAKECMDGOALS)),authentik)
|
||||
$(UV) run coverage combine
|
||||
$(UV) run coverage html
|
||||
$(UV) run coverage report
|
||||
|
||||
lint-fix-rust:
|
||||
$(CARGO) +nightly fmt --all -- --config-path "${PWD}/.cargo/rustfmt.toml"
|
||||
|
||||
lint-fix: lint-fix-rust ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||
$(UV) run black $(PY_SOURCES)
|
||||
$(UV) run ruff check --fix $(PY_SOURCES)
|
||||
|
||||
lint-spellcheck: ## Reports spelling errors.
|
||||
npm run lint:spellcheck
|
||||
lint-codespell: ## Reports spelling errors.
|
||||
$(UV) run codespell -w
|
||||
|
||||
lint: ci-lint-bandit ci-lint-mypy ci-lint-cargo-deny ci-lint-cargo-machete ## Lint the python and golang sources
|
||||
lint: ci-bandit ci-mypy ## Lint the python and golang sources
|
||||
golangci-lint run -v
|
||||
|
||||
core-install:
|
||||
@@ -121,7 +117,8 @@ core-i18n-extract:
|
||||
--no-obsolete \
|
||||
--ignore web \
|
||||
--ignore internal \
|
||||
--ignore packages/client-ts \
|
||||
--ignore ${GEN_API_TS} \
|
||||
--ignore ${GEN_API_GO} \
|
||||
--ignore website \
|
||||
-l en
|
||||
|
||||
@@ -144,14 +141,8 @@ dev-create-db:
|
||||
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
|
||||
|
||||
update-test-mmdb: ## Update test GeoIP and ASN Databases
|
||||
curl \
|
||||
-L \
|
||||
-o ${PWD}/tests/geoip/GeoLite2-ASN-Test.mmdb \
|
||||
https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb
|
||||
curl \
|
||||
-L \
|
||||
-o ${PWD}/tests/geoip/GeoLite2-City-Test.mmdb \
|
||||
https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb
|
||||
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb
|
||||
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb
|
||||
|
||||
bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
|
||||
ifndef version
|
||||
@@ -160,7 +151,6 @@ endif
|
||||
$(eval current_version := $(shell cat ${PWD}/internal/constants/VERSION))
|
||||
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' ${PWD}/pyproject.toml
|
||||
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' ${PWD}/authentik/__init__.py
|
||||
$(SED_INPLACE) "s/version = \"${current_version}\"/version = \"$(version)\"" ${PWD}/Cargo.toml ${PWD}/Cargo.lock
|
||||
$(MAKE) gen-build gen-compose aws-cfn
|
||||
$(SED_INPLACE) "s/\"${current_version}\"/\"$(version)\"/" ${PWD}/package.json ${PWD}/package-lock.json ${PWD}/web/package.json ${PWD}/web/package-lock.json
|
||||
echo -n $(version) > ${PWD}/internal/constants/VERSION
|
||||
@@ -178,23 +168,13 @@ gen-build: ## Extract the schema from the database
|
||||
gen-compose:
|
||||
$(UV) run scripts/generate_compose.py
|
||||
|
||||
gen-changelog: ## (Release) generate the changelog based from the commits since the last version
|
||||
# These are best-effort guesses based on commit messages
|
||||
$(eval last_version := $(shell git tag --list 'version/*' --sort 'version:refname' | grep -vE 'rc\d+$$' | tail -1))
|
||||
$(eval current_commit := $(shell git rev-parse HEAD))
|
||||
git log --pretty=format:"- %s" $(shell git merge-base ${last_version} ${current_commit})...${current_commit} > merged_to_current
|
||||
git log --pretty=format:"- %s" $(shell git merge-base ${last_version} ${current_commit})...${last_version} > merged_to_last
|
||||
grep -Eo 'cherry-pick (#\d+)' merged_to_last | cut -d ' ' -f 2 | sed 's/.*/(&)$$/' > cherry_picked_to_last
|
||||
grep -vf cherry_picked_to_last merged_to_current | sort > changelog.md
|
||||
rm merged_to_current
|
||||
rm merged_to_last
|
||||
rm cherry_picked_to_last
|
||||
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
|
||||
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
|
||||
npx prettier --write changelog.md
|
||||
|
||||
gen-diff: ## (Release) generate the changelog diff between the current schema and the last version
|
||||
$(eval last_version := $(shell git tag --list 'version/*' --sort 'version:refname' | grep -vE 'rc\d+$$' | tail -1))
|
||||
git show ${last_version}:schema.yml > schema-old.yml
|
||||
docker compose -f scripts/compose.yml run --rm --user "${UID}:${GID}" diff \
|
||||
gen-diff: ## (Release) generate the changelog diff between the current schema and the last tag
|
||||
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > schema-old.yml
|
||||
docker compose -f scripts/api/compose.yml run --rm --user "${UID}:${GID}" diff \
|
||||
--markdown \
|
||||
/local/diff.md \
|
||||
/local/schema-old.yml \
|
||||
@@ -204,26 +184,51 @@ gen-diff: ## (Release) generate the changelog diff between the current schema a
|
||||
$(SED_INPLACE) 's/}/}/g' diff.md
|
||||
npx prettier --write diff.md
|
||||
|
||||
gen-client-go: ## Build and install the authentik API for Golang
|
||||
$(UV) run make -C "${PWD}/packages/client-go" build
|
||||
gen-clean-ts: ## Remove generated API client for TypeScript
|
||||
rm -rf ${PWD}/${GEN_API_TS}/
|
||||
rm -rf ${PWD}/web/node_modules/@goauthentik/api/
|
||||
|
||||
gen-client-rust: ## Build and install the authentik API for Rust
|
||||
$(UV) run make -C "${PWD}/packages/client-rust" build version=${NPM_VERSION}
|
||||
make lint-fix-rust
|
||||
gen-clean-py: ## Remove generated API client for Python
|
||||
rm -rf ${PWD}/${GEN_API_PY}
|
||||
|
||||
gen-client-ts: ## Build and install the authentik API for Typescript into the authentik UI Application
|
||||
make -C "${PWD}/packages/client-ts" build
|
||||
npm --prefix web install
|
||||
gen-clean-go: ## Remove generated API client for Go
|
||||
rm -rf ${PWD}/${GEN_API_GO}
|
||||
|
||||
_gen-clients: gen-client-go gen-client-rust gen-client-ts
|
||||
gen-clients: ## Build and install API clients used by authentik
|
||||
$(MAKE) _gen-clients -j
|
||||
gen-clean: gen-clean-ts gen-clean-go gen-clean-py ## Remove generated API clients
|
||||
|
||||
gen: gen-build gen-clients ## Build and install API schema and clients used by authentik
|
||||
gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescript into the authentik UI Application
|
||||
docker compose -f scripts/api/compose.yml run --rm --user "${UID}:${GID}" gen \
|
||||
generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/${GEN_API_TS} \
|
||||
-c /local/scripts/api/ts-config.yaml \
|
||||
--additional-properties=npmVersion=${NPM_VERSION} \
|
||||
--git-repo-id authentik \
|
||||
--git-user-id goauthentik
|
||||
|
||||
cd ${PWD}/${GEN_API_TS} && npm i
|
||||
cd ${PWD}/${GEN_API_TS} && npm link
|
||||
cd ${PWD}/web && npm link @goauthentik/api
|
||||
|
||||
gen-client-py: gen-clean-py ## Build and install the authentik API for Python
|
||||
mkdir -p ${PWD}/${GEN_API_PY}
|
||||
git clone --depth 1 https://github.com/goauthentik/client-python.git ${PWD}/${GEN_API_PY}
|
||||
cp ${PWD}/schema.yml ${PWD}/${GEN_API_PY}
|
||||
make -C ${PWD}/${GEN_API_PY} build version=${NPM_VERSION}
|
||||
|
||||
gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
|
||||
mkdir -p ${PWD}/${GEN_API_GO}
|
||||
git clone --depth 1 https://github.com/goauthentik/client-go.git ${PWD}/${GEN_API_GO}
|
||||
cp ${PWD}/schema.yml ${PWD}/${GEN_API_GO}
|
||||
make -C ${PWD}/${GEN_API_GO} build version=${NPM_VERSION}
|
||||
go mod edit -replace goauthentik.io/api/v3=./${GEN_API_GO}
|
||||
|
||||
gen-dev-config: ## Generate a local development config file
|
||||
$(UV) run scripts/generate_config.py
|
||||
|
||||
gen: gen-build gen-client-ts
|
||||
|
||||
#########################
|
||||
## Node.js
|
||||
#########################
|
||||
@@ -271,7 +276,7 @@ docs: docs-lint-fix docs-build ## Automatically fix formatting issues in the Au
|
||||
docs-install:
|
||||
npm ci --prefix website
|
||||
|
||||
docs-lint-fix: lint-spellcheck
|
||||
docs-lint-fix: lint-codespell
|
||||
npm run --prefix website prettier
|
||||
|
||||
docs-build:
|
||||
@@ -292,7 +297,7 @@ docs-api-build:
|
||||
npm run --prefix website -w api build
|
||||
|
||||
docs-api-watch: ## Build and watch the API documentation
|
||||
npm run --prefix website -w api generate
|
||||
npm run --prefix website -w api build:api
|
||||
npm run --prefix website -w api start
|
||||
|
||||
docs-api-clean: ## Clean generated API documentation
|
||||
@@ -303,6 +308,7 @@ docs-api-clean: ## Clean generated API documentation
|
||||
#########################
|
||||
|
||||
docker: ## Build a docker image of the current source tree
|
||||
mkdir -p ${GEN_API_TS}
|
||||
DOCKER_BUILDKIT=1 docker build . -f lifecycle/container/Dockerfile --progress plain --tag ${DOCKER_IMAGE}
|
||||
|
||||
test-docker:
|
||||
@@ -315,42 +321,28 @@ test-docker:
|
||||
# which makes the YAML File a lot smaller
|
||||
|
||||
ci--meta-debug:
|
||||
$(UV) run python -V || echo "No python installed"
|
||||
$(CARGO) --version || echo "No rust installed"
|
||||
node --version || echo "No node installed"
|
||||
$(UV) run python -V
|
||||
node --version
|
||||
|
||||
ci-lint-mypy: ci--meta-debug
|
||||
ci-mypy: ci--meta-debug
|
||||
$(UV) run mypy --strict $(PY_SOURCES)
|
||||
|
||||
ci-lint-black: ci--meta-debug
|
||||
ci-black: ci--meta-debug
|
||||
$(UV) run black --check $(PY_SOURCES)
|
||||
|
||||
ci-lint-ruff: ci--meta-debug
|
||||
ci-ruff: ci--meta-debug
|
||||
$(UV) run ruff check $(PY_SOURCES)
|
||||
|
||||
ci-lint-spellcheck: ci--meta-debug
|
||||
npm run lint:spellcheck
|
||||
ci-codespell: ci--meta-debug
|
||||
$(UV) run codespell -s
|
||||
|
||||
ci-lint-bandit: ci--meta-debug
|
||||
ci-bandit: ci--meta-debug
|
||||
$(UV) run bandit -c pyproject.toml -r $(PY_SOURCES) -iii
|
||||
|
||||
ci-lint-pending-migrations: ci--meta-debug
|
||||
ci-pending-migrations: ci--meta-debug
|
||||
$(UV) run ak makemigrations --check
|
||||
|
||||
ci-lint-cargo-deny: ci--meta-debug
|
||||
$(CARGO) deny --locked --workspace check --config "${PWD}/.cargo/deny.toml"
|
||||
|
||||
ci-lint-cargo-machete: ci--meta-debug
|
||||
$(CARGO) machete
|
||||
|
||||
ci-lint-rustfmt: ci--meta-debug
|
||||
$(CARGO) +nightly fmt --all --check -- --config-path "${PWD}/.cargo/rustfmt.toml"
|
||||
|
||||
ci-lint-clippy: ci--meta-debug
|
||||
$(CARGO) clippy --workspace -- -D warnings
|
||||
|
||||
ci-test: ci--meta-debug
|
||||
$(UV) run coverage run manage.py test --keepdb --parallel auto authentik
|
||||
$(UV) run coverage combine
|
||||
$(UV) run coverage run manage.py test --keepdb authentik
|
||||
$(UV) run coverage report
|
||||
$(UV) run coverage xml
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
|
||||
VERSION = "2026.5.0-rc1"
|
||||
VERSION = "2026.2.3-rc1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
"""Meta API"""
|
||||
|
||||
from django.apps import apps
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.fields import BooleanField, CharField
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from authentik.api.validation import validate
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import AttributesMixin
|
||||
from authentik.lib.api import Models
|
||||
from authentik.lib.utils.reflection import get_apps
|
||||
from authentik.policies.event_matcher.models import model_choices
|
||||
|
||||
|
||||
class AppSerializer(PassiveSerializer):
|
||||
@@ -39,19 +36,12 @@ class AppsViewSet(ViewSet):
|
||||
class ModelViewSet(ViewSet):
|
||||
"""Read-only view list all installed models"""
|
||||
|
||||
class ModelFilterSerializer(PassiveSerializer):
|
||||
filter_has_attributes = BooleanField(allow_null=True, default=None)
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(responses={200: AppSerializer(many=True)}, parameters=[ModelFilterSerializer])
|
||||
@validate(ModelFilterSerializer, "query")
|
||||
def list(self, request: Request, query: ModelFilterSerializer) -> Response:
|
||||
@extend_schema(responses={200: AppSerializer(many=True)})
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Read-only view list all installed models"""
|
||||
data = []
|
||||
for name, label in Models.choices:
|
||||
if query.validated_data["filter_has_attributes"]:
|
||||
if not issubclass(apps.get_model(name), AttributesMixin):
|
||||
continue
|
||||
for name, label in model_choices():
|
||||
data.append({"name": name, "label": label})
|
||||
return Response(AppSerializer(data, many=True).data)
|
||||
|
||||
@@ -94,7 +94,7 @@ class Backend:
|
||||
|
||||
Args:
|
||||
file_path: Relative file path
|
||||
request: Optional Django HttpRequest for fully qualified URL building
|
||||
request: Optional Django HttpRequest for fully qualifed URL building
|
||||
use_cache: whether to retrieve the URL from cache
|
||||
|
||||
Returns:
|
||||
@@ -106,7 +106,6 @@ class Backend:
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> dict[str, str] | None:
|
||||
"""
|
||||
Get URLs for each theme variant when filename contains %(theme)s.
|
||||
@@ -122,7 +121,7 @@ class Backend:
|
||||
return None
|
||||
|
||||
return {
|
||||
theme: self.file_url(substitute_theme(name, theme), request, use_cache=use_cache)
|
||||
theme: self.file_url(substitute_theme(name, theme), request, use_cache=True)
|
||||
for theme in get_valid_themes()
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ class PassthroughBackend(Backend):
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> dict[str, str] | None:
|
||||
"""Support themed URLs for external URLs with %(theme)s placeholder.
|
||||
|
||||
|
||||
@@ -100,25 +100,13 @@ class S3Backend(ManageableBackend):
|
||||
f"storage.{self.usage.value}.{self.name}.addressing_style",
|
||||
CONFIG.get(f"storage.{self.name}.addressing_style", "auto"),
|
||||
)
|
||||
signature_version = CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.signature_version",
|
||||
CONFIG.get(f"storage.{self.name}.signature_version", "s3v4"),
|
||||
)
|
||||
# Keep signature_version pass-through and let boto3/botocore handle it.
|
||||
# In boto3's S3 configuration docs, `s3v4` (default) and deprecated `s3`
|
||||
# are the documented values:
|
||||
# https://github.com/boto/boto3/blob/791a3e8f36d83664a47b4281a0586b3546cef3ec/docs/source/guide/configuration.rst?plain=1#L398-L407
|
||||
# Botocore also supports additional signer names, so we intentionally do
|
||||
# not enforce a restricted allowlist here.
|
||||
|
||||
return self.session.client(
|
||||
"s3",
|
||||
endpoint_url=endpoint_url,
|
||||
use_ssl=use_ssl,
|
||||
region_name=region_name,
|
||||
config=Config(
|
||||
signature_version=signature_version, s3={"addressing_style": addressing_style}
|
||||
),
|
||||
config=Config(signature_version="s3v4", s3={"addressing_style": addressing_style}),
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from unittest import skipUnless
|
||||
|
||||
from botocore.exceptions import UnsupportedSignatureVersionError
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.admin.files.tests.utils import FileTestS3BackendMixin, s3_test_server_available
|
||||
@@ -82,27 +81,6 @@ class TestS3Backend(FileTestS3BackendMixin, TestCase):
|
||||
self.assertIn("X-Amz-Signature=", url)
|
||||
self.assertIn("test.png", url)
|
||||
|
||||
def test_client_signature_version_default_v4(self):
|
||||
"""Test S3 client defaults to v4 signature when not configured."""
|
||||
self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3v4")
|
||||
|
||||
@CONFIG.patch("storage.s3.signature_version", "s3")
|
||||
def test_client_signature_version_global_override(self):
|
||||
"""Test S3 client respects globally configured signature version."""
|
||||
self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3")
|
||||
|
||||
@CONFIG.patch("storage.s3.signature_version", "s3v4")
|
||||
@CONFIG.patch("storage.media.s3.signature_version", "s3")
|
||||
def test_client_signature_version_media_override(self):
|
||||
"""Test usage-specific signature version takes precedence over global."""
|
||||
self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3")
|
||||
|
||||
@CONFIG.patch("storage.media.s3.signature_version", "not-a-real-signature")
|
||||
def test_client_signature_version_unsupported(self):
|
||||
"""Test unsupported signature version raises botocore error."""
|
||||
with self.assertRaises(UnsupportedSignatureVersionError):
|
||||
self.media_s3_backend.file_url("test.png", use_cache=False)
|
||||
|
||||
@CONFIG.patch("storage.s3.bucket_name", "test-bucket")
|
||||
def test_file_exists_true(self):
|
||||
"""Test file_exists returns True for existing file"""
|
||||
|
||||
@@ -74,10 +74,6 @@ class FileManager:
|
||||
) -> str:
|
||||
"""
|
||||
Get URL for accessing the file.
|
||||
|
||||
Set ``use_cache=False`` when the caller needs a fresh signed URL instead
|
||||
of a cached one, for example when serializing flow/login payloads that
|
||||
may be refreshed after the previous JWT has expired.
|
||||
"""
|
||||
if not name:
|
||||
return ""
|
||||
@@ -87,7 +83,7 @@ class FileManager:
|
||||
|
||||
for backend in self.backends:
|
||||
if backend.supports_file(name):
|
||||
return backend.file_url(name, request, use_cache=use_cache)
|
||||
return backend.file_url(name, request)
|
||||
|
||||
LOGGER.warning(f"Could not find file backend for file: {name}")
|
||||
return ""
|
||||
@@ -96,14 +92,10 @@ class FileManager:
|
||||
self,
|
||||
name: str | None,
|
||||
request: HttpRequest | Request | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> dict[str, str] | None:
|
||||
"""
|
||||
Get URLs for each theme variant when filename contains %(theme)s.
|
||||
|
||||
``use_cache`` has the same semantics as ``file_url()`` and allows
|
||||
callers to force regeneration of expiring signed URLs.
|
||||
|
||||
Returns dict mapping theme to URL if %(theme)s present, None otherwise.
|
||||
"""
|
||||
if not name:
|
||||
@@ -114,7 +106,7 @@ class FileManager:
|
||||
|
||||
for backend in self.backends:
|
||||
if backend.supports_file(name):
|
||||
return backend.themed_urls(name, request, use_cache=use_cache)
|
||||
return backend.themed_urls(name, request)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Test file service layer"""
|
||||
|
||||
from unittest import skipUnless
|
||||
from unittest.mock import Mock
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.http import HttpRequest
|
||||
@@ -54,19 +53,6 @@ class TestResolveFileUrlBasic(TestCase):
|
||||
result = manager.file_url("/static/authentik/sources/icon.svg")
|
||||
self.assertEqual(result, "/static/authentik/sources/icon.svg")
|
||||
|
||||
def test_file_url_forwards_use_cache(self):
|
||||
"""Test file_url forwards use_cache to backend."""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
backend = Mock()
|
||||
backend.supports_file.return_value = True
|
||||
backend.file_url.return_value = "/files/media/public/test.png?token=fresh"
|
||||
manager.backends = [backend]
|
||||
|
||||
result = manager.file_url("test.png", use_cache=False)
|
||||
|
||||
self.assertEqual(result, "/files/media/public/test.png?token=fresh")
|
||||
backend.file_url.assert_called_once_with("test.png", None, use_cache=False)
|
||||
|
||||
|
||||
class TestResolveFileUrlFileBackend(FileTestFileBackendMixin, TestCase):
|
||||
def test_resolve_storage_file(self):
|
||||
|
||||
@@ -106,14 +106,14 @@ class TokenAuthentication(BaseAuthentication):
|
||||
if not auth_credentials:
|
||||
return None
|
||||
# first, check traditional tokens
|
||||
key_token = Token.objects.filter(
|
||||
key_token = Token.filter_not_expired(
|
||||
key=auth_credentials, intent=TokenIntents.INTENT_API
|
||||
).first()
|
||||
if key_token:
|
||||
CTX_AUTH_VIA.set("api_token")
|
||||
return key_token.user, key_token
|
||||
# then try to auth via JWT
|
||||
jwt_token = AccessToken.objects.filter(
|
||||
jwt_token = AccessToken.filter_not_expired(
|
||||
token=auth_credentials, _scope__icontains=SCOPE_AUTHENTIK_API
|
||||
).first()
|
||||
if jwt_token:
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
"""Pagination which includes total pages and current page"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from drf_spectacular.plumbing import build_object_type
|
||||
from rest_framework import pagination
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentik.api.search.ql import QLSearch
|
||||
from authentik.api.v3.schema.pagination import PAGINATION
|
||||
from authentik.api.v3.schema.search import AUTOCOMPLETE_SCHEMA
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models import QuerySet
|
||||
from rest_framework.request import Request
|
||||
from authentik.api.v3.schema.response import PAGINATION
|
||||
|
||||
|
||||
class Pagination(pagination.PageNumberPagination):
|
||||
@@ -21,14 +13,14 @@ class Pagination(pagination.PageNumberPagination):
|
||||
page_query_param = "page"
|
||||
page_size_query_param = "page_size"
|
||||
|
||||
def get_page_size(self, request: Request) -> int:
|
||||
def get_page_size(self, request):
|
||||
if self.page_size_query_param in request.query_params:
|
||||
page_size = super().get_page_size(request)
|
||||
if page_size is not None:
|
||||
return min(super().get_page_size(request), request.tenant.pagination_max_page_size)
|
||||
return request.tenant.pagination_default_page_size
|
||||
|
||||
def get_paginated_response(self, data) -> Response:
|
||||
def get_paginated_response(self, data):
|
||||
previous_page_number = 0
|
||||
if self.page.has_previous():
|
||||
previous_page_number = self.page.previous_page_number()
|
||||
@@ -47,33 +39,16 @@ class Pagination(pagination.PageNumberPagination):
|
||||
"end_index": self.page.end_index(),
|
||||
},
|
||||
"results": data,
|
||||
"autocomplete": self.get_autocomplete(),
|
||||
}
|
||||
)
|
||||
|
||||
def paginate_queryset(self, queryset: QuerySet, request: Request, view=None):
|
||||
self.view = view
|
||||
return super().paginate_queryset(queryset, request, view)
|
||||
|
||||
def get_autocomplete(self):
|
||||
schema = QLSearch().get_schema(self.request, self.view)
|
||||
introspections = {}
|
||||
if hasattr(self.view, "get_ql_fields"):
|
||||
from authentik.api.search.schema import AKQLSchemaSerializer
|
||||
|
||||
introspections = AKQLSchemaSerializer().serialize(
|
||||
schema(self.page.paginator.object_list.model)
|
||||
)
|
||||
return introspections
|
||||
|
||||
def get_paginated_response_schema(self, schema):
|
||||
return build_object_type(
|
||||
properties={
|
||||
"pagination": PAGINATION.ref,
|
||||
"results": schema,
|
||||
"autocomplete": AUTOCOMPLETE_SCHEMA.ref,
|
||||
},
|
||||
required=["pagination", "results", "autocomplete"],
|
||||
required=["pagination", "results"],
|
||||
)
|
||||
|
||||
|
||||
|
||||
103
authentik/api/schema.py
Normal file
103
authentik/api/schema.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
from drf_spectacular.plumbing import ResolvedComponent
|
||||
from drf_spectacular.renderers import OpenApiJsonRenderer
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.apps import AuthentikAPIConfig
|
||||
from authentik.api.v3.schema.query import QUERY_PARAMS
|
||||
from authentik.api.v3.schema.response import (
|
||||
GENERIC_ERROR,
|
||||
GENERIC_ERROR_RESPONSE,
|
||||
PAGINATION,
|
||||
VALIDATION_ERROR,
|
||||
VALIDATION_ERROR_RESPONSE,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def preprocess_schema_exclude_non_api(endpoints: list[tuple[str, Any, Any, Callable]], **kwargs):
|
||||
"""Filter out all API Views which are not mounted under /api"""
|
||||
return [
|
||||
(path, path_regex, method, callback)
|
||||
for path, path_regex, method, callback in endpoints
|
||||
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
|
||||
]
|
||||
|
||||
|
||||
def postprocess_schema_register(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Register custom schema components"""
|
||||
LOGGER.debug("Registering custom schemas")
|
||||
generator.registry.register_on_missing(PAGINATION)
|
||||
generator.registry.register_on_missing(GENERIC_ERROR)
|
||||
generator.registry.register_on_missing(GENERIC_ERROR_RESPONSE)
|
||||
generator.registry.register_on_missing(VALIDATION_ERROR)
|
||||
generator.registry.register_on_missing(VALIDATION_ERROR_RESPONSE)
|
||||
for query in QUERY_PARAMS.values():
|
||||
generator.registry.register_on_missing(query)
|
||||
return result
|
||||
|
||||
|
||||
def postprocess_schema_responses(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Default error responses"""
|
||||
LOGGER.debug("Adding default error responses")
|
||||
for path in result["paths"].values():
|
||||
for method in path.values():
|
||||
method["responses"].setdefault("400", VALIDATION_ERROR_RESPONSE.ref)
|
||||
method["responses"].setdefault("403", GENERIC_ERROR_RESPONSE.ref)
|
||||
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
|
||||
# This is a workaround for authentik/stages/prompt/stage.py
|
||||
# since the serializer PromptChallengeResponse
|
||||
# accepts dynamic keys
|
||||
for component in result["components"]["schemas"]:
|
||||
if component == "PromptChallengeResponseRequest":
|
||||
comp = result["components"]["schemas"][component]
|
||||
comp["additionalProperties"] = {}
|
||||
return result
|
||||
|
||||
|
||||
def postprocess_schema_query_params(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Optimise pagination parameters, instead of redeclaring parameters for each endpoint
|
||||
declare them globally and refer to them"""
|
||||
LOGGER.debug("Deduplicating query parameters")
|
||||
for path in result["paths"].values():
|
||||
for method in path.values():
|
||||
for idx, param in enumerate(method.get("parameters", [])):
|
||||
if param["name"] not in QUERY_PARAMS:
|
||||
continue
|
||||
method["parameters"][idx] = QUERY_PARAMS[param["name"]].ref
|
||||
return result
|
||||
|
||||
|
||||
def postprocess_schema_remove_unused(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Remove unused components"""
|
||||
# To check if the schema is used, render it to JSON and then substring check that
|
||||
# less efficient than walking through the tree but a lot simpler and no
|
||||
# possibility that we miss something
|
||||
raw = OpenApiJsonRenderer().render(result, renderer_context={}).decode()
|
||||
count = 0
|
||||
for key in result["components"][ResolvedComponent.SCHEMA].keys():
|
||||
schema_usages = raw.count(f"#/components/{ResolvedComponent.SCHEMA}/{key}")
|
||||
if schema_usages >= 1:
|
||||
continue
|
||||
del generator.registry[(key, ResolvedComponent.SCHEMA)]
|
||||
count += 1
|
||||
LOGGER.debug("Removing unused components", count=count)
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
return result
|
||||
@@ -1,31 +1,73 @@
|
||||
"""authentik API Modelviewset tests"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.test import TestCase
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.admin.api.version_history import VersionHistoryViewSet
|
||||
from authentik.api.v3.urls import router
|
||||
from authentik.core.tests.utils import RequestFactory, create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.tenants.api.domains import DomainViewSet
|
||||
from authentik.tenants.api.tenants import TenantViewSet
|
||||
from authentik.tenants.utils import get_current_tenant
|
||||
|
||||
|
||||
class TestModelViewSets(TestCase):
|
||||
"""Test Viewset"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_test_admin_user()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def viewset_tester_factory(test_viewset: type[ModelViewSet]) -> Callable:
|
||||
|
||||
def viewset_tester_factory(test_viewset: type[ModelViewSet], full=True) -> dict[str, Callable]:
|
||||
"""Test Viewset"""
|
||||
|
||||
def tester(self: TestModelViewSets):
|
||||
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
|
||||
def test_attrs(self: TestModelViewSets) -> None:
|
||||
"""Test attributes we require on all viewsets"""
|
||||
self.assertIsNotNone(getattr(test_viewset, "ordering", None))
|
||||
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
|
||||
filterset_class = getattr(test_viewset, "filterset_class", None)
|
||||
if not filterset_class:
|
||||
self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None))
|
||||
|
||||
return tester
|
||||
def test_ordering(self: TestModelViewSets) -> None:
|
||||
"""Test that all ordering fields are correct"""
|
||||
view = test_viewset.as_view({"get": "list"})
|
||||
for ordering_field in test_viewset.ordering:
|
||||
with self.subTest(ordering_field):
|
||||
req = self.factory.get(
|
||||
f"/?{urlencode({'ordering': ordering_field}, doseq=True)}", user=self.user
|
||||
)
|
||||
req.tenant = get_current_tenant()
|
||||
res = view(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_search(self: TestModelViewSets) -> None:
|
||||
"""Test that search fields are correct"""
|
||||
view = test_viewset.as_view({"get": "list"})
|
||||
req = self.factory.get(
|
||||
f"/?{urlencode({'search': generate_id()}, doseq=True)}", user=self.user
|
||||
)
|
||||
req.tenant = get_current_tenant()
|
||||
res = view(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
cases = {
|
||||
"attrs": test_attrs,
|
||||
}
|
||||
if full:
|
||||
cases["ordering"] = test_ordering
|
||||
cases["search"] = test_search
|
||||
return cases
|
||||
|
||||
|
||||
for _, viewset, _ in router.registry:
|
||||
if not issubclass(viewset, ModelViewSet | ReadOnlyModelViewSet):
|
||||
continue
|
||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}", viewset_tester_factory(viewset))
|
||||
full = viewset not in [VersionHistoryViewSet, DomainViewSet, TenantViewSet]
|
||||
for test, case in viewset_tester_factory(viewset, full=full).items():
|
||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}_{test}", case)
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from drf_spectacular.contrib.django_filters import (
|
||||
DjangoFilterExtension as BaseDjangoFilterExtension,
|
||||
)
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
from drf_spectacular.plumbing import (
|
||||
ResolvedComponent,
|
||||
follow_field_source,
|
||||
)
|
||||
from drf_spectacular.renderers import OpenApiJsonRenderer
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.apps import AuthentikAPIConfig
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def preprocess_schema_exclude_non_api(endpoints: list[tuple[str, Any, Any, Callable]], **kwargs):
|
||||
"""Filter out all API Views which are not mounted under /api"""
|
||||
return [
|
||||
(path, path_regex, method, callback)
|
||||
for path, path_regex, method, callback in endpoints
|
||||
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
|
||||
]
|
||||
|
||||
|
||||
def postprocess_schema_remove_unused(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Remove unused components"""
|
||||
# To check if the schema is used, render it to JSON and then substring check that
|
||||
# less efficient than walking through the tree but a lot simpler and no
|
||||
# possibility that we miss something
|
||||
raw = OpenApiJsonRenderer().render(result, renderer_context={}).decode()
|
||||
count = 0
|
||||
for key in result["components"][ResolvedComponent.SCHEMA].keys():
|
||||
schema_usages = raw.count(f"#/components/{ResolvedComponent.SCHEMA}/{key}")
|
||||
if schema_usages >= 1:
|
||||
continue
|
||||
del generator.registry[(key, ResolvedComponent.SCHEMA)]
|
||||
count += 1
|
||||
LOGGER.debug("Removing unused components", count=count)
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
return result
|
||||
|
||||
|
||||
class DjangoFilterExtension(BaseDjangoFilterExtension):
|
||||
"""
|
||||
From https://github.com/netbox-community/netbox/pull/21521:
|
||||
|
||||
Overrides drf-spectacular's DjangoFilterExtension to fix a regression in v0.29.0 where
|
||||
_get_model_field() incorrectly double-appends to_field_name when field_name already ends
|
||||
with that value (e.g. field_name='tags__slug', to_field_name='slug' produces the invalid
|
||||
path ['tags', 'slug', 'slug']). This caused hundreds of spurious warnings during schema
|
||||
generation for filters such as TagFilter, TenancyFilterSet.tenant, and OwnerFilterMixin.owner.
|
||||
|
||||
See: https://github.com/netbox-community/netbox/issues/20787
|
||||
https://github.com/tfranzel/drf-spectacular/issues/1475
|
||||
"""
|
||||
|
||||
priority = 1
|
||||
|
||||
def _get_model_field(self, filter_field, model):
|
||||
if not filter_field.field_name:
|
||||
return None
|
||||
path = filter_field.field_name.split("__")
|
||||
to_field_name = filter_field.extra.get("to_field_name")
|
||||
if to_field_name is not None and path[-1] != to_field_name:
|
||||
path.append(to_field_name)
|
||||
return follow_field_source(model, path, emit_warnings=False)
|
||||
@@ -1,287 +0,0 @@
|
||||
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from enum import Enum
|
||||
|
||||
from django.db.models import Choices
|
||||
from django.utils.translation import get_language
|
||||
from drf_spectacular.drainage import error, warn
|
||||
from drf_spectacular.hooks import postprocess_schema_enum_id_removal
|
||||
from drf_spectacular.plumbing import (
|
||||
ResolvedComponent,
|
||||
deep_import_string,
|
||||
list_hash,
|
||||
safe_ref,
|
||||
)
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from inflection import camelize
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
# See https://github.com/tfranzel/drf-spectacular/blob/master/drf_spectacular/hooks.py
|
||||
# and https://github.com/tfranzel/drf-spectacular/issues/520
|
||||
def postprocess_schema_enums(result, generator, **kwargs): # noqa: PLR0912, PLR0915
|
||||
"""
|
||||
simple replacement of Enum/Choices that globally share the same name and have
|
||||
the same choices. Aids client generation to not generate a separate enum for
|
||||
every occurrence. only takes effect when replacement is guaranteed to be correct.
|
||||
"""
|
||||
|
||||
def is_enum_prop(prop_schema):
|
||||
return (
|
||||
"enum" in prop_schema
|
||||
or prop_schema.get("type") == "array"
|
||||
and "enum" in prop_schema.get("items", {})
|
||||
)
|
||||
|
||||
def iter_field_schemas():
|
||||
def iter_prop_containers(schema, component_name=None):
|
||||
if not component_name:
|
||||
for _component_name, _schema in schema.items():
|
||||
if spectacular_settings.COMPONENT_SPLIT_PATCH:
|
||||
_component_name = re.sub("^Patched(.+)", r"\1", _component_name)
|
||||
if spectacular_settings.COMPONENT_SPLIT_REQUEST:
|
||||
_component_name = re.sub("(.+)Request$", r"\1", _component_name)
|
||||
yield from iter_prop_containers(_schema, _component_name)
|
||||
elif isinstance(schema, list):
|
||||
for item in schema:
|
||||
yield from iter_prop_containers(item, component_name)
|
||||
elif isinstance(schema, dict):
|
||||
if schema.get("properties"):
|
||||
yield component_name, schema["properties"]
|
||||
yield from iter_prop_containers(schema.get("oneOf", []), component_name)
|
||||
yield from iter_prop_containers(schema.get("allOf", []), component_name)
|
||||
yield from iter_prop_containers(schema.get("anyOf", []), component_name)
|
||||
|
||||
def iter_path_parameters():
|
||||
for path in result.get("paths", {}).values():
|
||||
for operation in path.values():
|
||||
for parameter in operation.get("parameters", []):
|
||||
parameter_schema = parameter.get("schema", {})
|
||||
if is_enum_prop(parameter_schema):
|
||||
# Move description into enum schema
|
||||
if "description" in parameter:
|
||||
parameter_schema["description"] = parameter.pop("description")
|
||||
if "name" not in parameter:
|
||||
continue
|
||||
yield "", {parameter["name"]: parameter_schema}
|
||||
|
||||
component_schemas = result.get("components", {}).get("schemas", {})
|
||||
|
||||
yield from iter_prop_containers(component_schemas)
|
||||
yield from iter_path_parameters()
|
||||
|
||||
def create_enum_component(name, schema):
|
||||
component = ResolvedComponent(
|
||||
name=name,
|
||||
type=ResolvedComponent.SCHEMA,
|
||||
schema=schema,
|
||||
object=name,
|
||||
)
|
||||
generator.registry.register_on_missing(component)
|
||||
return component
|
||||
|
||||
def extract_hash(schema):
|
||||
if "x-spec-enum-id" in schema:
|
||||
# try to use the injected enum hash first as it generated from (name, value) tuples,
|
||||
# which prevents collisions on choice sets only differing in labels not values.
|
||||
return schema["x-spec-enum-id"]
|
||||
else:
|
||||
# fall back to actual list hashing when we encounter enums not generated by us.
|
||||
# remove blank/null entry for hashing. will be reconstructed in the last step
|
||||
return list_hash([(i, i) for i in schema["enum"] if i not in ("", None)])
|
||||
|
||||
overrides = load_enum_name_overrides()
|
||||
|
||||
prop_hash_mapping = defaultdict(set)
|
||||
hash_name_mapping = defaultdict(set)
|
||||
# collect all enums, their names and choice sets
|
||||
for component_name, props in iter_field_schemas():
|
||||
for prop_name, prop_schema in props.items():
|
||||
_prop_schema = prop_schema
|
||||
if prop_schema.get("type") == "array":
|
||||
_prop_schema = prop_schema.get("items", {})
|
||||
if "enum" not in _prop_schema:
|
||||
continue
|
||||
|
||||
prop_enum_cleaned_hash = extract_hash(_prop_schema)
|
||||
prop_hash_mapping[prop_name].add(prop_enum_cleaned_hash)
|
||||
hash_name_mapping[prop_enum_cleaned_hash].add((component_name, prop_name))
|
||||
|
||||
# get the suffix to be used for enums from settings
|
||||
enum_suffix = spectacular_settings.ENUM_SUFFIX
|
||||
|
||||
# traverse all enum properties and generate a name for the choice set. naming collisions
|
||||
# are resolved and a warning is emitted. giving a choice set multiple names is technically
|
||||
# correct but potentially unwanted. also emit a warning there to make the user aware.
|
||||
enum_name_mapping = {}
|
||||
for prop_name, prop_hash_set in prop_hash_mapping.items():
|
||||
for prop_hash in prop_hash_set:
|
||||
if prop_hash in overrides:
|
||||
enum_name = overrides[prop_hash]
|
||||
elif len(prop_hash_set) == 1:
|
||||
# prop_name has been used exclusively for one choice set (best case)
|
||||
enum_name = f"{camelize(prop_name)}{enum_suffix}"
|
||||
elif len(hash_name_mapping[prop_hash]) == 1:
|
||||
# prop_name has multiple choice sets, but each one limited to one component only
|
||||
component_name, _ = next(iter(hash_name_mapping[prop_hash]))
|
||||
enum_name = f"{camelize(component_name)}{camelize(prop_name)}{enum_suffix}"
|
||||
else:
|
||||
enum_name = f"{camelize(prop_name)}{prop_hash[:3].capitalize()}{enum_suffix}"
|
||||
warn(
|
||||
f"enum naming encountered a non-optimally resolvable collision for fields "
|
||||
f'named "{prop_name}". The same name has been used for multiple choice sets '
|
||||
f'in multiple components. The collision was resolved with "{enum_name}". '
|
||||
f"add an entry to ENUM_NAME_OVERRIDES to fix the naming."
|
||||
)
|
||||
if enum_name_mapping.get(prop_hash, enum_name) != enum_name:
|
||||
warn(
|
||||
f"encountered multiple names for the same choice set ({enum_name}). This "
|
||||
f"may be unwanted even though the generated schema is technically correct. "
|
||||
f"Add an entry to ENUM_NAME_OVERRIDES to fix the naming."
|
||||
)
|
||||
del enum_name_mapping[prop_hash]
|
||||
else:
|
||||
enum_name_mapping[prop_hash] = enum_name
|
||||
enum_name_mapping[(prop_hash, prop_name)] = enum_name
|
||||
|
||||
# replace all enum occurrences with a enum schema component. cut out the
|
||||
# enum, replace it with a reference and add a corresponding component.
|
||||
for _, props in iter_field_schemas():
|
||||
for prop_name, _prop_schema in props.items():
|
||||
prop_schema = _prop_schema
|
||||
is_array = prop_schema.get("type") == "array"
|
||||
if is_array:
|
||||
prop_schema = prop_schema.get("items", {})
|
||||
|
||||
if "enum" not in prop_schema:
|
||||
continue
|
||||
|
||||
prop_enum_original_list = prop_schema["enum"]
|
||||
prop_schema["enum"] = [i for i in prop_schema["enum"] if i not in ["", None]]
|
||||
prop_hash = extract_hash(prop_schema)
|
||||
# when choice sets are reused under multiple names, the generated name cannot be
|
||||
# resolved from the hash alone. fall back to prop_name and hash for resolution.
|
||||
enum_name = enum_name_mapping.get(prop_hash) or enum_name_mapping[prop_hash, prop_name]
|
||||
|
||||
# split property into remaining property and enum component parts
|
||||
enum_schema = {k: v for k, v in prop_schema.items() if k in ["type", "enum"]}
|
||||
prop_schema = {
|
||||
k: v for k, v in prop_schema.items() if k not in ["type", "enum", "x-spec-enum-id"]
|
||||
}
|
||||
|
||||
# separate actual description from name-value tuples
|
||||
if spectacular_settings.ENUM_GENERATE_CHOICE_DESCRIPTION:
|
||||
if prop_schema.get("description", "").startswith("*"):
|
||||
enum_schema["description"] = prop_schema.pop("description")
|
||||
elif "\n\n*" in prop_schema.get("description", ""):
|
||||
_, _, post = prop_schema["description"].partition("\n\n*")
|
||||
enum_schema["description"] = "*" + post
|
||||
|
||||
components = [create_enum_component(enum_name, schema=enum_schema)]
|
||||
if spectacular_settings.ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE:
|
||||
if "" in prop_enum_original_list:
|
||||
components.append(
|
||||
create_enum_component(f"Blank{enum_suffix}", schema={"enum": [""]})
|
||||
)
|
||||
if None in prop_enum_original_list:
|
||||
if spectacular_settings.OAS_VERSION.startswith("3.1"):
|
||||
components.append(
|
||||
create_enum_component(f"Null{enum_suffix}", schema={"type": "null"})
|
||||
)
|
||||
else:
|
||||
components.append(
|
||||
create_enum_component(f"Null{enum_suffix}", schema={"enum": [None]})
|
||||
)
|
||||
|
||||
# undo OAS 3.1 type list NULL construction as we cover
|
||||
# this in a separate component already
|
||||
if spectacular_settings.OAS_VERSION.startswith("3.1") and isinstance(
|
||||
enum_schema["type"], list
|
||||
):
|
||||
enum_schema["type"] = [t for t in enum_schema["type"] if t != "null"][0]
|
||||
|
||||
if len(components) == 1:
|
||||
prop_schema.update(components[0].ref)
|
||||
else:
|
||||
prop_schema.update({"oneOf": [c.ref for c in components]})
|
||||
|
||||
patch_target = props[prop_name] # noqa: PLR1733
|
||||
if is_array:
|
||||
patch_target = patch_target["items"]
|
||||
|
||||
# Replace existing schema information with reference
|
||||
patch_target.clear()
|
||||
patch_target.update(safe_ref(prop_schema))
|
||||
|
||||
# sort again with additional components
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
|
||||
# remove remaining ids that were not part of this hook (operation parameters mainly)
|
||||
postprocess_schema_enum_id_removal(result, generator)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Fixed version of `load_enum_name_overrides()` with a LRU cache based on language
|
||||
# *and* enum overrides.
|
||||
# Without this, API generation breaks if there is more than 1 API present (such as in split APIs)
|
||||
# Original source: drf-spectacular/drf_spectacular/plumbing.py
|
||||
def load_enum_name_overrides():
|
||||
cache_key = get_language() or ""
|
||||
|
||||
for k, v in sorted(spectacular_settings.ENUM_NAME_OVERRIDES.items()):
|
||||
cache_key += f";{k}:{v}"
|
||||
|
||||
return _load_enum_name_overrides(cache_key)
|
||||
|
||||
|
||||
# Original source: drf-spectacular/drf_spectacular/plumbing.py
|
||||
# Only change: cache_key argument instead of language.
|
||||
@functools.lru_cache
|
||||
def _load_enum_name_overrides(cache_key):
|
||||
overrides = {}
|
||||
for name, _choices in spectacular_settings.ENUM_NAME_OVERRIDES.items():
|
||||
choices = _choices
|
||||
if isinstance(choices, str):
|
||||
choices = deep_import_string(choices)
|
||||
if not choices:
|
||||
warn(
|
||||
f"unable to load choice override for {name} from ENUM_NAME_OVERRIDES. "
|
||||
f"please check module path string."
|
||||
)
|
||||
continue
|
||||
if inspect.isclass(choices) and issubclass(choices, Choices):
|
||||
choices = choices.choices
|
||||
if inspect.isclass(choices) and issubclass(choices, Enum):
|
||||
choices = [(c.value, c.name) for c in choices]
|
||||
normalized_choices = []
|
||||
for choice in choices:
|
||||
# Allow None values in the simple values list case
|
||||
if isinstance(choice, str) or choice is None:
|
||||
# TODO warning
|
||||
normalized_choices.append((choice, choice)) # simple choice list
|
||||
elif isinstance(choice[1], (list, tuple)):
|
||||
normalized_choices.extend(choice[1]) # categorized nested choices
|
||||
else:
|
||||
normalized_choices.append(choice) # normal 2-tuple form
|
||||
|
||||
# Get all of choice values that should be used in the hash, blank and
|
||||
# None values get excluded in the post-processing hook for enum overrides,
|
||||
# so we do the same here to ensure the hashes match
|
||||
hashable_values = [
|
||||
(value, label) for value, label in normalized_choices if value not in ["", None]
|
||||
]
|
||||
overrides[list_hash(hashable_values)] = name
|
||||
|
||||
if len(spectacular_settings.ENUM_NAME_OVERRIDES) != len(overrides):
|
||||
error(
|
||||
"ENUM_NAME_OVERRIDES has duplication issues. Encountered multiple names "
|
||||
"for the same choice set. Enum naming might be unexpected."
|
||||
)
|
||||
return overrides
|
||||
@@ -1,32 +0,0 @@
|
||||
from drf_spectacular.plumbing import (
|
||||
ResolvedComponent,
|
||||
build_basic_type,
|
||||
build_object_type,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
PAGINATION = ResolvedComponent(
|
||||
name="Pagination",
|
||||
type=ResolvedComponent.SCHEMA,
|
||||
object="Pagination",
|
||||
schema=build_object_type(
|
||||
properties={
|
||||
"next": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"previous": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"count": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"current": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"total_pages": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"start_index": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"end_index": build_basic_type(OpenApiTypes.NUMBER),
|
||||
},
|
||||
required=[
|
||||
"next",
|
||||
"previous",
|
||||
"count",
|
||||
"current",
|
||||
"total_pages",
|
||||
"start_index",
|
||||
"end_index",
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -1,17 +1,10 @@
|
||||
from typing import Any
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
from drf_spectacular.plumbing import (
|
||||
ResolvedComponent,
|
||||
build_basic_type,
|
||||
build_parameter_type,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
QUERY_PARAMS = {
|
||||
"ordering": ResolvedComponent(
|
||||
@@ -70,18 +63,3 @@ QUERY_PARAMS = {
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def postprocess_schema_query_params(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Optimize pagination parameters, instead of redeclaring parameters for each endpoint
|
||||
declare them globally and refer to them"""
|
||||
LOGGER.debug("Deduplicating query parameters")
|
||||
for path in result["paths"].values():
|
||||
for method in path.values():
|
||||
for idx, param in enumerate(method.get("parameters", [])):
|
||||
if param["name"] not in QUERY_PARAMS:
|
||||
continue
|
||||
method["parameters"][idx] = QUERY_PARAMS[param["name"]].ref
|
||||
return result
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
from typing import Any
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
from drf_spectacular.plumbing import (
|
||||
ResolvedComponent,
|
||||
build_array_type,
|
||||
build_basic_type,
|
||||
build_object_type,
|
||||
)
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework.settings import api_settings
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.v3.schema.pagination import PAGINATION
|
||||
from authentik.api.v3.schema.query import QUERY_PARAMS
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
GENERIC_ERROR = ResolvedComponent(
|
||||
name="GenericError",
|
||||
@@ -67,40 +57,28 @@ VALIDATION_ERROR_RESPONSE = ResolvedComponent(
|
||||
"description": "",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def postprocess_schema_register(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Register custom schema components"""
|
||||
LOGGER.debug("Registering custom schemas")
|
||||
generator.registry.register_on_missing(PAGINATION)
|
||||
generator.registry.register_on_missing(GENERIC_ERROR)
|
||||
generator.registry.register_on_missing(GENERIC_ERROR_RESPONSE)
|
||||
generator.registry.register_on_missing(VALIDATION_ERROR)
|
||||
generator.registry.register_on_missing(VALIDATION_ERROR_RESPONSE)
|
||||
for query in QUERY_PARAMS.values():
|
||||
generator.registry.register_on_missing(query)
|
||||
return result
|
||||
|
||||
|
||||
def postprocess_schema_responses(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Default error responses"""
|
||||
LOGGER.debug("Adding default error responses")
|
||||
for path in result["paths"].values():
|
||||
for method in path.values():
|
||||
method["responses"].setdefault("400", VALIDATION_ERROR_RESPONSE.ref)
|
||||
method["responses"].setdefault("403", GENERIC_ERROR_RESPONSE.ref)
|
||||
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
|
||||
# This is a workaround for authentik/stages/prompt/stage.py
|
||||
# since the serializer PromptChallengeResponse
|
||||
# accepts dynamic keys
|
||||
for component in result["components"]["schemas"]:
|
||||
if component == "PromptChallengeResponseRequest":
|
||||
comp = result["components"]["schemas"][component]
|
||||
comp["additionalProperties"] = {}
|
||||
return result
|
||||
PAGINATION = ResolvedComponent(
|
||||
name="Pagination",
|
||||
type=ResolvedComponent.SCHEMA,
|
||||
object="Pagination",
|
||||
schema=build_object_type(
|
||||
properties={
|
||||
"next": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"previous": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"count": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"current": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"total_pages": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"start_index": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"end_index": build_basic_type(OpenApiTypes.NUMBER),
|
||||
},
|
||||
required=[
|
||||
"next",
|
||||
"previous",
|
||||
"count",
|
||||
"current",
|
||||
"total_pages",
|
||||
"start_index",
|
||||
"end_index",
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from drf_spectacular.plumbing import ResolvedComponent, build_object_type
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
|
||||
|
||||
AUTOCOMPLETE_SCHEMA = ResolvedComponent(
|
||||
name="Autocomplete",
|
||||
object="Autocomplete",
|
||||
type=ResolvedComponent.SCHEMA,
|
||||
schema=build_object_type(additionalProperties={}),
|
||||
)
|
||||
|
||||
|
||||
def postprocess_schema_search_autocomplete(result, generator: SchemaGenerator, **kwargs):
|
||||
generator.registry.register_on_missing(AUTOCOMPLETE_SCHEMA)
|
||||
|
||||
return result
|
||||
@@ -1,60 +1,24 @@
|
||||
"""Serializer mixin for managed models"""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
DateTimeField,
|
||||
FileField,
|
||||
)
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, DateTimeField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ListSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.validation import validate
|
||||
from authentik.blueprints.models import BlueprintInstance
|
||||
from authentik.blueprints.v1.common import Blueprint
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.blueprints.v1.oci import OCI_PREFIX
|
||||
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.events.logs import LogEventSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
|
||||
def get_blueprints():
|
||||
if settings.DEBUG:
|
||||
return blueprints_find_dict()
|
||||
return blueprints_find_dict.send().get_result(block=True)
|
||||
|
||||
|
||||
class BlueprintUploadSerializer(PassiveSerializer):
|
||||
"""Serializer to upload file"""
|
||||
|
||||
file = FileField(required=False)
|
||||
path = CharField(required=False)
|
||||
|
||||
def validate_path(self, path: str) -> str:
|
||||
"""Ensure the path (if set) specified is retrievable"""
|
||||
if path == "":
|
||||
return path
|
||||
files: list[dict] = get_blueprints()
|
||||
if path not in [file["path"] for file in files]:
|
||||
raise ValidationError(_("Blueprint file does not exist"))
|
||||
return path
|
||||
|
||||
|
||||
class ManagedSerializer:
|
||||
"""Managed Serializer"""
|
||||
|
||||
@@ -75,7 +39,7 @@ class BlueprintInstanceSerializer(ModelSerializer):
|
||||
"""Ensure the path (if set) specified is retrievable"""
|
||||
if path == "" or path.startswith(OCI_PREFIX):
|
||||
return path
|
||||
files: list[dict] = get_blueprints()
|
||||
files: list[dict] = blueprints_find_dict.send().get_result(block=True)
|
||||
if path not in [file["path"] for file in files]:
|
||||
raise ValidationError(_("Blueprint file does not exist"))
|
||||
return path
|
||||
@@ -124,33 +88,6 @@ class BlueprintInstanceSerializer(ModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
def check_blueprint_perms(blueprint: Blueprint, user: User, explicit_action: str | None = None):
|
||||
"""Check for individual permissions for each model in a blueprint"""
|
||||
for entry in blueprint.entries:
|
||||
full_model = entry.get_model(blueprint)
|
||||
app, __, model = full_model.partition(".")
|
||||
perms = [
|
||||
f"{app}.add_{model}",
|
||||
f"{app}.change_{model}",
|
||||
f"{app}.delete_{model}",
|
||||
]
|
||||
if explicit_action:
|
||||
perms = [f"{app}.{explicit_action}_{model}"]
|
||||
for perm in perms:
|
||||
if not user.has_perm(perm):
|
||||
raise PermissionDenied(
|
||||
{
|
||||
entry.id: _(
|
||||
"User lacks permission to create {model}".format_map(
|
||||
{
|
||||
"model": full_model,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Blueprint instances"""
|
||||
|
||||
@@ -160,12 +97,6 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
filterset_fields = ["name", "path"]
|
||||
ordering = ["name"]
|
||||
|
||||
class BlueprintImportResultSerializer(PassiveSerializer):
|
||||
"""Logs of an attempted blueprint import"""
|
||||
|
||||
logs = LogEventSerializer(many=True, read_only=True)
|
||||
success = BooleanField(read_only=True)
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: ListSerializer(
|
||||
@@ -184,7 +115,7 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def available(self, request: Request) -> Response:
|
||||
"""Get blueprints"""
|
||||
files: list[dict] = get_blueprints()
|
||||
files: list[dict] = blueprints_find_dict.send().get_result(block=True)
|
||||
return Response(files)
|
||||
|
||||
@permission_required("authentik_blueprints.view_blueprintinstance")
|
||||
@@ -200,53 +131,3 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
blueprint = self.get_object()
|
||||
apply_blueprint.send_with_options(args=(blueprint.pk,), rel_obj=blueprint)
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
@extend_schema(
|
||||
request={"multipart/form-data": BlueprintUploadSerializer},
|
||||
responses={
|
||||
204: BlueprintImportResultSerializer,
|
||||
400: BlueprintImportResultSerializer,
|
||||
},
|
||||
)
|
||||
@action(url_path="import", detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
|
||||
@validate(
|
||||
BlueprintUploadSerializer,
|
||||
)
|
||||
def import_(self, request: Request, body: BlueprintUploadSerializer) -> Response:
|
||||
"""Import blueprint from .yaml file and apply it once, without creating an instance"""
|
||||
string_contents = ""
|
||||
if body.validated_data.get("file"):
|
||||
file = cast(InMemoryUploadedFile, body.validated_data["file"])
|
||||
string_contents = file.read().decode()
|
||||
elif body.validated_data.get("path"):
|
||||
string_contents = BlueprintInstance(
|
||||
path=body.validated_data.get("path")
|
||||
).retrieve_file()
|
||||
else:
|
||||
raise ValidationError("Either path or file must be set")
|
||||
importer = Importer.from_string(string_contents)
|
||||
|
||||
check_blueprint_perms(importer.blueprint, request.user)
|
||||
|
||||
valid, logs = importer.validate()
|
||||
|
||||
import_response = self.BlueprintImportResultSerializer(
|
||||
data={
|
||||
"logs": [],
|
||||
"success": False,
|
||||
}
|
||||
)
|
||||
import_response.is_valid(raise_exception=True)
|
||||
|
||||
import_response.initial_data["logs"] = [LogEventSerializer(log).data for log in logs]
|
||||
import_response.initial_data["success"] = valid
|
||||
import_response.is_valid()
|
||||
if not valid:
|
||||
return Response(data=import_response.initial_data, status=200)
|
||||
|
||||
successful = importer.apply()
|
||||
import_response.initial_data["success"] = successful
|
||||
import_response.is_valid()
|
||||
if not successful:
|
||||
return Response(data=import_response.initial_data, status=200)
|
||||
return Response(data=import_response.initial_data, status=200)
|
||||
|
||||
@@ -272,7 +272,7 @@ class Importer:
|
||||
and entry.state != BlueprintEntryDesiredState.MUST_CREATED
|
||||
):
|
||||
self.logger.debug(
|
||||
"Initialize serializer with instance",
|
||||
"Initialise serializer with instance",
|
||||
model=model,
|
||||
instance=model_instance,
|
||||
pk=model_instance.pk,
|
||||
@@ -290,7 +290,7 @@ class Importer:
|
||||
)
|
||||
else:
|
||||
self.logger.debug(
|
||||
"Initialized new serializer instance",
|
||||
"Initialised new serializer instance",
|
||||
model=model,
|
||||
**cleanse_dict(updated_identifiers),
|
||||
)
|
||||
|
||||
@@ -101,23 +101,13 @@ class Brand(SerializerModel):
|
||||
"""Get themed URLs for branding_favicon if it contains %(theme)s"""
|
||||
return get_file_manager(FileUsage.MEDIA).themed_urls(self.branding_favicon)
|
||||
|
||||
def branding_default_flow_background_url(self, request=None, use_cache: bool = True) -> str:
|
||||
def branding_default_flow_background_url(self) -> str:
|
||||
"""Get branding_default_flow_background URL"""
|
||||
return get_file_manager(FileUsage.MEDIA).file_url(
|
||||
self.branding_default_flow_background,
|
||||
request,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
return get_file_manager(FileUsage.MEDIA).file_url(self.branding_default_flow_background)
|
||||
|
||||
def branding_default_flow_background_themed_urls(
|
||||
self, request=None, use_cache: bool = True
|
||||
) -> dict[str, str] | None:
|
||||
def branding_default_flow_background_themed_urls(self) -> dict[str, str] | None:
|
||||
"""Get themed URLs for branding_default_flow_background if it contains %(theme)s"""
|
||||
return get_file_manager(FileUsage.MEDIA).themed_urls(
|
||||
self.branding_default_flow_background,
|
||||
request,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
return get_file_manager(FileUsage.MEDIA).themed_urls(self.branding_default_flow_background)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
|
||||
@@ -21,9 +21,6 @@ PROMPT_CONSENT = "consent"
|
||||
PROMPT_LOGIN = "login"
|
||||
|
||||
PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS = "goauthentik.io/providers/oauth2/iframe_sessions"
|
||||
PLAN_CONTEXT_POST_LOGOUT_REDIRECT_URI = "goauthentik.io/providers/oauth2/post_logout_redirect_uri"
|
||||
|
||||
OAUTH2_BINDING = "redirect"
|
||||
|
||||
SCOPE_OPENID = "openid"
|
||||
SCOPE_OPENID_PROFILE = "profile"
|
||||
@@ -40,9 +37,6 @@ TOKEN_TYPE = "Bearer" # nosec
|
||||
|
||||
SCOPE_AUTHENTIK_API = "goauthentik.io/api"
|
||||
|
||||
# URI schemes that are forbidden for redirect URIs
|
||||
FORBIDDEN_URI_SCHEMES = {"javascript", "data", "vbscript"}
|
||||
|
||||
# Read/write full user (including email)
|
||||
SCOPE_GITHUB_USER = "user"
|
||||
# Read user (without email)
|
||||
|
||||
@@ -6,7 +6,6 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.core.api.object_attributes import AttributesMixinSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.core.models import (
|
||||
@@ -15,7 +14,7 @@ from authentik.core.models import (
|
||||
)
|
||||
|
||||
|
||||
class ApplicationEntitlementSerializer(AttributesMixinSerializer, ModelSerializer):
|
||||
class ApplicationEntitlementSerializer(ModelSerializer):
|
||||
"""ApplicationEntitlement Serializer"""
|
||||
|
||||
def validate_app(self, app: Application) -> Application:
|
||||
@@ -48,7 +47,8 @@ class ApplicationEntitlementViewSet(UsedByMixin, ModelViewSet):
|
||||
search_fields = [
|
||||
"pbm_uuid",
|
||||
"name",
|
||||
"app",
|
||||
"app__name",
|
||||
"app__slug",
|
||||
"attributes",
|
||||
]
|
||||
filterset_fields = [
|
||||
|
||||
@@ -25,7 +25,6 @@ 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 ModelSerializer, ThemedUrlsSerializer
|
||||
from authentik.core.apps import AppAccessWithoutBindings
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.events.logs import LogEventSerializer, capture_logs
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||
@@ -160,16 +159,15 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
return queryset
|
||||
|
||||
def _get_allowed_applications(
|
||||
self, paginated_apps: Iterator[Application], user: User | None = None
|
||||
self, pagined_apps: Iterator[Application], user: User | None = None
|
||||
) -> list[Application]:
|
||||
applications = []
|
||||
request = self.request._request
|
||||
if user:
|
||||
request = copy(request)
|
||||
request.user = user
|
||||
for application in paginated_apps:
|
||||
for application in pagined_apps:
|
||||
engine = PolicyEngine(application, request.user, request)
|
||||
engine.empty_result = AppAccessWithoutBindings.get()
|
||||
engine.build()
|
||||
if engine.passing:
|
||||
applications.append(application)
|
||||
@@ -227,7 +225,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
if not for_user:
|
||||
raise ValidationError({"for_user": "User not found"})
|
||||
engine = PolicyEngine(application, for_user, request)
|
||||
engine.empty_result = AppAccessWithoutBindings.get()
|
||||
engine.use_cache = False
|
||||
with capture_logs() as logs:
|
||||
engine.build()
|
||||
|
||||
@@ -32,19 +32,19 @@ from authentik.rbac.decorators import permission_required
|
||||
class UserAgentDeviceDict(TypedDict):
|
||||
"""User agent device"""
|
||||
|
||||
brand: str
|
||||
brand: str | None = None
|
||||
family: str
|
||||
model: str
|
||||
model: str | None = None
|
||||
|
||||
|
||||
class UserAgentOSDict(TypedDict):
|
||||
"""User agent os"""
|
||||
|
||||
family: str
|
||||
major: str
|
||||
minor: str
|
||||
patch: str
|
||||
patch_minor: str
|
||||
major: str | None = None
|
||||
minor: str | None = None
|
||||
patch: str | None = None
|
||||
patch_minor: str | None = None
|
||||
|
||||
|
||||
class UserAgentBrowserDict(TypedDict):
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.http import Http404
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from djangoql.schema import BoolField, StrField
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
OpenApiResponse,
|
||||
@@ -26,11 +25,7 @@ from rest_framework.serializers import ListSerializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.authentication import TokenAuthentication
|
||||
from authentik.api.search.fields import (
|
||||
JSONSearchField,
|
||||
)
|
||||
from authentik.api.validation import validate
|
||||
from authentik.core.api.object_attributes import AttributesMixinSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import Group, User
|
||||
@@ -76,7 +71,7 @@ class RelatedGroupSerializer(ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class GroupSerializer(AttributesMixinSerializer, ModelSerializer):
|
||||
class GroupSerializer(ModelSerializer):
|
||||
"""Group Serializer"""
|
||||
|
||||
attributes = JSONDictField(required=False)
|
||||
@@ -270,6 +265,12 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
]
|
||||
|
||||
def get_ql_fields(self):
|
||||
from djangoql.schema import BoolField, StrField
|
||||
|
||||
from authentik.enterprise.search.fields import (
|
||||
JSONSearchField,
|
||||
)
|
||||
|
||||
return [
|
||||
StrField(Group, "name"),
|
||||
BoolField(Group, "is_superuser", nullable=True),
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, SerializerMethodField
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.core.models import AttributesMixin, ObjectAttribute
|
||||
from authentik.lib.utils.dict import get_path_from_dict
|
||||
|
||||
|
||||
class AttributesMixinSerializer(ModelSerializer):
|
||||
|
||||
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
model = self.Meta.model
|
||||
attrs = data.get("attributes", {})
|
||||
attributes = ObjectAttribute.objects.filter(
|
||||
object_type=ContentType.objects.get_for_model(model),
|
||||
enabled=True,
|
||||
)
|
||||
for attr in attributes:
|
||||
value = get_path_from_dict(attrs, attr.key)
|
||||
attr.run_validation(value)
|
||||
return data
|
||||
|
||||
|
||||
class ContentTypeSerializer(ModelSerializer):
|
||||
app_label = CharField(read_only=True)
|
||||
model = CharField(read_only=True)
|
||||
verbose_name_plural = SerializerMethodField()
|
||||
fully_qualified_model = SerializerMethodField()
|
||||
|
||||
def get_fully_qualified_model(self, ct: ContentType) -> str:
|
||||
return f"{ct.app_label}.{ct.model}"
|
||||
|
||||
def get_verbose_name_plural(self, ct: ContentType) -> str:
|
||||
return ct.model_class()._meta.verbose_name_plural
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = ("id", "app_label", "model", "verbose_name_plural", "fully_qualified_model")
|
||||
|
||||
|
||||
class ObjectAttributeSerializer(ModelSerializer):
|
||||
|
||||
object_type = CharField()
|
||||
object_type_obj = ContentTypeSerializer(read_only=True, source="object_type")
|
||||
|
||||
def validate_object_type(self, fqm: str) -> ContentType:
|
||||
app_label, _, model = fqm.partition(".")
|
||||
ct = ContentType.objects.filter(app_label=app_label, model=model).first()
|
||||
if not ct or not issubclass(ct.model_class(), AttributesMixin):
|
||||
raise ValidationError("Invalid object type")
|
||||
return ct
|
||||
|
||||
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
||||
if attrs.get("is_unique") and attrs.get("is_array"):
|
||||
raise ValidationError(_("Unique cannot be enabled for arrays."))
|
||||
return super().validate(attrs)
|
||||
|
||||
class Meta:
|
||||
model = ObjectAttribute
|
||||
fields = [
|
||||
"pk",
|
||||
"object_type",
|
||||
"object_type_obj",
|
||||
"enabled",
|
||||
"created",
|
||||
"key",
|
||||
"label",
|
||||
"last_updated",
|
||||
"regex",
|
||||
"type",
|
||||
"group",
|
||||
"managed",
|
||||
"is_unique",
|
||||
"is_required",
|
||||
"is_array",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"last_updated": {"read_only": True},
|
||||
"created": {"read_only": True},
|
||||
"pk": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
class ObjectAttributeViewSet(ModelViewSet):
|
||||
serializer_class = ObjectAttributeSerializer
|
||||
queryset = ObjectAttribute.objects.all()
|
||||
filterset_fields = ["object_type__model", "object_type__app_label", "enabled"]
|
||||
search_fields = ["key", "label", "group", "object_type__model", "object_type__app_label"]
|
||||
ordering = ["key"]
|
||||
@@ -124,7 +124,7 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Token Viewset"""
|
||||
|
||||
lookup_field = "identifier"
|
||||
queryset = Token.objects.including_expired().all()
|
||||
queryset = Token.objects.all()
|
||||
serializer_class = TokenSerializer
|
||||
search_fields = [
|
||||
"identifier",
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
from django.apps import apps
|
||||
from django.db.models import Model
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_field
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField, ListField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
@@ -12,7 +13,6 @@ from rest_framework.views import APIView
|
||||
from yaml import ScalarNode
|
||||
|
||||
from authentik.api.validation import validate
|
||||
from authentik.blueprints.api import check_blueprint_perms
|
||||
from authentik.blueprints.v1.common import (
|
||||
Blueprint,
|
||||
BlueprintEntry,
|
||||
@@ -165,7 +165,21 @@ class TransactionalApplicationView(APIView):
|
||||
def put(self, request: Request, body: TransactionApplicationSerializer) -> Response:
|
||||
"""Convert data into a blueprint, validate it and apply it"""
|
||||
blueprint: Blueprint = body.validated_data
|
||||
check_blueprint_perms(blueprint, request.user, explicit_action="add")
|
||||
for entry in blueprint.entries:
|
||||
full_model = entry.get_model(blueprint)
|
||||
app, __, model = full_model.partition(".")
|
||||
if not request.user.has_perm(f"{app}.add_{model}"):
|
||||
raise PermissionDenied(
|
||||
{
|
||||
entry.id: _(
|
||||
"User lacks permission to create {model}".format_map(
|
||||
{
|
||||
"model": full_model,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
importer = Importer(blueprint, {})
|
||||
applied = importer.apply()
|
||||
response = {"applied": False, "logs": []}
|
||||
|
||||
@@ -22,7 +22,6 @@ from django_filters.filters import (
|
||||
UUIDFilter,
|
||||
)
|
||||
from django_filters.filterset import FilterSet
|
||||
from djangoql.schema import BoolField, StrField
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
@@ -56,14 +55,9 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.authentication import TokenAuthentication
|
||||
from authentik.api.search.fields import (
|
||||
ChoiceSearchField,
|
||||
JSONSearchField,
|
||||
)
|
||||
from authentik.api.validation import validate
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.api.object_attributes import AttributesMixinSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import (
|
||||
JSONDictField,
|
||||
@@ -129,7 +123,7 @@ class PartialGroupSerializer(ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class UserSerializer(AttributesMixinSerializer, ModelSerializer):
|
||||
class UserSerializer(ModelSerializer):
|
||||
"""User Serializer"""
|
||||
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
@@ -530,6 +524,13 @@ class UserViewSet(
|
||||
]
|
||||
|
||||
def get_ql_fields(self):
|
||||
from djangoql.schema import BoolField, StrField
|
||||
|
||||
from authentik.enterprise.search.fields import (
|
||||
ChoiceSearchField,
|
||||
JSONSearchField,
|
||||
)
|
||||
|
||||
return [
|
||||
StrField(User, "username"),
|
||||
StrField(User, "name"),
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
"""authentik core app config"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.tasks.schedules.common import ScheduleSpec
|
||||
from authentik.tenants.flags import Flag
|
||||
|
||||
|
||||
class AppAccessWithoutBindings(Flag[bool], key="core_default_app_access"):
|
||||
|
||||
default = True
|
||||
visibility = "none"
|
||||
description = _(
|
||||
"Configure if applications without any policy/group/user bindings "
|
||||
"should be accessible to any user."
|
||||
)
|
||||
|
||||
|
||||
class AuthentikCoreConfig(ManagedAppConfig):
|
||||
|
||||
@@ -81,7 +81,7 @@ class TokenBackend(InbuiltBackend):
|
||||
User().set_password(password, request=request)
|
||||
return None
|
||||
|
||||
tokens = Token.objects.filter(
|
||||
tokens = Token.filter_not_expired(
|
||||
user=user, key=password, intent=TokenIntents.INTENT_APP_PASSWORD
|
||||
)
|
||||
if not tokens.exists():
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
# Generated by Django 5.2.13 on 2026-04-11 18:35
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0057_remove_user_groups_remove_user_user_permissions_and_more"),
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ObjectAttribute",
|
||||
fields=[
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("last_updated", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"managed",
|
||||
models.TextField(
|
||||
default=None,
|
||||
help_text="Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
|
||||
null=True,
|
||||
unique=True,
|
||||
verbose_name="Managed by authentik",
|
||||
),
|
||||
),
|
||||
(
|
||||
"attribute_id",
|
||||
models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
|
||||
),
|
||||
("enabled", models.BooleanField(default=True)),
|
||||
("label", models.TextField()),
|
||||
("group", models.TextField(blank=True)),
|
||||
("key", models.TextField()),
|
||||
(
|
||||
"type",
|
||||
models.TextField(
|
||||
choices=[("text", "Text"), ("number", "Number"), ("boolean", "Boolean")]
|
||||
),
|
||||
),
|
||||
("is_unique", models.BooleanField(default=False)),
|
||||
("is_required", models.BooleanField(default=False)),
|
||||
("regex", models.TextField(blank=True)),
|
||||
("is_array", models.BooleanField(default=False)),
|
||||
(
|
||||
"object_type",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Object Attribute",
|
||||
"verbose_name_plural": "Object Attributes",
|
||||
"unique_together": {("object_type", "key", "enabled")},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import re
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from enum import StrEnum
|
||||
from hashlib import sha256
|
||||
from typing import Any, Self
|
||||
@@ -13,11 +13,10 @@ from deepmerge import always_merger
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.contrib.auth.models import AbstractUser, Permission
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.sessions.base_session import AbstractBaseSession
|
||||
from django.core.validators import validate_slug
|
||||
from django.db import models
|
||||
from django.db.models import Manager, Q, QuerySet, options
|
||||
from django.db.models import Q, QuerySet, options
|
||||
from django.http import HttpRequest
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
@@ -27,7 +26,6 @@ from guardian.models import RoleModelPermission, RoleObjectPermission
|
||||
from model_utils.managers import InheritanceManager
|
||||
from psqlextra.indexes import UniqueIndex
|
||||
from psqlextra.models import PostgresMaterializedViewModel
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@@ -47,7 +45,6 @@ from authentik.lib.models import (
|
||||
SerializerModel,
|
||||
)
|
||||
from authentik.lib.utils.inheritance import get_deepest_child
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.rbac.models import Role
|
||||
@@ -520,7 +517,7 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
|
||||
@property
|
||||
def ak_groups(self):
|
||||
"""This is a proxy for a renamed, deprecated field."""
|
||||
from authentik.events.models import Event
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
deprecation = "authentik.core.models.User.ak_groups"
|
||||
replacement = "authentik.core.models.User.groups"
|
||||
@@ -547,9 +544,21 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
|
||||
cause=cause,
|
||||
stacktrace=stacktrace,
|
||||
)
|
||||
Event.log_deprecation(
|
||||
deprecation, message=message_event, cause=cause, replacement=replacement
|
||||
)
|
||||
if not Event.filter_not_expired(
|
||||
action=EventAction.CONFIGURATION_WARNING,
|
||||
context__deprecation=deprecation,
|
||||
context__cause=cause,
|
||||
).exists():
|
||||
event = Event.new(
|
||||
EventAction.CONFIGURATION_WARNING,
|
||||
deprecation=deprecation,
|
||||
replacement=replacement,
|
||||
message=message_event,
|
||||
cause=cause,
|
||||
)
|
||||
event.expires = datetime.now() + timedelta(days=30)
|
||||
event.save()
|
||||
|
||||
return self.groups
|
||||
|
||||
def set_password(self, raw_password, signal=True, sender=None, request=None):
|
||||
@@ -798,11 +807,11 @@ class Application(SerializerModel, PolicyBindingModel):
|
||||
|
||||
def backchannel_provider_for[T: Provider](self, provider_type: type[T], **kwargs) -> T | None:
|
||||
"""Get Backchannel provider for a specific type"""
|
||||
provider: BackchannelProvider | None = self.backchannel_providers.filter(
|
||||
providers = self.backchannel_providers.filter(
|
||||
**{f"{provider_type._meta.model_name}__isnull": False},
|
||||
**kwargs,
|
||||
).first()
|
||||
return getattr(provider, provider_type._meta.model_name) if provider else None
|
||||
)
|
||||
return getattr(providers.first(), provider_type._meta.model_name)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
@@ -953,34 +962,21 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
def get_icon_url(self, request=None, use_cache: bool = True) -> str | None:
|
||||
"""Get the URL to the source icon."""
|
||||
if not self.icon:
|
||||
return None
|
||||
return get_file_manager(FileUsage.MEDIA).file_url(self.icon, request, use_cache=use_cache)
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
"""Get the URL to the source icon"""
|
||||
return self.get_icon_url()
|
||||
|
||||
def get_icon_themed_urls(
|
||||
self,
|
||||
request=None,
|
||||
use_cache: bool = True,
|
||||
) -> dict[str, str] | None:
|
||||
"""Get themed URLs for icon if it contains %(theme)s."""
|
||||
if not self.icon:
|
||||
return None
|
||||
return get_file_manager(FileUsage.MEDIA).themed_urls(
|
||||
self.icon,
|
||||
request,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
|
||||
return get_file_manager(FileUsage.MEDIA).file_url(self.icon)
|
||||
|
||||
@property
|
||||
def icon_themed_urls(self) -> dict[str, str] | None:
|
||||
return self.get_icon_themed_urls()
|
||||
"""Get themed URLs for icon if it contains %(theme)s"""
|
||||
if not self.icon:
|
||||
return None
|
||||
|
||||
return get_file_manager(FileUsage.MEDIA).themed_urls(self.icon)
|
||||
|
||||
def get_user_path(self) -> str:
|
||||
"""Get user path, fallback to default for formatting errors"""
|
||||
@@ -1100,24 +1096,12 @@ class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||
unique_together = (("group", "source"),)
|
||||
|
||||
|
||||
class ExpiringManager(Manager):
|
||||
"""Manager for expiring objects which filters out expired objects by default"""
|
||||
|
||||
def get_queryset(self):
|
||||
return QuerySet(self.model, using=self._db).exclude(expires__lt=now(), expiring=True)
|
||||
|
||||
def including_expired(self):
|
||||
return QuerySet(self.model, using=self._db)
|
||||
|
||||
|
||||
class ExpiringModel(models.Model):
|
||||
"""Base Model which can expire, and is automatically cleaned up."""
|
||||
|
||||
expires = models.DateTimeField(default=None, null=True)
|
||||
expiring = models.BooleanField(default=True)
|
||||
|
||||
objects = ExpiringManager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
indexes = [
|
||||
@@ -1141,23 +1125,7 @@ class ExpiringModel(models.Model):
|
||||
def filter_not_expired(cls, **kwargs) -> QuerySet[Self]:
|
||||
"""Filer for tokens which are not expired yet or are not expiring,
|
||||
and match filters in `kwargs`"""
|
||||
from authentik.events.models import Event
|
||||
|
||||
deprecation_id = f"{class_to_path(cls)}.filter_not_expired"
|
||||
|
||||
Event.log_deprecation(
|
||||
deprecation_id,
|
||||
message=(
|
||||
".filter_not_expired() is deprecated as the default lookup now excludes "
|
||||
"expired objects."
|
||||
),
|
||||
)
|
||||
|
||||
for obj in (
|
||||
cls.objects.including_expired()
|
||||
.filter(**kwargs)
|
||||
.filter(Q(expires__lt=now(), expiring=True))
|
||||
):
|
||||
for obj in cls.objects.filter(**kwargs).filter(Q(expires__lt=now(), expiring=True)):
|
||||
obj.delete()
|
||||
return cls.objects.filter(**kwargs)
|
||||
|
||||
@@ -1360,61 +1328,3 @@ class AuthenticatedSession(SerializerModel):
|
||||
session=Session.objects.filter(session_key=request.session.session_key).first(),
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
class ObjectAttribute(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
"""User-defined schema for models' `attributes` JSON field."""
|
||||
|
||||
class AttributeType(models.TextChoices):
|
||||
TEXT = "text"
|
||||
NUMBER = "number"
|
||||
BOOLEAN = "boolean"
|
||||
|
||||
attribute_id = models.UUIDField(default=uuid4, primary_key=True)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
object_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
label = models.TextField()
|
||||
group = models.TextField(blank=True)
|
||||
key = models.TextField()
|
||||
|
||||
type = models.TextField(choices=AttributeType.choices)
|
||||
is_unique = models.BooleanField(default=False)
|
||||
is_required = models.BooleanField(default=False)
|
||||
regex = models.TextField(blank=True)
|
||||
is_array = models.BooleanField(default=False)
|
||||
|
||||
def run_validation(self, value: Any) -> None:
|
||||
err_key = f"attributes_{self.key.replace(".", "_")}"
|
||||
if self.is_required and value is None:
|
||||
raise ValidationError({err_key: _("This field is required")})
|
||||
if self.is_array:
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise ValidationError({err_key: _("Value must be an array.")})
|
||||
if self.regex != "":
|
||||
if not all(re.fullmatch(self.regex, v) for v in value):
|
||||
raise ValidationError({err_key: _("Value does not match configured pattern.")})
|
||||
else:
|
||||
if self.is_unique:
|
||||
model: type[models.Model] = self.object_type.model_class()
|
||||
lookup_key = f"attributes__{self.key.replace(".", "__")}"
|
||||
if model.objects.filter(**{lookup_key: value}).exists():
|
||||
raise ValidationError({err_key: _("Value is not unique.")})
|
||||
if self.regex != "":
|
||||
if not re.fullmatch(self.regex, value):
|
||||
raise ValidationError({err_key: _("Value does not match configured pattern.")})
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.core.api.object_attributes import ObjectAttributeSerializer
|
||||
|
||||
return ObjectAttributeSerializer
|
||||
|
||||
def __str__(self):
|
||||
return f"Object attribute '{self.key}' for content type {self.object_type_id}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Object Attribute")
|
||||
verbose_name_plural = _("Object Attributes")
|
||||
unique_together = (("object_type", "key", "enabled"),)
|
||||
|
||||
@@ -72,7 +72,6 @@ class SessionStore(SessionBase):
|
||||
# and their descriptors fail to initialize (e.g., missing storage)
|
||||
# TypeError - can happen with incompatible pickled objects
|
||||
# If any of these happen, just return an empty dictionary (an empty session)
|
||||
LOGGER.warning("Failed to decode session data", exc_info=True)
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
@@ -27,10 +27,7 @@ def clean_expired_models():
|
||||
for cls in ExpiringModel.__subclasses__():
|
||||
cls: ExpiringModel
|
||||
objects = (
|
||||
cls.objects.including_expired()
|
||||
.all()
|
||||
.exclude(expiring=False)
|
||||
.exclude(expiring=True, expires__gt=now())
|
||||
cls.objects.all().exclude(expiring=False).exclude(expiring=True, expires__gt=now())
|
||||
)
|
||||
amount = objects.count()
|
||||
for obj in chunked_queryset(objects):
|
||||
|
||||
@@ -21,10 +21,6 @@
|
||||
{% block head_before %}
|
||||
{% endblock %}
|
||||
|
||||
{% block interface_stylesheet %}
|
||||
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/interface-%v.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% include "base/theme.html" %}
|
||||
|
||||
<style data-id="brand-css">{{ brand_css }}</style>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{% load static %}
|
||||
{% load authentik_core %}
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/interface-%v.css' %}" />
|
||||
|
||||
{% if ui_theme == "dark" %}
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="theme-color" content="#18191a">
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
"""Test interface view redirect behavior by user type"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import Application, UserTypes
|
||||
from authentik.core.tests.utils import create_test_brand, create_test_user
|
||||
|
||||
|
||||
class TestInterfaceRedirects(TestCase):
|
||||
"""Test RootRedirectView and BrandDefaultRedirectView redirect logic by user type"""
|
||||
|
||||
def setUp(self):
|
||||
self.app = Application.objects.create(name="test-app", slug="test-app")
|
||||
self.brand: Brand = create_test_brand(default_application=self.app)
|
||||
|
||||
def _assert_redirects_to_app(self, url_name: str, user_type: UserTypes):
|
||||
user = create_test_user(type=user_type)
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(reverse(f"authentik_core:{url_name}"))
|
||||
self.assertRedirects(
|
||||
response,
|
||||
reverse(
|
||||
"authentik_core:application-launch", kwargs={"application_slug": self.app.slug}
|
||||
),
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
def _assert_no_redirect(self, url_name: str, user_type: UserTypes):
|
||||
"""Internal users should not be redirected away."""
|
||||
user = create_test_user(type=user_type)
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(reverse(f"authentik_core:{url_name}"))
|
||||
# Internal users get a 200 (rendered template) or redirect to if-user, not to the app
|
||||
app_url = reverse(
|
||||
"authentik_core:application-launch", kwargs={"application_slug": self.app.slug}
|
||||
)
|
||||
self.assertNotEqual(response.get("Location"), app_url)
|
||||
|
||||
# --- RootRedirectView ---
|
||||
|
||||
def test_root_redirect_external_user(self):
|
||||
"""External users are redirected to the default app from root"""
|
||||
self._assert_redirects_to_app("root-redirect", UserTypes.EXTERNAL)
|
||||
|
||||
def test_root_redirect_service_account(self):
|
||||
"""Service accounts are redirected to the default app from root"""
|
||||
self._assert_redirects_to_app("root-redirect", UserTypes.SERVICE_ACCOUNT)
|
||||
|
||||
def test_root_redirect_internal_service_account(self):
|
||||
"""Internal service accounts are redirected to the default app from root"""
|
||||
self._assert_redirects_to_app("root-redirect", UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
|
||||
def test_root_redirect_internal_user(self):
|
||||
"""Internal users are NOT redirected to the app from root"""
|
||||
self._assert_no_redirect("root-redirect", UserTypes.INTERNAL)
|
||||
|
||||
# --- BrandDefaultRedirectView (if/user/) ---
|
||||
|
||||
def test_if_user_external_user(self):
|
||||
"""External users are redirected to the default app from if/user/"""
|
||||
self._assert_redirects_to_app("if-user", UserTypes.EXTERNAL)
|
||||
|
||||
def test_if_user_service_account(self):
|
||||
"""Service accounts are redirected to the default app from if/user/"""
|
||||
self._assert_redirects_to_app("if-user", UserTypes.SERVICE_ACCOUNT)
|
||||
|
||||
def test_if_user_internal_service_account(self):
|
||||
"""Internal service accounts are redirected to the default app from if/user/"""
|
||||
self._assert_redirects_to_app("if-user", UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
|
||||
def test_if_user_internal_user(self):
|
||||
"""Internal users are NOT redirected to the app from if/user/"""
|
||||
self._assert_no_redirect("if-user", UserTypes.INTERNAL)
|
||||
|
||||
# --- BrandDefaultRedirectView (if/admin/) ---
|
||||
|
||||
def test_if_admin_service_account(self):
|
||||
"""Service accounts are redirected to the default app from if/admin/"""
|
||||
self._assert_redirects_to_app("if-admin", UserTypes.SERVICE_ACCOUNT)
|
||||
|
||||
def test_if_admin_internal_service_account(self):
|
||||
"""Internal service accounts are redirected to the default app from if/admin/"""
|
||||
self._assert_redirects_to_app("if-admin", UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
|
||||
def test_if_admin_internal_user(self):
|
||||
"""Internal users are NOT redirected to the app from if/admin/"""
|
||||
self._assert_no_redirect("if-admin", UserTypes.INTERNAL)
|
||||
|
||||
# --- No default app set ---
|
||||
|
||||
def test_service_account_no_default_app_access_denied(self):
|
||||
"""Service accounts get access denied when no default app is configured"""
|
||||
self.brand.default_application = None
|
||||
self.brand.save()
|
||||
user = create_test_user(type=UserTypes.SERVICE_ACCOUNT)
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(reverse("authentik_core:if-user"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b"Interface can only be accessed by internal users", response.content)
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.utils.timezone import now
|
||||
@@ -10,9 +9,6 @@ from freezegun import freeze_time
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.core.models import Provider, Source, Token
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
@@ -33,74 +29,6 @@ class TestModels(TestCase):
|
||||
freeze.tick(timedelta(seconds=1))
|
||||
self.assertFalse(token.is_expired)
|
||||
|
||||
def test_filter_not_expired_warning(self):
|
||||
"""Test filter_not_expired's deprecation message"""
|
||||
id = generate_id()
|
||||
Token.objects.create(
|
||||
expires=now() - timedelta(hours=1),
|
||||
expiring=True,
|
||||
user=get_anonymous_user(),
|
||||
identifier=id,
|
||||
)
|
||||
self.assertFalse(Token.filter_not_expired(identifier=id).exists())
|
||||
event = Event.objects.filter(action=EventAction.CONFIGURATION_WARNING).first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertEqual(
|
||||
event.context["deprecation"], "authentik.core.models.Token.filter_not_expired"
|
||||
)
|
||||
|
||||
@patch("authentik.core.models.get_file_manager")
|
||||
def test_source_icon_url_can_bypass_cache(self, get_file_manager):
|
||||
request = RequestFactory().get("/")
|
||||
manager = get_file_manager.return_value
|
||||
manager.file_url.return_value = "/files/media/public/source-icons/icon.svg?token=fresh"
|
||||
|
||||
source = Source(icon="source-icons/icon.svg")
|
||||
|
||||
self.assertEqual(
|
||||
source.get_icon_url(request, use_cache=False),
|
||||
"/files/media/public/source-icons/icon.svg?token=fresh",
|
||||
)
|
||||
manager.file_url.assert_called_once_with(
|
||||
"source-icons/icon.svg",
|
||||
request,
|
||||
use_cache=False,
|
||||
)
|
||||
|
||||
@patch("authentik.flows.models.get_file_manager")
|
||||
def test_flow_background_urls_can_bypass_cache(self, get_file_manager):
|
||||
request = RequestFactory().get("/")
|
||||
manager = get_file_manager.return_value
|
||||
manager.file_url.return_value = "/files/media/public/background.svg?token=fresh"
|
||||
manager.themed_urls.return_value = {
|
||||
"light": "/files/media/public/background-light.svg?token=fresh",
|
||||
"dark": "/files/media/public/background-dark.svg?token=fresh",
|
||||
}
|
||||
|
||||
flow = Flow(background="background-%(theme)s.svg")
|
||||
|
||||
self.assertEqual(
|
||||
flow.background_url(request, use_cache=False),
|
||||
"/files/media/public/background.svg?token=fresh",
|
||||
)
|
||||
self.assertEqual(
|
||||
flow.background_themed_urls(request, use_cache=False),
|
||||
{
|
||||
"light": "/files/media/public/background-light.svg?token=fresh",
|
||||
"dark": "/files/media/public/background-dark.svg?token=fresh",
|
||||
},
|
||||
)
|
||||
manager.file_url.assert_called_once_with(
|
||||
"background-%(theme)s.svg",
|
||||
request,
|
||||
use_cache=False,
|
||||
)
|
||||
manager.themed_urls.assert_called_once_with(
|
||||
"background-%(theme)s.svg",
|
||||
request,
|
||||
use_cache=False,
|
||||
)
|
||||
|
||||
|
||||
def source_tester_factory(test_model: type[Source]) -> Callable:
|
||||
"""Test source"""
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
"""Test object attributes API"""
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.api.object_attributes import ContentType
|
||||
from authentik.core.models import ObjectAttribute, User
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
class TestObjectAttributesAPI(APITestCase):
|
||||
"""Test object attributes API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_create(self):
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:objectattribute-list"),
|
||||
data={
|
||||
"object_type": "authentik_core.user",
|
||||
"enabled": False,
|
||||
"key": "employeeNumber",
|
||||
"label": "Employee Number",
|
||||
"type": "text",
|
||||
"group": "Employee",
|
||||
"is_unique": False,
|
||||
"is_required": False,
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 201)
|
||||
attr = ObjectAttribute.objects.filter(key="employeeNumber").first()
|
||||
self.assertIsNotNone(attr)
|
||||
|
||||
def test_create_invalid(self):
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:objectattribute-list"),
|
||||
data={
|
||||
"object_type": "authentik_core.objectattribute",
|
||||
"enabled": False,
|
||||
"key": "employeeNumber",
|
||||
"label": "Employee Number",
|
||||
"type": "text",
|
||||
"group": "Employee",
|
||||
"is_unique": False,
|
||||
"is_required": False,
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(res.content, {"object_type": ["Invalid object type"]})
|
||||
|
||||
def test_create_invalid_array_unique(self):
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:objectattribute-list"),
|
||||
data={
|
||||
"object_type": "authentik_core.user",
|
||||
"enabled": False,
|
||||
"key": "employeeNumber",
|
||||
"label": "Employee Number",
|
||||
"type": "text",
|
||||
"group": "Employee",
|
||||
"is_unique": True,
|
||||
"is_required": False,
|
||||
"is_array": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
res.content, {"non_field_errors": ["Unique cannot be enabled for arrays."]}
|
||||
)
|
||||
|
||||
def test_update(self):
|
||||
attr = ObjectAttribute.objects.create(
|
||||
object_type=ContentType.objects.get_for_model(User),
|
||||
label="foo",
|
||||
key=generate_id(),
|
||||
type=ObjectAttribute.AttributeType.TEXT,
|
||||
)
|
||||
res = self.client.put(
|
||||
reverse("authentik_api:objectattribute-detail", kwargs={"pk": attr.pk}),
|
||||
data={
|
||||
"object_type": "authentik_core.user",
|
||||
"enabled": False,
|
||||
"key": attr.key,
|
||||
"label": "Employee Number",
|
||||
"type": "text",
|
||||
"group": "Employee",
|
||||
"is_unique": False,
|
||||
"is_required": False,
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
attr.refresh_from_db()
|
||||
self.assertEqual(attr.label, "Employee Number")
|
||||
|
||||
def test_user_attrib_validation_required(self):
|
||||
attr = ObjectAttribute.objects.create(
|
||||
object_type=ContentType.objects.get_for_model(User),
|
||||
label="foo",
|
||||
key=generate_id(),
|
||||
type=ObjectAttribute.AttributeType.TEXT,
|
||||
is_required=True,
|
||||
)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
|
||||
data={
|
||||
"attributes": {},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(res.content, {f"attributes_{attr.key}": ["This field is required"]})
|
||||
|
||||
def test_user_attrib_validation_unique(self):
|
||||
attr = ObjectAttribute.objects.create(
|
||||
object_type=ContentType.objects.get_for_model(User),
|
||||
label="foo",
|
||||
key=generate_id(),
|
||||
type=ObjectAttribute.AttributeType.TEXT,
|
||||
is_unique=True,
|
||||
)
|
||||
other_user = create_test_user()
|
||||
other_user.attributes[attr.key] = "foo"
|
||||
other_user.save()
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
|
||||
data={
|
||||
"attributes": {attr.key: "foo"},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(res.content, {f"attributes_{attr.key}": ["Value is not unique."]})
|
||||
|
||||
def test_user_attrib_validation_regex(self):
|
||||
attr = ObjectAttribute.objects.create(
|
||||
object_type=ContentType.objects.get_for_model(User),
|
||||
label="foo",
|
||||
key=generate_id(),
|
||||
type=ObjectAttribute.AttributeType.TEXT,
|
||||
regex="bar",
|
||||
)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
|
||||
data={
|
||||
"attributes": {attr.key: "foo"},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
res.content, {f"attributes_{attr.key}": ["Value does not match configured pattern."]}
|
||||
)
|
||||
|
||||
def test_user_attrib_validation_array(self):
|
||||
attr = ObjectAttribute.objects.create(
|
||||
object_type=ContentType.objects.get_for_model(User),
|
||||
label="foo",
|
||||
key=generate_id(),
|
||||
type=ObjectAttribute.AttributeType.TEXT,
|
||||
is_array=True,
|
||||
)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
|
||||
data={
|
||||
"attributes": {attr.key: "foo"},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(res.content, {f"attributes_{attr.key}": ["Value must be an array."]})
|
||||
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
|
||||
data={
|
||||
"attributes": {attr.key: ["foo"]},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_user_attrib_validation_array_regex(self):
|
||||
attr = ObjectAttribute.objects.create(
|
||||
object_type=ContentType.objects.get_for_model(User),
|
||||
label="foo",
|
||||
key=generate_id(),
|
||||
type=ObjectAttribute.AttributeType.TEXT,
|
||||
is_array=True,
|
||||
regex="bar",
|
||||
)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
|
||||
data={
|
||||
"attributes": {attr.key: ["foo"]},
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
res.content, {f"attributes_{attr.key}": ["Value does not match configured pattern."]}
|
||||
)
|
||||
@@ -63,7 +63,7 @@ class TestPropertyMappingAPI(APITestCase):
|
||||
PropertyMappingSerializer().validate_expression("/")
|
||||
|
||||
def test_types(self):
|
||||
"""Test PropertyMapping's types endpoint"""
|
||||
"""Test PropertyMappigns's types endpoint"""
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:propertymapping-types"),
|
||||
)
|
||||
|
||||
@@ -173,7 +173,7 @@ class TestTokenAPI(APITestCase):
|
||||
|
||||
def test_list(self):
|
||||
"""Test Token List (Test normal authentication)"""
|
||||
Token.objects.including_expired().all().delete()
|
||||
Token.objects.all().delete()
|
||||
token_should: Token = Token.objects.create(
|
||||
identifier="test", expiring=False, user=self.user
|
||||
)
|
||||
@@ -185,7 +185,7 @@ class TestTokenAPI(APITestCase):
|
||||
|
||||
def test_list_with_permission(self):
|
||||
"""Test Token List (Test with `view_token` permission)"""
|
||||
Token.objects.including_expired().all().delete()
|
||||
Token.objects.all().delete()
|
||||
token_should: Token = Token.objects.create(
|
||||
identifier="test", expiring=False, user=self.user
|
||||
)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"""Test token auth"""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from authentik.core.auth import TokenBackend
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
@@ -31,15 +28,6 @@ class TestTokenAuth(TestCase):
|
||||
TokenBackend().authenticate(self.request, "test-user", self.token.key), self.user
|
||||
)
|
||||
|
||||
def test_token_auth_expired(self):
|
||||
"""Test auth with token"""
|
||||
self.token.expiring = True
|
||||
self.token.expires = now() - timedelta(hours=1)
|
||||
self.token.save()
|
||||
self.assertEqual(
|
||||
TokenBackend().authenticate(self.request, "test-user", self.token.key), None
|
||||
)
|
||||
|
||||
def test_token_auth_none(self):
|
||||
"""Test auth with token (non-existent user)"""
|
||||
self.assertIsNone(
|
||||
|
||||
@@ -9,7 +9,6 @@ from authentik.core.api.applications import ApplicationViewSet
|
||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
||||
from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
|
||||
from authentik.core.api.groups import GroupViewSet
|
||||
from authentik.core.api.object_attributes import ObjectAttributeViewSet
|
||||
from authentik.core.api.property_mappings import PropertyMappingViewSet
|
||||
from authentik.core.api.providers import ProviderViewSet
|
||||
from authentik.core.api.sources import (
|
||||
@@ -83,7 +82,6 @@ api_urlpatterns = [
|
||||
("core/groups", GroupViewSet),
|
||||
("core/users", UserViewSet),
|
||||
("core/tokens", TokenViewSet),
|
||||
("core/object_attributes", ObjectAttributeViewSet),
|
||||
("sources/all", SourceViewSet),
|
||||
("sources/user_connections/all", UserSourceConnectionViewSet),
|
||||
("sources/group_connections/all", GroupSourceConnectionViewSet),
|
||||
|
||||
@@ -26,11 +26,7 @@ class RootRedirectView(RedirectView):
|
||||
query_string = True
|
||||
|
||||
def redirect_to_app(self, request: HttpRequest):
|
||||
if request.user.is_authenticated and request.user.type in (
|
||||
UserTypes.EXTERNAL,
|
||||
UserTypes.SERVICE_ACCOUNT,
|
||||
UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
||||
):
|
||||
if request.user.is_authenticated and request.user.type == UserTypes.EXTERNAL:
|
||||
brand: Brand = request.brand
|
||||
if brand.default_application:
|
||||
return redirect(
|
||||
@@ -66,11 +62,7 @@ class BrandDefaultRedirectView(InterfaceView):
|
||||
"""By default redirect to default app"""
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
if request.user.is_authenticated and request.user.type in (
|
||||
UserTypes.EXTERNAL,
|
||||
UserTypes.SERVICE_ACCOUNT,
|
||||
UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
||||
):
|
||||
if request.user.is_authenticated and request.user.type == UserTypes.EXTERNAL:
|
||||
brand: Brand = request.brand
|
||||
if brand.default_application:
|
||||
return redirect(
|
||||
|
||||
@@ -114,16 +114,15 @@ def certificate_discovery():
|
||||
discovered = 0
|
||||
for file in glob(CONFIG.get("cert_discovery_dir") + "/**", recursive=True):
|
||||
path = Path(file)
|
||||
if not path.exists() or path.is_dir():
|
||||
if not path.exists():
|
||||
continue
|
||||
if path.is_dir():
|
||||
continue
|
||||
# For certbot setups, we want to ignore archive.
|
||||
if "archive" in file:
|
||||
continue
|
||||
# Handle additionalOutputFormats from cert-manager gracefully
|
||||
if path.name in ["ca.crt", "tls-combined.pem", "key.der"]:
|
||||
continue
|
||||
# Support certbot & kubernetes.io/tls directory structure
|
||||
if path.name in ["fullchain.pem", "privkey.pem", "tls.crt", "tls.key"]:
|
||||
# Support certbot's directory structure
|
||||
if path.name in ["fullchain.pem", "privkey.pem"]:
|
||||
cert_name = path.parent.name
|
||||
else:
|
||||
cert_name = path.name.replace(path.suffix, "")
|
||||
|
||||
@@ -355,16 +355,6 @@ class TestCrypto(APITestCase):
|
||||
subject_alt_names=[],
|
||||
validity_days=3,
|
||||
)
|
||||
|
||||
name3 = generate_id()
|
||||
builder3 = CertificateBuilder(name3)
|
||||
with self.assertRaises(ValueError):
|
||||
builder3.save()
|
||||
builder3.build(
|
||||
subject_alt_names=[],
|
||||
validity_days=3,
|
||||
)
|
||||
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
with open(f"{temp_dir}/foo.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder.certificate)
|
||||
@@ -375,8 +365,6 @@ class TestCrypto(APITestCase):
|
||||
_cert.write(builder2.certificate)
|
||||
with open(f"{temp_dir}/foo.bar/privkey.pem", "w+", encoding="utf-8") as _key:
|
||||
_key.write(builder2.private_key)
|
||||
with open(f"{temp_dir}/tls-combined.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder3.certificate)
|
||||
with CONFIG.patch("cert_discovery_dir", temp_dir):
|
||||
certificate_discovery.send()
|
||||
keypair: CertificateKeyPair = CertificateKeyPair.objects.filter(
|
||||
@@ -388,9 +376,6 @@ class TestCrypto(APITestCase):
|
||||
self.assertTrue(
|
||||
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists()
|
||||
)
|
||||
self.assertFalse(
|
||||
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "tls-combined").exists()
|
||||
)
|
||||
|
||||
def test_discovery_updating_same_private_key(self):
|
||||
"""Test certificate discovery updating certs with matching private keys"""
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.object_attributes import AttributesMixinSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.endpoints.models import DeviceAccessGroup
|
||||
|
||||
|
||||
class DeviceAccessGroupSerializer(AttributesMixinSerializer, ModelSerializer):
|
||||
class DeviceAccessGroupSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceAccessGroup
|
||||
|
||||
@@ -97,7 +97,7 @@ class DeviceViewSet(
|
||||
def summary(self, request: Request) -> Response:
|
||||
delta = now() - timedelta(hours=24)
|
||||
unreachable = (
|
||||
Device.objects.all()
|
||||
Device.filter_not_expired()
|
||||
.annotate(
|
||||
latest_snapshot=Subquery(
|
||||
DeviceFactSnapshot.objects.filter(connection__device=OuterRef("pk"))
|
||||
@@ -110,7 +110,7 @@ class DeviceViewSet(
|
||||
.count()
|
||||
)
|
||||
data = {
|
||||
"total_count": Device.objects.all().count(),
|
||||
"total_count": Device.filter_not_expired().count(),
|
||||
"unreachable_count": unreachable,
|
||||
# Currently not supported
|
||||
"outdated_agent_count": 0,
|
||||
|
||||
@@ -65,9 +65,7 @@ class AgentConnectorSerializer(ConnectorSerializer):
|
||||
class MDMConfigSerializer(PassiveSerializer):
|
||||
|
||||
platform = ChoiceField(choices=OSFamily.choices)
|
||||
enrollment_token = PrimaryKeyRelatedField(
|
||||
queryset=EnrollmentToken.objects.including_expired().all()
|
||||
)
|
||||
enrollment_token = PrimaryKeyRelatedField(queryset=EnrollmentToken.objects.all())
|
||||
|
||||
def validate_platform(self, platform: OSFamily) -> OSFamily:
|
||||
if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]:
|
||||
@@ -138,7 +136,7 @@ class AgentConnectorViewSet(
|
||||
device=device,
|
||||
connector=token.connector,
|
||||
)
|
||||
DeviceToken.objects.including_expired().filter(device=connection).delete()
|
||||
DeviceToken.objects.filter(device=connection).delete()
|
||||
token = DeviceToken.objects.create(device=connection, expiring=False)
|
||||
return Response(
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ class AgentEnrollmentAuth(BaseAuthentication):
|
||||
def authenticate(self, request: Request) -> tuple[User, Any] | None:
|
||||
auth = get_authorization_header(request)
|
||||
key = validate_auth(auth)
|
||||
token = EnrollmentToken.objects.filter(key=key).first()
|
||||
token = EnrollmentToken.filter_not_expired(key=key).first()
|
||||
if not token:
|
||||
raise PermissionDenied()
|
||||
if not token.connector.enabled:
|
||||
@@ -50,7 +50,7 @@ class AgentAuth(BaseAuthentication):
|
||||
key = validate_auth(auth, format="bearer+agent")
|
||||
if not key:
|
||||
return None
|
||||
device_token = DeviceToken.objects.filter(key=key).first()
|
||||
device_token = DeviceToken.filter_not_expired(key=key).first()
|
||||
if not device_token:
|
||||
raise PermissionDenied()
|
||||
if not device_token.device.connector.enabled:
|
||||
@@ -91,7 +91,7 @@ class DeviceAuthFedAuthentication(BaseAuthentication):
|
||||
if not raw_token:
|
||||
LOGGER.warning("Missing token")
|
||||
return None
|
||||
device = Device.objects.filter(name=request.query_params.get("device")).first()
|
||||
device = Device.filter_not_expired(name=request.query_params.get("device")).first()
|
||||
if not device:
|
||||
LOGGER.warning("Couldn't find device")
|
||||
return None
|
||||
|
||||
@@ -53,11 +53,11 @@ class EndpointAgentChallengeResponse(ChallengeResponse):
|
||||
except PyJWTError as exc:
|
||||
self.stage.logger.warning("Could not parse response", exc=exc)
|
||||
raise ValidationError("Invalid challenge response") from None
|
||||
device = Device.objects.filter(identifier=raw["iss"]).first()
|
||||
device = Device.filter_not_expired(identifier=raw["iss"]).first()
|
||||
if not device:
|
||||
self.stage.logger.warning("Could not find device for challenge")
|
||||
raise ValidationError("Invalid challenge response")
|
||||
for token in DeviceToken.objects.filter(
|
||||
for token in DeviceToken.filter_not_expired(
|
||||
device__device=device,
|
||||
device__connector=self.stage.executor.current_stage.connector,
|
||||
).values_list("key", flat=True):
|
||||
|
||||
@@ -89,7 +89,7 @@ class TestAgentAPI(APITestCase):
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
device = Device.objects.filter(identifier=ident).first()
|
||||
device = Device.filter_not_expired(identifier=ident).first()
|
||||
self.assertIsNotNone(device)
|
||||
self.assertEqual(device.access_group, device_group)
|
||||
|
||||
@@ -104,7 +104,7 @@ class TestAgentAPI(APITestCase):
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertFalse(Device.objects.filter(identifier=dev_id).exists())
|
||||
self.assertFalse(Device.filter_not_expired(identifier=dev_id).exists())
|
||||
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_config(self):
|
||||
|
||||
@@ -63,7 +63,7 @@ class OperatingSystemSerializer(Serializer):
|
||||
"Operating System version, must always be the version number but may contain build name"
|
||||
),
|
||||
)
|
||||
arch = CharField(required=False)
|
||||
arch = CharField(required=True)
|
||||
|
||||
|
||||
class NetworkInterfaceSerializer(Serializer):
|
||||
|
||||
@@ -54,7 +54,7 @@ class Device(InternallyManagedMixin, ExpiringModel, AttributesMixin, PolicyBindi
|
||||
def facts(self) -> DeviceFactSnapshot:
|
||||
data = {}
|
||||
last_updated = datetime.fromtimestamp(0, UTC)
|
||||
for snapshot_data, snapshort_created in DeviceFactSnapshot.objects.filter(
|
||||
for snapshot_data, snapshort_created in DeviceFactSnapshot.filter_not_expired(
|
||||
snapshot_id__in=Subquery(
|
||||
DeviceFactSnapshot.objects.filter(
|
||||
connection__connector=OuterRef("connection__connector"), connection__device=self
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Enterprise app config"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
from authentik.tenants.flags import Flag
|
||||
@@ -10,9 +9,6 @@ from authentik.tenants.flags import Flag
|
||||
class AuditIncludeExpandedDiff(Flag[bool], key="enterprise_audit_include_expanded_diff"):
|
||||
default = False
|
||||
visibility = "none"
|
||||
description = _(
|
||||
"Include additional information in audit logs, may incur a performance penalty."
|
||||
)
|
||||
|
||||
|
||||
class AuthentikEnterpriseAuditConfig(EnterpriseConfig):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user