mirror of
https://github.com/goauthentik/authentik
synced 2026-05-14 10:56:52 +02:00
Compare commits
150 Commits
core/separ
...
cherry-pic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5f69300b0 | ||
|
|
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,35 +0,0 @@
|
||||
[licenses]
|
||||
allow = ["Apache-2.0", "MIT", "MPL-2.0", "Unicode-3.0"]
|
||||
|
||||
[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,16 +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
|
||||
where_single_line = true
|
||||
wrap_comments = true
|
||||
@@ -89,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"
|
||||
|
||||
|
||||
|
||||
57
.github/actions/setup/action.yml
vendored
57
.github/actions/setup/action.yml
vendored
@@ -4,10 +4,15 @@ 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"
|
||||
working-directory:
|
||||
description: |
|
||||
Optional working directory if this repo isn't in the root of the actions workspace.
|
||||
When set, needs to contain a trailing slash
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -22,60 +27,32 @@ runs:
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
- name: Install uv
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v5
|
||||
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v5
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Setup python
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v5
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
python-version-file: "${{ inputs.working-directory }}pyproject.toml"
|
||||
- name: Install Python deps
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
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@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
|
||||
with:
|
||||
rustflags: ""
|
||||
- name: Setup rust (nightly)
|
||||
if: ${{ contains(inputs.dependencies, 'rust-nightly') }}
|
||||
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # 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: web/package.json
|
||||
node-version-file: ${{ inputs.working-directory }}web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: 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: package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: package-lock.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Install Node deps
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
shell: bash
|
||||
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@4b73464bb391d4059bd26b0524d20df3927bd417 # v5
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
go-version-file: "${{ inputs.working-directory }}go.mod"
|
||||
- name: Setup docker cache
|
||||
if: ${{ contains(inputs.dependencies, 'runtime') }}
|
||||
uses: AndreKurait/docker-cache@0fe76702a40db986d9663c24954fc14c6a6031b7
|
||||
@@ -84,6 +61,7 @@ runs:
|
||||
- name: Setup dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'runtime') }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||
docker compose -f .github/actions/setup/compose.yml up -d
|
||||
@@ -91,6 +69,7 @@ runs:
|
||||
- name: Generate config
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
shell: uv run python {0}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
from authentik.lib.generators import generate_id
|
||||
from yaml import safe_dump
|
||||
|
||||
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@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
with:
|
||||
files: ${{ inputs.files }}
|
||||
flags: ${{ inputs.flags }}
|
||||
use_oidc: true
|
||||
- uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
with:
|
||||
files: ${{ inputs.files }}
|
||||
flags: ${{ inputs.flags }}
|
||||
use_oidc: true
|
||||
report_type: test_results
|
||||
|
||||
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
|
||||
17
.github/dependabot.yml
vendored
17
.github/dependabot.yml
vendored
@@ -38,21 +38,6 @@ updates:
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rust
|
||||
|
||||
- package-ecosystem: rust-toolchain
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
|
||||
#endregion
|
||||
|
||||
#region Web
|
||||
|
||||
- package-ecosystem: npm
|
||||
@@ -249,7 +234,7 @@ updates:
|
||||
|
||||
- package-ecosystem: docker
|
||||
directories:
|
||||
- /lifecycle/container
|
||||
- /
|
||||
- /website
|
||||
schedule:
|
||||
interval: daily
|
||||
|
||||
@@ -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,23 +56,23 @@ jobs:
|
||||
release: ${{ inputs.release }}
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- 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
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Generate API Clients
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
id: push
|
||||
with:
|
||||
context: .
|
||||
@@ -95,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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@8aac06098a12365ccdf99372dcfb453ccce8a0b0 # 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 }}
|
||||
|
||||
4
.github/workflows/api-ts-publish.yml
vendored
4
.github/workflows/api-ts-publish.yml
vendored
@@ -18,14 +18,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # 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/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.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:
|
||||
|
||||
16
.github/workflows/ci-main-daily.yml
vendored
16
.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,14 +15,14 @@ jobs:
|
||||
matrix:
|
||||
version:
|
||||
- docs
|
||||
- version-2025-12
|
||||
- version-2025-10
|
||||
- version-2025-4
|
||||
- version-2025-2
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- run: |
|
||||
current="$(pwd)"
|
||||
dir="/tmp/authentik/${{ matrix.version }}"
|
||||
mkdir -p "${dir}/lifecycle/container"
|
||||
cd "${dir}"
|
||||
wget "https://${{ matrix.version }}.goauthentik.io/docker-compose.yml" -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
|
||||
|
||||
74
.github/workflows/ci-main.yml
vendored
74
.github/workflows/ci-main.yml
vendored
@@ -28,46 +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-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: generate schema
|
||||
run: make migrate gen-build
|
||||
- name: ensure schema is up-to-date
|
||||
run: git diff --exit-code -- schema.yml blueprints/schema.json
|
||||
run: uv run make ci-${{ matrix.job }}
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -186,7 +160,7 @@ 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
|
||||
@@ -231,7 +205,7 @@ jobs:
|
||||
run: |
|
||||
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
|
||||
- id: cache-web
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4
|
||||
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
|
||||
@@ -274,7 +248,7 @@ jobs:
|
||||
run: |
|
||||
docker compose -f tests/openid_conformance/compose.yml up -d --quiet-pull
|
||||
- id: cache-web
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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
|
||||
@@ -295,38 +269,14 @@ jobs:
|
||||
with:
|
||||
flags: conformance
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
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
|
||||
- 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: test-rust
|
||||
path: target/llvm-cov-target/rust.json
|
||||
ci-core-mark:
|
||||
if: always()
|
||||
needs:
|
||||
- lint
|
||||
- test-gen-build
|
||||
- test-migrations
|
||||
- test-migrations-from-stable
|
||||
- test-unittest
|
||||
|
||||
18
.github/workflows/ci-outpost.yml
vendored
18
.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@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Prepare and generate API
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Setup authentik env
|
||||
@@ -90,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
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
|
||||
- name: Login to Container Registry
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
run: make gen-client-go
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: lifecycle/container/${{ matrix.type }}.Dockerfile
|
||||
@@ -122,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:
|
||||
@@ -148,10 +148,10 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # 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"
|
||||
|
||||
6
.github/workflows/ci-web.yml
vendored
6
.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"
|
||||
@@ -49,7 +49,7 @@ 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"
|
||||
@@ -77,7 +77,7 @@ 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"
|
||||
|
||||
4
.github/workflows/gen-image-compress.yml
vendored
4
.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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Compress images
|
||||
id: compress
|
||||
uses: calibreapp/image-actions@03c976c29803442fc4040a9de5509669e7759b81 # main
|
||||
uses: calibreapp/image-actions@d9c8ee5c3dc52ae4622c82ead88d658f4b16b65f # main
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
compressOnly: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
|
||||
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@f8d387b68d61c58ab83c6c016672934102569859 # 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@f8d387b68d61c58ab83c6c016672934102569859 # 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
|
||||
|
||||
4
.github/workflows/release-branch-off.yml
vendored
4
.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@f8d387b68d61c58ab83c6c016672934102569859 # 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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
|
||||
32
.github/workflows/release-publish.yml
vendored
32
.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.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@4b73464bb391d4059bd26b0524d20df3927bd417 # 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
|
||||
@@ -108,18 +108,18 @@ jobs:
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
id: push
|
||||
with:
|
||||
push: true
|
||||
@@ -129,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 }}
|
||||
@@ -152,10 +152,10 @@ jobs:
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # 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"
|
||||
@@ -180,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 }}
|
||||
|
||||
8
.github/workflows/release-tag.yml
vendored
8
.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@f8d387b68d61c58ab83c6c016672934102569859 # 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@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
|
||||
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@f8d387b68d61c58ab83c6c016672934102569859 # 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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
|
||||
21
.gitignore
vendored
21
.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
|
||||
|
||||
|
||||
12
.vscode/settings.json
vendored
12
.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": [
|
||||
@@ -53,9 +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"]
|
||||
"github-actions.workflows.pinned.workflows": [
|
||||
".github/workflows/ci-main.yml"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -39,7 +34,6 @@ 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
|
||||
|
||||
271
Cargo.lock
generated
271
Cargo.lock
generated
@@ -1,271 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docsmg"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"colored",
|
||||
"dotenvy",
|
||||
"eyre",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
||||
dependencies = [
|
||||
"indenter",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
133
Cargo.toml
133
Cargo.toml
@@ -1,133 +0,0 @@
|
||||
[workspace]
|
||||
members = ["website/scripts/docsmg"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
homepage = "https://goauthentik.io"
|
||||
repository = "https://github.com/goauthentik/authentik.git"
|
||||
license-file = "LICENSE"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = { version = "4.5.59", features = ["derive", "env"] }
|
||||
colored = "3.1.1"
|
||||
dotenvy = "0.15.7"
|
||||
eyre = "0.6.12"
|
||||
regex = "1.12.3"
|
||||
|
||||
[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
|
||||
### pedantic group
|
||||
redundant_closure_for_method_calls = "allow"
|
||||
too_many_lines = "allow"
|
||||
### nursery
|
||||
redundant_pub_crate = "allow"
|
||||
option_if_let_else = "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"
|
||||
doc_paragraphs_missing_punctuation = "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"
|
||||
66
Makefile
66
Makefile
@@ -23,7 +23,6 @@ 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
|
||||
@@ -70,26 +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 html
|
||||
$(UV) run coverage report
|
||||
|
||||
lint-fix: ## 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)
|
||||
$(CARGO) +nightly fmt --all -- --config-path .cargo/rustfmt.toml
|
||||
|
||||
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:
|
||||
@@ -173,22 +168,12 @@ 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
|
||||
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 \
|
||||
@@ -291,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:
|
||||
@@ -336,40 +321,27 @@ 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 .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 .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 authentik
|
||||
$(UV) run coverage report
|
||||
|
||||
30
SECURITY.md
30
SECURITY.md
@@ -60,6 +60,36 @@ authentik reserves the right to reclassify CVSS as necessary. To determine sever
|
||||
| 7.0 – 8.9 | High |
|
||||
| 9.0 – 10.0 | Critical |
|
||||
|
||||
## Intended functionality
|
||||
|
||||
The following capabilities are part of intentional system design and should not be reported as security vulnerabilities:
|
||||
|
||||
- Expressions (property mappings/policies/prompts) can execute arbitrary Python code without safeguards.
|
||||
|
||||
This is expected behavior. Any user with permission to create or modify objects containing expression fields can write code that is executed within authentik. If a vulnerability allows a user without the required permissions to write or modify code and have it executed, that would be a valid security report.
|
||||
|
||||
However, the fact that expressions are executed as part of normal operations is not considered a privilege escalation or security vulnerability.
|
||||
|
||||
- Blueprints can access all files on the filesystem.
|
||||
|
||||
This access is intentional to allow legitimate configuration and deployment tasks. It does not represent a security problem by itself.
|
||||
|
||||
- Importing blueprints allows arbitrary modification of application objects.
|
||||
|
||||
This is intended functionality. This behavior reflects the privileged design of blueprint imports. It is "exploitable" when importing blueprints from untrusted sources without reviewing the blueprint beforehand. However, any method to create, modify or execute blueprints without the required permissions would be a valid security report.
|
||||
|
||||
- Flow imports may contain objects other than flows (such as policies, users, groups, etc.)
|
||||
|
||||
This is expected behavior as flow imports are blueprint files.
|
||||
|
||||
- Prompt HTML is not escaped.
|
||||
|
||||
Prompts intentionally allow raw HTML, including script tags, so they can be used to create interactive or customized user interface elements. Because of this, scripts within prompts may affect or interact with the surrounding page as designed.
|
||||
|
||||
- Open redirects that do not include tokens or other sensitive information are not considered a security vulnerability.
|
||||
|
||||
Redirects that only change navigation flow and do not expose session tokens, API keys, or other confidential data are considered acceptable and do not require reporting.
|
||||
|
||||
## Disclosure process
|
||||
|
||||
1. Report from Github or Issue is reported via Email as listed above.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
|
||||
VERSION = "2026.5.0-rc1"
|
||||
VERSION = "2026.2.2-rc2"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -71,7 +71,7 @@ def postprocess_schema_responses(
|
||||
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
|
||||
"""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():
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -48,7 +47,12 @@ class ApplicationSerializer(ModelSerializer):
|
||||
"""Application Serializer"""
|
||||
|
||||
launch_url = SerializerMethodField()
|
||||
provider_obj = ProviderSerializer(source="get_provider", required=False, read_only=True)
|
||||
provider_obj = ProviderSerializer(
|
||||
source="get_provider",
|
||||
required=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
)
|
||||
backchannel_providers_obj = ProviderSerializer(
|
||||
source="backchannel_providers", required=False, read_only=True, many=True
|
||||
)
|
||||
@@ -155,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)
|
||||
@@ -222,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()
|
||||
@@ -242,6 +244,11 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="superuser_full_list",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.BOOL,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="for_user",
|
||||
location=OpenApiParameter.QUERY,
|
||||
@@ -252,17 +259,18 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.BOOL,
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: ApplicationSerializer(many=True),
|
||||
},
|
||||
operation_id="core_applications_accessible_list",
|
||||
]
|
||||
)
|
||||
@action(methods=["GET"], detail=False, url_path="@accessible")
|
||||
def accessible(self, request: Request) -> Response:
|
||||
"""Get applications accessible for user"""
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Custom list method that checks Policy based access instead of guardian"""
|
||||
should_cache = request.query_params.get("search", "") == ""
|
||||
|
||||
superuser_full_list = (
|
||||
str(request.query_params.get("superuser_full_list", "false")).lower() == "true"
|
||||
)
|
||||
if superuser_full_list and request.user.is_superuser:
|
||||
return super().list(request)
|
||||
|
||||
only_with_launch_url = str(
|
||||
request.query_params.get("only_with_launch_url", "false")
|
||||
).lower()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -80,10 +80,10 @@ class TestApplicationsAPI(APITestCase):
|
||||
self.assertEqual(body["passing"], False)
|
||||
self.assertEqual(body["messages"], ["dummy"])
|
||||
|
||||
def test_list_accessible(self):
|
||||
"""Test list operation without"""
|
||||
def test_list(self):
|
||||
"""Test list operation without superuser_full_list"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("authentik_api:application-accessible"))
|
||||
response = self.client.get(reverse("authentik_api:application-list"))
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{
|
||||
@@ -136,10 +136,12 @@ class TestApplicationsAPI(APITestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_list_rbac(self):
|
||||
"""Test list operation"""
|
||||
def test_list_superuser_full_list(self):
|
||||
"""Test list operation with superuser_full_list"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("authentik_api:application-list"))
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:application-list") + "?superuser_full_list=true"
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -18,7 +18,10 @@ from authentik.rbac.decorators import permission_required
|
||||
class EnrollmentTokenSerializer(ModelSerializer):
|
||||
|
||||
device_group_obj = DeviceAccessGroupSerializer(
|
||||
source="device_group", read_only=True, required=False
|
||||
source="device_group",
|
||||
read_only=True,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
|
||||
@@ -37,6 +37,8 @@ class AgentEnrollmentAuth(BaseAuthentication):
|
||||
token = EnrollmentToken.filter_not_expired(key=key).first()
|
||||
if not token:
|
||||
raise PermissionDenied()
|
||||
if not token.connector.enabled:
|
||||
raise PermissionDenied()
|
||||
CTX_AUTH_VIA.set("endpoint_token_enrollment")
|
||||
return (DeviceUser(), token)
|
||||
|
||||
@@ -51,6 +53,8 @@ class AgentAuth(BaseAuthentication):
|
||||
device_token = DeviceToken.filter_not_expired(key=key).first()
|
||||
if not device_token:
|
||||
raise PermissionDenied()
|
||||
if not device_token.device.connector.enabled:
|
||||
raise PermissionDenied()
|
||||
if device_token.device.device.is_expired:
|
||||
raise PermissionDenied()
|
||||
CTX_AUTH_VIA.set("endpoint_token")
|
||||
|
||||
@@ -58,6 +58,16 @@ class TestAgentAPI(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_enroll_disabled(self):
|
||||
self.connector.enabled = False
|
||||
self.connector.save()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-enroll"),
|
||||
data={"device_serial": generate_id(), "device_name": "bar"},
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_enroll_token_delete(self):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-enroll"),
|
||||
@@ -104,6 +114,16 @@ class TestAgentAPI(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_config_disabled(self):
|
||||
self.connector.enabled = False
|
||||
self.connector.save()
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:agentconnector-agent-config"),
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_check_in(self):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-check-in"),
|
||||
@@ -112,6 +132,16 @@ class TestAgentAPI(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
def test_check_in_disabled(self):
|
||||
self.connector.enabled = False
|
||||
self.connector.save()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-check-in"),
|
||||
data=CHECK_IN_DATA_VALID,
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_check_in_token_expired(self):
|
||||
self.device_token.expiring = True
|
||||
self.device_token.expires = now() - timedelta(hours=1)
|
||||
|
||||
@@ -44,6 +44,3 @@ class BaseController[T: "Connector"]:
|
||||
|
||||
def stage_view_authentication(self) -> StageView | None:
|
||||
return None
|
||||
|
||||
def sync_endpoints(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -162,11 +162,8 @@ class Connector(ScheduledModel, SerializerModel):
|
||||
|
||||
@property
|
||||
def schedule_specs(self) -> list[ScheduleSpec]:
|
||||
from authentik.endpoints.controller import Capabilities
|
||||
from authentik.endpoints.tasks import endpoints_sync
|
||||
|
||||
if Capabilities.ENROLL_AUTOMATIC_API not in self.controller(self).capabilities():
|
||||
return []
|
||||
return [
|
||||
ScheduleSpec(
|
||||
actor=endpoints_sync,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from authentik.endpoints.models import EndpointStage, StageMode
|
||||
from authentik.endpoints.models import Connector, EndpointStage, StageMode
|
||||
from authentik.flows.stage import StageView
|
||||
|
||||
PLAN_CONTEXT_ENDPOINT_CONNECTOR = "endpoint_connector"
|
||||
@@ -8,7 +8,10 @@ class EndpointStageView(StageView):
|
||||
|
||||
def _get_inner(self) -> StageView | None:
|
||||
stage: EndpointStage = self.executor.current_stage
|
||||
inner_stage: type[StageView] | None = stage.connector.stage
|
||||
connector: Connector = stage.connector
|
||||
if not connector.enabled:
|
||||
return None
|
||||
inner_stage: type[StageView] | None = connector.stage
|
||||
if not inner_stage:
|
||||
return None
|
||||
return inner_stage(self.executor, request=self.request)
|
||||
|
||||
@@ -17,11 +17,11 @@ def endpoints_sync(connector_pk: Any):
|
||||
connector: Connector | None = (
|
||||
Connector.objects.filter(pk=connector_pk).select_subclasses().first()
|
||||
)
|
||||
if not connector:
|
||||
if not connector or not connector.enabled:
|
||||
return
|
||||
controller = connector.controller
|
||||
ctrl = controller(connector)
|
||||
if Capabilities.ENROLL_AUTOMATIC_API not in ctrl.capabilities():
|
||||
if Capabilities.AUTOMATIC_API not in ctrl.capabilities():
|
||||
return
|
||||
LOGGER.info("Syncing connector", connector=connector.name)
|
||||
ctrl.sync_endpoints()
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.endpoints.controller import BaseController, Capabilities
|
||||
from authentik.endpoints.models import Connector
|
||||
from authentik.endpoints.tasks import endpoints_sync
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
class TestEndpointTasks(APITestCase):
|
||||
def test_agent_sync(self):
|
||||
class controller(BaseController):
|
||||
def capabilities(self):
|
||||
return [Capabilities.ENROLL_AUTOMATIC_API]
|
||||
|
||||
def sync_endpoints(self):
|
||||
pass
|
||||
|
||||
with patch.object(Connector, "controller", PropertyMock(return_value=controller)):
|
||||
connector = Connector.objects.create(name=generate_id())
|
||||
self.assertEqual(len(connector.schedule_specs), 1)
|
||||
|
||||
endpoints_sync.send(connector.pk).get_result(block=True)
|
||||
|
||||
def test_agent_no_sync(self):
|
||||
class controller(BaseController):
|
||||
def capabilities(self):
|
||||
return []
|
||||
|
||||
with patch.object(Connector, "controller", PropertyMock(return_value=controller)):
|
||||
connector = Connector.objects.create(name=generate_id())
|
||||
self.assertEqual(len(connector.schedule_specs), 0)
|
||||
|
||||
endpoints_sync.send(connector.pk).get_result(block=True)
|
||||
@@ -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):
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
"""GoogleChromeConnector API Views"""
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.endpoints.api.connectors import ConnectorSerializer
|
||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
|
||||
|
||||
|
||||
class GoogleChromeConnectorSerializer(EnterpriseRequiredMixin, ConnectorSerializer):
|
||||
"""GoogleChromeConnector Serializer"""
|
||||
|
||||
chrome_url = SerializerMethodField()
|
||||
|
||||
def get_chrome_url(self, _: GoogleChromeConnector) -> str | None:
|
||||
"""Full URL to be used in Google Workspace configuration"""
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return True
|
||||
return request.build_absolute_uri(
|
||||
reverse("authentik_endpoints_connectors_google_chrome:chrome")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = GoogleChromeConnector
|
||||
fields = ConnectorSerializer.Meta.fields + ["credentials", "chrome_url"]
|
||||
|
||||
|
||||
class GoogleChromeConnectorViewSet(UsedByMixin, ModelViewSet):
|
||||
"""GoogleChromeConnector Viewset"""
|
||||
|
||||
queryset = GoogleChromeConnector.objects.all()
|
||||
serializer_class = GoogleChromeConnectorSerializer
|
||||
filterset_fields = [
|
||||
"name",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
@@ -1,13 +0,0 @@
|
||||
"""authentik Endpoint app config"""
|
||||
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
|
||||
|
||||
class AuthentikEndpointsConnectorGoogleChromeAppConfig(EnterpriseConfig):
|
||||
"""authentik endpoint config"""
|
||||
|
||||
name = "authentik.enterprise.endpoints.connectors.google_chrome"
|
||||
label = "authentik_endpoints_connectors_google_chrome"
|
||||
verbose_name = "authentik Enterprise.Endpoints.Connectors.Google Chrome"
|
||||
default = True
|
||||
mountpoint = "endpoints/google/"
|
||||
@@ -1,116 +0,0 @@
|
||||
from json import dumps, loads
|
||||
|
||||
from django.http import HttpRequest, HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
from authentik.endpoints.controller import BaseController, Capabilities
|
||||
from authentik.endpoints.facts import DeviceFacts, OSFamily
|
||||
from authentik.endpoints.models import Device, DeviceConnection
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.google_schema import (
|
||||
DeviceSignals,
|
||||
VerifyChallengeResponseResult,
|
||||
)
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
|
||||
from authentik.policies.utils import delete_none_values
|
||||
|
||||
# Header we get from chrome that initiates verified access
|
||||
HEADER_DEVICE_TRUST = "X-Device-Trust"
|
||||
# Header we send to the client with the challenge
|
||||
HEADER_ACCESS_CHALLENGE = "X-Verified-Access-Challenge"
|
||||
# Header we get back from the client that we verify with google
|
||||
HEADER_ACCESS_CHALLENGE_RESPONSE = "X-Verified-Access-Challenge-Response"
|
||||
# Header value for x-device-trust that initiates the flow
|
||||
DEVICE_TRUST_VERIFIED_ACCESS = "VerifiedAccess"
|
||||
|
||||
|
||||
class GoogleChromeController(BaseController[GoogleChromeConnector]):
|
||||
|
||||
def __init__(self, connector):
|
||||
super().__init__(connector)
|
||||
self.google_client = build(
|
||||
"verifiedaccess",
|
||||
"v2",
|
||||
cache_discovery=False,
|
||||
**connector.google_credentials(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def vendor_identifier() -> str:
|
||||
return "chrome.google.com"
|
||||
|
||||
def capabilities(self) -> list[Capabilities]:
|
||||
return [Capabilities.STAGE_ENDPOINTS, Capabilities.ENROLL_AUTOMATIC_USER]
|
||||
|
||||
def generate_challenge(self, request: HttpRequest) -> HttpResponseRedirect:
|
||||
challenge = self.google_client.challenge().generate().execute()
|
||||
res = HttpResponseRedirect(
|
||||
request.build_absolute_uri(
|
||||
reverse("authentik_endpoints_connectors_google_chrome:chrome")
|
||||
)
|
||||
)
|
||||
res[HEADER_ACCESS_CHALLENGE] = dumps(challenge)
|
||||
return res
|
||||
|
||||
def validate_challenge(self, response: str) -> Device:
|
||||
response = VerifyChallengeResponseResult(
|
||||
self.google_client.challenge().verify(body=loads(response)).execute()
|
||||
)
|
||||
# Remove deprecated string representation of deviceSignals
|
||||
response.pop("deviceSignal", None)
|
||||
signals = DeviceSignals(response["deviceSignals"])
|
||||
device, _ = Device.objects.update_or_create(
|
||||
identifier=signals["serialNumber"],
|
||||
defaults={
|
||||
"name": signals["hostname"],
|
||||
},
|
||||
)
|
||||
conn, _ = DeviceConnection.objects.update_or_create(
|
||||
device=device,
|
||||
connector=self.connector,
|
||||
)
|
||||
conn.create_snapshot(self.convert_data(signals))
|
||||
return device
|
||||
|
||||
def convert_os_family(self, family) -> OSFamily:
|
||||
return {
|
||||
"CHROME_OS": OSFamily.linux,
|
||||
"CHROMIUM_OS": OSFamily.linux,
|
||||
"WINDOWS": OSFamily.windows,
|
||||
"MAC_OS_X": OSFamily.macOS,
|
||||
"LINUX": OSFamily.linux,
|
||||
}.get(family, OSFamily.other)
|
||||
|
||||
def convert_data(self, raw_signals: DeviceSignals):
|
||||
data = {
|
||||
"os": delete_none_values(
|
||||
{
|
||||
"family": self.convert_os_family(raw_signals["operatingSystem"]),
|
||||
"version": raw_signals["osVersion"],
|
||||
}
|
||||
),
|
||||
"disks": [],
|
||||
"network": delete_none_values(
|
||||
{
|
||||
"hostname": raw_signals["hostname"],
|
||||
"interfaces": [],
|
||||
"firewall_enabled": raw_signals["osFirewall"] == "OS_FIREWALL_ENABLED",
|
||||
},
|
||||
),
|
||||
"hardware": delete_none_values(
|
||||
{
|
||||
"model": raw_signals["deviceModel"],
|
||||
"manufacturer": raw_signals["deviceManufacturer"],
|
||||
"serial": raw_signals["serialNumber"],
|
||||
}
|
||||
),
|
||||
"vendor": {
|
||||
self.vendor_identifier(): {
|
||||
"agent_version": raw_signals["browserVersion"],
|
||||
"raw": raw_signals,
|
||||
},
|
||||
},
|
||||
}
|
||||
facts = DeviceFacts(data=data)
|
||||
facts.is_valid(raise_exception=True)
|
||||
return facts.validated_data
|
||||
@@ -1,129 +0,0 @@
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
# Based on https://github.com/henribru/google-api-python-client-stubs/blob/master/googleapiclient-stubs/_apis/verifiedaccess/v2/schemas.pyi
|
||||
|
||||
|
||||
class Antivirus(TypedDict, total=False):
|
||||
state: Literal["STATE_UNSPECIFIED", "MISSING", "DISABLED", "ENABLED"]
|
||||
|
||||
|
||||
class Challenge(TypedDict, total=False):
|
||||
challenge: str
|
||||
|
||||
|
||||
class CrowdStrikeAgent(TypedDict, total=False):
|
||||
agentId: str
|
||||
customerId: str
|
||||
|
||||
|
||||
class DeviceSignals(TypedDict, total=False):
|
||||
allowScreenLock: bool
|
||||
antivirus: Antivirus
|
||||
browserVersion: str
|
||||
builtInDnsClientEnabled: bool
|
||||
chromeRemoteDesktopAppBlocked: bool
|
||||
crowdStrikeAgent: CrowdStrikeAgent
|
||||
deviceAffiliationIds: list[str]
|
||||
deviceEnrollmentDomain: str
|
||||
deviceManufacturer: str
|
||||
deviceModel: str
|
||||
diskEncryption: Literal[
|
||||
"DISK_ENCRYPTION_UNSPECIFIED",
|
||||
"DISK_ENCRYPTION_UNKNOWN",
|
||||
"DISK_ENCRYPTION_DISABLED",
|
||||
"DISK_ENCRYPTION_ENCRYPTED",
|
||||
]
|
||||
displayName: str
|
||||
hostname: str
|
||||
imei: list[str]
|
||||
macAddresses: list[str]
|
||||
meid: list[str]
|
||||
operatingSystem: Literal[
|
||||
"OPERATING_SYSTEM_UNSPECIFIED",
|
||||
"CHROME_OS",
|
||||
"CHROMIUM_OS",
|
||||
"WINDOWS",
|
||||
"MAC_OS_X",
|
||||
"LINUX",
|
||||
]
|
||||
osFirewall: Literal[
|
||||
"OS_FIREWALL_UNSPECIFIED",
|
||||
"OS_FIREWALL_UNKNOWN",
|
||||
"OS_FIREWALL_DISABLED",
|
||||
"OS_FIREWALL_ENABLED",
|
||||
]
|
||||
osVersion: str
|
||||
passwordProtectionWarningTrigger: Literal[
|
||||
"PASSWORD_PROTECTION_WARNING_TRIGGER_UNSPECIFIED",
|
||||
"POLICY_UNSET",
|
||||
"PASSWORD_PROTECTION_OFF",
|
||||
"PASSWORD_REUSE",
|
||||
"PHISHING_REUSE",
|
||||
]
|
||||
profileAffiliationIds: list[str]
|
||||
profileEnrollmentDomain: str
|
||||
realtimeUrlCheckMode: Literal[
|
||||
"REALTIME_URL_CHECK_MODE_UNSPECIFIED",
|
||||
"REALTIME_URL_CHECK_MODE_DISABLED",
|
||||
"REALTIME_URL_CHECK_MODE_ENABLED_MAIN_FRAME",
|
||||
]
|
||||
safeBrowsingProtectionLevel: Literal[
|
||||
"SAFE_BROWSING_PROTECTION_LEVEL_UNSPECIFIED", "INACTIVE", "STANDARD", "ENHANCED"
|
||||
]
|
||||
screenLockSecured: Literal[
|
||||
"SCREEN_LOCK_SECURED_UNSPECIFIED",
|
||||
"SCREEN_LOCK_SECURED_UNKNOWN",
|
||||
"SCREEN_LOCK_SECURED_DISABLED",
|
||||
"SCREEN_LOCK_SECURED_ENABLED",
|
||||
]
|
||||
secureBootMode: Literal[
|
||||
"SECURE_BOOT_MODE_UNSPECIFIED",
|
||||
"SECURE_BOOT_MODE_UNKNOWN",
|
||||
"SECURE_BOOT_MODE_DISABLED",
|
||||
"SECURE_BOOT_MODE_ENABLED",
|
||||
]
|
||||
serialNumber: str
|
||||
siteIsolationEnabled: bool
|
||||
systemDnsServers: list[str]
|
||||
thirdPartyBlockingEnabled: bool
|
||||
trigger: Literal["TRIGGER_UNSPECIFIED", "TRIGGER_BROWSER_NAVIGATION", "TRIGGER_LOGIN_SCREEN"]
|
||||
windowsMachineDomain: str
|
||||
windowsUserDomain: str
|
||||
|
||||
|
||||
class Empty(TypedDict, total=False): ...
|
||||
|
||||
|
||||
class VerifyChallengeResponseRequest(TypedDict, total=False):
|
||||
challengeResponse: str
|
||||
expectedIdentity: str
|
||||
|
||||
|
||||
class VerifyChallengeResponseResult(TypedDict, total=False):
|
||||
attestedDeviceId: str
|
||||
customerId: str
|
||||
deviceEnrollmentId: str
|
||||
devicePermanentId: str
|
||||
deviceSignal: str
|
||||
deviceSignals: DeviceSignals
|
||||
keyTrustLevel: Literal[
|
||||
"KEY_TRUST_LEVEL_UNSPECIFIED",
|
||||
"CHROME_OS_VERIFIED_MODE",
|
||||
"CHROME_OS_DEVELOPER_MODE",
|
||||
"CHROME_BROWSER_HW_KEY",
|
||||
"CHROME_BROWSER_OS_KEY",
|
||||
"CHROME_BROWSER_NO_KEY",
|
||||
]
|
||||
profileCustomerId: str
|
||||
profileKeyTrustLevel: Literal[
|
||||
"KEY_TRUST_LEVEL_UNSPECIFIED",
|
||||
"CHROME_OS_VERIFIED_MODE",
|
||||
"CHROME_OS_DEVELOPER_MODE",
|
||||
"CHROME_BROWSER_HW_KEY",
|
||||
"CHROME_BROWSER_OS_KEY",
|
||||
"CHROME_BROWSER_NO_KEY",
|
||||
]
|
||||
profilePermanentId: str
|
||||
signedPublicKeyAndChallenge: str
|
||||
virtualDeviceId: str
|
||||
virtualProfileId: str
|
||||
@@ -1,38 +0,0 @@
|
||||
# Generated by Django 5.2.11 on 2026-03-01 18:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_endpoints", "0004_deviceaccessgroup_attributes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="GoogleChromeConnector",
|
||||
fields=[
|
||||
(
|
||||
"connector_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_endpoints.connector",
|
||||
),
|
||||
),
|
||||
("credentials", models.JSONField()),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Google Device Trust Connector",
|
||||
"verbose_name_plural": "Google Device Trust Connectors",
|
||||
},
|
||||
bases=("authentik_endpoints.connector",),
|
||||
),
|
||||
]
|
||||
@@ -1,69 +0,0 @@
|
||||
"""Endpoint stage"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from google.oauth2.service_account import Credentials
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.endpoints.models import Connector
|
||||
from authentik.flows.stage import StageView
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
|
||||
GoogleChromeController,
|
||||
)
|
||||
|
||||
|
||||
class GoogleChromeConnector(Connector):
|
||||
"""Verify Google Chrome Device Trust connection for the user's browser."""
|
||||
|
||||
credentials = models.JSONField()
|
||||
|
||||
def google_credentials(self):
|
||||
return {
|
||||
"credentials": Credentials.from_service_account_info(
|
||||
self.credentials, scopes=["https://www.googleapis.com/auth/verifiedaccess"]
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def icon_url(self):
|
||||
return static("authentik/sources/google.svg")
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.api import (
|
||||
GoogleChromeConnectorSerializer,
|
||||
)
|
||||
|
||||
return GoogleChromeConnectorSerializer
|
||||
|
||||
@property
|
||||
def stage(self) -> type[StageView] | None:
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.stage import (
|
||||
GoogleChromeStageView,
|
||||
)
|
||||
|
||||
return GoogleChromeStageView
|
||||
|
||||
@property
|
||||
def controller(self) -> type[GoogleChromeController]:
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
|
||||
GoogleChromeController,
|
||||
)
|
||||
|
||||
return GoogleChromeController
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-endpoints-connector-gdtc-form"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Google Device Trust Connector {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Google Device Trust Connector")
|
||||
verbose_name_plural = _("Google Device Trust Connectors")
|
||||
@@ -1,32 +0,0 @@
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from authentik.flows.challenge import (
|
||||
Challenge,
|
||||
ChallengeResponse,
|
||||
FrameChallenge,
|
||||
FrameChallengeResponse,
|
||||
)
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
|
||||
|
||||
class GoogleChromeStageView(ChallengeStageView):
|
||||
"""Endpoint stage"""
|
||||
|
||||
response_class = FrameChallengeResponse
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
return FrameChallenge(
|
||||
data={
|
||||
"component": "xak-flow-frame",
|
||||
"url": self.request.build_absolute_uri(
|
||||
reverse("authentik_endpoints_connectors_google_chrome:chrome")
|
||||
),
|
||||
"loading_overlay": True,
|
||||
"loading_text": _("Verifying your browser..."),
|
||||
}
|
||||
)
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
return self.executor.stage_ok()
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"devicePermanentId": "6f30327d-e436-4f7a-9f89-c37a7b6bf408",
|
||||
"keyTrustLevel": "CHROME_BROWSER_HW_KEY",
|
||||
"virtualDeviceId": "Z5DDF07GK6",
|
||||
"customerId": "qewrqer",
|
||||
"deviceSignals": {
|
||||
"deviceManufacturer": "Apple Inc.",
|
||||
"deviceModel": "MacBookPro18,1",
|
||||
"operatingSystem": "MAC_OS_X",
|
||||
"osVersion": "26.2.0",
|
||||
"displayName": "jens-mac-vm",
|
||||
"diskEncryption": "DISK_ENCRYPTION_ENCRYPTED",
|
||||
"serialNumber": "Z5DDF07GK6",
|
||||
"osFirewall": "OS_FIREWALL_DISABLED",
|
||||
"systemDnsServers": [
|
||||
"10.120.20.250:53"
|
||||
],
|
||||
"hostname": "jens-mac-vm.lab.beryju.org",
|
||||
"macAddresses": [
|
||||
"f4:d4:88:79:07:0e"
|
||||
],
|
||||
"screenLockSecured": "SCREEN_LOCK_SECURED_ENABLED",
|
||||
"deviceEnrollmentDomain": "beryju.org",
|
||||
"browserVersion": "145.0.7632.76",
|
||||
"deviceAffiliationIds": [
|
||||
"qewrqer"
|
||||
],
|
||||
"builtInDnsClientEnabled": true,
|
||||
"chromeRemoteDesktopAppBlocked": false,
|
||||
"safeBrowsingProtectionLevel": "STANDARD",
|
||||
"siteIsolationEnabled": true,
|
||||
"passwordProtectionWarningTrigger": "POLICY_UNSET",
|
||||
"realtimeUrlCheckMode": "REALTIME_URL_CHECK_MODE_DISABLED",
|
||||
"trigger": "TRIGGER_BROWSER_NAVIGATION"
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
from json import dumps
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.tests.utils import RequestFactory
|
||||
from authentik.endpoints.facts import OSFamily
|
||||
from authentik.endpoints.models import Device
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
|
||||
HEADER_ACCESS_CHALLENGE,
|
||||
GoogleChromeController,
|
||||
)
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
|
||||
from authentik.enterprise.providers.google_workspace.clients.test_http import MockHTTP
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
|
||||
|
||||
class TestGoogleChromeConnector(APITestCase):
|
||||
def setUp(self):
|
||||
self.connector = GoogleChromeConnector.objects.create(
|
||||
name=generate_id(),
|
||||
credentials={},
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
self.api_key = generate_id()
|
||||
|
||||
def test_generate_challenge(self):
|
||||
req = self.factory.get("/")
|
||||
challenge = generate_id()
|
||||
http = MockHTTP()
|
||||
http.add_response(
|
||||
f"https://verifiedaccess.googleapis.com/v2/challenge:generate?key={self.api_key}&alt=json",
|
||||
{"challenge": challenge},
|
||||
method="POST",
|
||||
)
|
||||
with patch(
|
||||
"authentik.enterprise.endpoints.connectors.google_chrome.models.GoogleChromeConnector.google_credentials",
|
||||
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
|
||||
):
|
||||
controller = GoogleChromeController(self.connector)
|
||||
res = controller.generate_challenge(req)
|
||||
self.assertEqual(
|
||||
res["Location"],
|
||||
req.build_absolute_uri(
|
||||
reverse("authentik_endpoints_connectors_google_chrome:chrome")
|
||||
),
|
||||
)
|
||||
self.assertEqual(res.headers[HEADER_ACCESS_CHALLENGE], dumps({"challenge": challenge}))
|
||||
|
||||
def test_validate_challenge(self):
|
||||
http = MockHTTP()
|
||||
http.add_response(
|
||||
f"https://verifiedaccess.googleapis.com/v2/challenge:verify?key={self.api_key}&alt=json",
|
||||
load_fixture("fixtures/host_macos.json"),
|
||||
method="POST",
|
||||
)
|
||||
with patch(
|
||||
"authentik.enterprise.endpoints.connectors.google_chrome.models.GoogleChromeConnector.google_credentials",
|
||||
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
|
||||
):
|
||||
controller = GoogleChromeController(self.connector)
|
||||
controller.validate_challenge(dumps("{}"))
|
||||
device = Device.objects.get(identifier="Z5DDF07GK6")
|
||||
self.assertIsNotNone(device)
|
||||
self.assertEqual(device.cached_facts.data["os"]["family"], OSFamily.macOS)
|
||||
@@ -1,91 +0,0 @@
|
||||
from json import dumps
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.tests.utils import RequestFactory, create_test_flow
|
||||
from authentik.endpoints.models import Device, EndpointStage
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
|
||||
from authentik.enterprise.providers.google_workspace.clients.test_http import MockHTTP
|
||||
from authentik.flows.models import FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_DEVICE
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
|
||||
|
||||
class TestChromeDTCView(FlowTestCase):
|
||||
def setUp(self):
|
||||
self.flow = create_test_flow()
|
||||
self.connector = GoogleChromeConnector.objects.create(
|
||||
name=generate_id(),
|
||||
credentials={},
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
self.api_key = generate_id()
|
||||
self.stage = EndpointStage.objects.create(
|
||||
name=generate_id(),
|
||||
connector=self.connector,
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
target=self.flow,
|
||||
stage=self.stage,
|
||||
order=0,
|
||||
)
|
||||
|
||||
def test_dtc_generate_verify(self):
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
self.assertStageResponse(
|
||||
res,
|
||||
self.flow,
|
||||
component="xak-flow-frame",
|
||||
url="http://testserver/endpoints/google/chrome/",
|
||||
)
|
||||
|
||||
challenge = generate_id()
|
||||
http = MockHTTP()
|
||||
http.add_response(
|
||||
f"https://verifiedaccess.googleapis.com/v2/challenge:generate?key={self.api_key}&alt=json",
|
||||
{"challenge": challenge},
|
||||
method="POST",
|
||||
)
|
||||
http.add_response(
|
||||
f"https://verifiedaccess.googleapis.com/v2/challenge:verify?key={self.api_key}&alt=json",
|
||||
load_fixture("fixtures/host_macos.json"),
|
||||
method="POST",
|
||||
)
|
||||
with patch(
|
||||
"authentik.enterprise.endpoints.connectors.google_chrome.models.GoogleChromeConnector.google_credentials",
|
||||
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
|
||||
):
|
||||
# Generate challenge
|
||||
res = self.client.get(
|
||||
reverse("authentik_endpoints_connectors_google_chrome:chrome"),
|
||||
HTTP_X_DEVICE_TRUST="VerifiedAccess",
|
||||
)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertEqual(
|
||||
res.headers["X-Verified-Access-Challenge"],
|
||||
dumps({"challenge": challenge}),
|
||||
)
|
||||
|
||||
# Validate challenge
|
||||
res = self.client.get(
|
||||
reverse("authentik_endpoints_connectors_google_chrome:chrome"),
|
||||
HTTP_X_VERIFIED_ACCESS_CHALLENGE_RESPONSE=dumps({}),
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
device = Device.objects.get(identifier="Z5DDF07GK6")
|
||||
self.assertIsNotNone(device)
|
||||
|
||||
# Continue flow
|
||||
with self.assertFlowFinishes() as plan:
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
self.assertStageRedirects(res, "/")
|
||||
plan = plan()
|
||||
plan_device = plan.context[PLAN_CONTEXT_DEVICE]
|
||||
self.assertEqual(device.pk, plan_device.pk)
|
||||
@@ -1,16 +0,0 @@
|
||||
"""API URLs"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.api import GoogleChromeConnectorViewSet
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.views.dtc import (
|
||||
GoogleChromeDeviceTrustConnector,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("chrome/", GoogleChromeDeviceTrustConnector.as_view(), name="chrome"),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
("endpoints/google_chrome/connectors", GoogleChromeConnectorViewSet),
|
||||
]
|
||||
@@ -1,46 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
|
||||
from authentik.endpoints.models import EndpointStage
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
|
||||
HEADER_ACCESS_CHALLENGE_RESPONSE,
|
||||
HEADER_DEVICE_TRUST,
|
||||
GoogleChromeController,
|
||||
)
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
|
||||
from authentik.flows.planner import PLAN_CONTEXT_DEVICE, FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
|
||||
|
||||
@method_decorator(xframe_options_sameorigin, name="dispatch")
|
||||
class GoogleChromeDeviceTrustConnector(View):
|
||||
"""Google Chrome Device-trust connector based endpoint authenticator"""
|
||||
|
||||
def get_flow_plan(self) -> FlowPlan:
|
||||
flow_plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
||||
return flow_plan
|
||||
|
||||
def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
|
||||
super().setup(request, *args, **kwargs)
|
||||
stage: EndpointStage = self.get_flow_plan().bindings[0].stage
|
||||
connector = GoogleChromeConnector.objects.filter(pk=stage.connector_id).first()
|
||||
if not connector:
|
||||
return HttpResponseBadRequest()
|
||||
self.controller: GoogleChromeController = connector.controller(connector)
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
x_device_trust = request.headers.get(HEADER_DEVICE_TRUST)
|
||||
x_access_challenge_response = request.headers.get(HEADER_ACCESS_CHALLENGE_RESPONSE)
|
||||
if x_device_trust == "VerifiedAccess" and x_access_challenge_response is None:
|
||||
return self.controller.generate_challenge(request)
|
||||
if x_access_challenge_response:
|
||||
device = self.controller.validate_challenge(x_access_challenge_response)
|
||||
flow_plan = self.get_flow_plan()
|
||||
flow_plan.context[PLAN_CONTEXT_DEVICE] = device
|
||||
self.request.session[SESSION_KEY_PLAN] = flow_plan
|
||||
return TemplateResponse(request, "flows/frame-submit.html")
|
||||
@@ -8,7 +8,6 @@ from dramatiq.actor import actor
|
||||
from requests.exceptions import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.apps import AppAccessWithoutBindings
|
||||
from authentik.core.models import User
|
||||
from authentik.enterprise.providers.ssf.models import (
|
||||
DeliveryMethods,
|
||||
@@ -69,7 +68,6 @@ def _check_app_access(stream: Stream, event_data: dict) -> bool:
|
||||
if not user:
|
||||
return True
|
||||
engine = PolicyEngine(stream.provider.backchannel_application, user)
|
||||
engine.empty_result = AppAccessWithoutBindings.get()
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
return engine.passing
|
||||
|
||||
@@ -4,7 +4,6 @@ TENANT_APPS = [
|
||||
"authentik.enterprise.audit",
|
||||
"authentik.enterprise.endpoints.connectors.agent",
|
||||
"authentik.enterprise.endpoints.connectors.fleet",
|
||||
"authentik.enterprise.endpoints.connectors.google_chrome",
|
||||
"authentik.enterprise.lifecycle",
|
||||
"authentik.enterprise.policies.unique_password",
|
||||
"authentik.enterprise.providers.google_workspace",
|
||||
|
||||
@@ -9,11 +9,6 @@ from django.views import View
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
|
||||
HEADER_ACCESS_CHALLENGE,
|
||||
HEADER_ACCESS_CHALLENGE_RESPONSE,
|
||||
HEADER_DEVICE_TRUST,
|
||||
)
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
||||
AuthenticatorEndpointGDTCStage,
|
||||
EndpointDevice,
|
||||
@@ -24,6 +19,15 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
from authentik.stages.user_login.stage import PLAN_CONTEXT_METHOD_ARGS_KNOWN_DEVICE
|
||||
|
||||
# Header we get from chrome that initiates verified access
|
||||
HEADER_DEVICE_TRUST = "X-Device-Trust"
|
||||
# Header we send to the client with the challenge
|
||||
HEADER_ACCESS_CHALLENGE = "X-Verified-Access-Challenge"
|
||||
# Header we get back from the client that we verify with google
|
||||
HEADER_ACCESS_CHALLENGE_RESPONSE = "X-Verified-Access-Challenge-Response"
|
||||
# Header value for x-device-trust that initiates the flow
|
||||
DEVICE_TRUST_VERIFIED_ACCESS = "VerifiedAccess"
|
||||
|
||||
PLAN_CONTEXT_METHOD_ARGS_ENDPOINTS = "endpoints"
|
||||
|
||||
|
||||
@@ -90,4 +94,4 @@ class GoogleChromeDeviceTrustConnector(View):
|
||||
PLAN_CONTEXT_METHOD_ARGS_KNOWN_DEVICE, True
|
||||
)
|
||||
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||
return TemplateResponse(request, "flows/frame-submit.html")
|
||||
return TemplateResponse(request, "stages/authenticator_endpoint/google_chrome_dtc.html")
|
||||
|
||||
@@ -63,7 +63,6 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||
"mode",
|
||||
"mode_verbose",
|
||||
"webhook_url",
|
||||
"webhook_ca",
|
||||
"webhook_mapping_body",
|
||||
"webhook_mapping_headers",
|
||||
"email_subject_prefix",
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-10 10:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_crypto", "0006_certificatekeypair_cert_expiry_and_more"),
|
||||
("authentik_events", "0016_alter_event_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="notificationtransport",
|
||||
name="webhook_ca",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
help_text="When set, the selected ceritifcate is used to validate the certificate of the webhook server.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="authentik_crypto.certificatekeypair",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -28,7 +28,6 @@ from authentik.core.middleware import (
|
||||
SESSION_KEY_IMPERSONATE_USER,
|
||||
)
|
||||
from authentik.core.models import ExpiringModel, Group, PropertyMapping, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.context_processors.base import get_context_processors
|
||||
from authentik.events.utils import (
|
||||
cleanse_dict,
|
||||
@@ -42,7 +41,6 @@ from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.errors import exception_to_dict
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.outposts.docker_tls import DockerInlineTLS
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.root.middleware import ClientIPMiddleware
|
||||
from authentik.root.ws.consumer import build_user_group
|
||||
@@ -328,16 +326,6 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
email_template = models.TextField(default=EmailTemplates.EVENT_NOTIFICATION)
|
||||
|
||||
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
|
||||
webhook_ca = models.ForeignKey(
|
||||
CertificateKeyPair,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
help_text=_(
|
||||
"When set, the selected ceritifcate is used to "
|
||||
"validate the certificate of the webhook server."
|
||||
),
|
||||
)
|
||||
webhook_mapping_body = models.ForeignKey(
|
||||
"NotificationWebhookMapping",
|
||||
on_delete=models.SET_DEFAULT,
|
||||
@@ -421,29 +409,21 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
notification=notification,
|
||||
)
|
||||
)
|
||||
|
||||
def send(**kwargs):
|
||||
try:
|
||||
response = get_http_session().post(
|
||||
self.webhook_url,
|
||||
json=default_body,
|
||||
headers=headers,
|
||||
**kwargs,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
raise NotificationTransportError(
|
||||
exc.response.text if exc.response else str(exc)
|
||||
) from exc
|
||||
return [
|
||||
response.status_code,
|
||||
response.text,
|
||||
]
|
||||
|
||||
if self.webhook_ca:
|
||||
with DockerInlineTLS(self.webhook_ca, authentication_kp=None) as tls:
|
||||
return send(verify=tls.ca_cert)
|
||||
return send()
|
||||
try:
|
||||
response = get_http_session().post(
|
||||
self.webhook_url,
|
||||
json=default_body,
|
||||
headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
raise NotificationTransportError(
|
||||
exc.response.text if exc.response else str(exc)
|
||||
) from exc
|
||||
return [
|
||||
response.status_code,
|
||||
response.text,
|
||||
]
|
||||
|
||||
def send_webhook_slack(self, notification: Notification) -> list[str]:
|
||||
"""Send notification to slack or slack-compatible endpoints"""
|
||||
|
||||
@@ -10,7 +10,6 @@ from requests_mock import Mocker
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.api.notification_transports import NotificationTransportSerializer
|
||||
from authentik.events.models import (
|
||||
Event,
|
||||
@@ -62,37 +61,6 @@ class TestEventTransports(TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_transport_webhook_ca_invalid_unset(self):
|
||||
"""Test webhook transport"""
|
||||
transport: NotificationTransport = NotificationTransport.objects.create(
|
||||
name=generate_id(),
|
||||
mode=TransportMode.WEBHOOK,
|
||||
webhook_url="https://localhost:1234/test",
|
||||
)
|
||||
with Mocker() as mocker:
|
||||
mocker.post("https://localhost:1234/test")
|
||||
transport.send(self.notification)
|
||||
self.assertEqual(mocker.call_count, 1)
|
||||
self.assertTrue(mocker.request_history[0].verify)
|
||||
|
||||
def test_transport_webhook_ca(self):
|
||||
"""Test webhook transport"""
|
||||
kp = CertificateKeyPair.objects.create(
|
||||
name=generate_id(),
|
||||
certificate_data="foo",
|
||||
)
|
||||
transport: NotificationTransport = NotificationTransport.objects.create(
|
||||
name=generate_id(),
|
||||
mode=TransportMode.WEBHOOK,
|
||||
webhook_url="https://localhost:1234/test",
|
||||
webhook_ca=kp,
|
||||
)
|
||||
with Mocker() as mocker:
|
||||
mocker.post("https://localhost:1234/test")
|
||||
transport.send(self.notification)
|
||||
self.assertEqual(mocker.call_count, 1)
|
||||
self.assertIsNotNone(mocker.request_history[0].verify)
|
||||
|
||||
def test_transport_webhook_mapping(self):
|
||||
"""Test webhook transport with custom mapping"""
|
||||
mapping_body = NotificationWebhookMapping.objects.create(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""authentik flows app config"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from prometheus_client import Gauge, Histogram
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
@@ -28,14 +27,12 @@ class RefreshOtherFlowsAfterAuthentication(Flag[bool], key="flows_refresh_others
|
||||
|
||||
default = False
|
||||
visibility = "public"
|
||||
description = _("Refresh other tabs after successful authentication.")
|
||||
|
||||
|
||||
class ContinuousLogin(Flag[bool], key="flows_continuous_login"):
|
||||
|
||||
default = False
|
||||
visibility = "public"
|
||||
description = _("Upon successful authentication, re-start authentication in other open tabs.")
|
||||
|
||||
|
||||
class AuthentikFlowsConfig(ManagedAppConfig):
|
||||
|
||||
@@ -27,10 +27,8 @@
|
||||
"layout": "{{ flow.layout }}",
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block interface_stylesheet %}
|
||||
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/flow-%v.css' %}" />
|
||||
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/static-%v.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
|
||||
@@ -423,5 +423,4 @@ if __name__ == "__main__":
|
||||
if len(argv) < 2: # noqa: PLR2004
|
||||
print(dumps(CONFIG.raw, indent=4, cls=AttrEncoder))
|
||||
else:
|
||||
for arg in argv[1:]:
|
||||
print(CONFIG.get(arg))
|
||||
print(CONFIG.get(argv[-1]))
|
||||
|
||||
@@ -22,7 +22,6 @@ postgresql:
|
||||
port: 5432
|
||||
password: "env://POSTGRES_PASSWORD"
|
||||
sslmode: disable
|
||||
conn_health_checks: false
|
||||
use_pool: False
|
||||
test:
|
||||
name: test_authentik
|
||||
@@ -33,18 +32,12 @@ postgresql:
|
||||
# host: replica1.example.com
|
||||
|
||||
listen:
|
||||
http:
|
||||
- "[::]:9000"
|
||||
https:
|
||||
- "[::]:9443"
|
||||
ldap:
|
||||
- "[::]:3389"
|
||||
ldaps:
|
||||
- "[::]:6636"
|
||||
radius:
|
||||
- "[::]:1812"
|
||||
metrics:
|
||||
- "[::]:9300"
|
||||
http: 0.0.0.0:9000
|
||||
https: 0.0.0.0:9443
|
||||
ldap: 0.0.0.0:3389
|
||||
ldaps: 0.0.0.0:6636
|
||||
radius: 0.0.0.0:1812
|
||||
metrics: 0.0.0.0:9300
|
||||
debug: 0.0.0.0:9900
|
||||
debug_py: 0.0.0.0:9901
|
||||
trusted_proxy_cidrs:
|
||||
@@ -144,7 +137,8 @@ tenants:
|
||||
blueprints_dir: /blueprints
|
||||
|
||||
web:
|
||||
workers: 2
|
||||
# No default here as it's set dynamically
|
||||
# workers: 2
|
||||
threads: 4
|
||||
path: /
|
||||
timeout_http_read_header: 5s
|
||||
@@ -172,7 +166,6 @@ storage:
|
||||
# region: "us-east-1"
|
||||
# use_ssl: True
|
||||
# endpoint: "https://s3.us-east-1.amazonaws.com"
|
||||
# signature_version: "s3v4"
|
||||
# access_key: ""
|
||||
# secret_key: ""
|
||||
# bucket_name: "authentik-data"
|
||||
|
||||
@@ -185,8 +185,10 @@ class KubernetesObjectReconciler[T]:
|
||||
|
||||
patch = self.get_patch()
|
||||
if patch is not None:
|
||||
current_json = ApiClient().sanitize_for_serialization(current)
|
||||
|
||||
try:
|
||||
current_json = ApiClient().sanitize_for_serialization(current)
|
||||
except AttributeError:
|
||||
current_json = asdict(current)
|
||||
try:
|
||||
if apply_patch(current_json, patch) != current_json:
|
||||
raise NeedsUpdate()
|
||||
|
||||
@@ -27,12 +27,6 @@ class DockerInlineTLS:
|
||||
self.authentication_kp = authentication_kp
|
||||
self._paths = []
|
||||
|
||||
def __enter__(self):
|
||||
return self.write()
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
self.cleanup()
|
||||
|
||||
def write_file(self, name: str, contents: str) -> str:
|
||||
"""Wrapper for mkstemp that uses fdopen"""
|
||||
path = Path(gettempdir(), name)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""authentik policies app config
|
||||
"""Authentik policies app config
|
||||
|
||||
Every system policy should be its own Django app under the `policies` app.
|
||||
For example: The 'dummy' policy is available at `authentik.policies.dummy`.
|
||||
@@ -7,6 +7,7 @@ For example: The 'dummy' policy is available at `authentik.policies.dummy`.
|
||||
from prometheus_client import Gauge, Histogram
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.tenants.flags import Flag
|
||||
|
||||
GAUGE_POLICIES_CACHED = Gauge(
|
||||
"authentik_policies_cached",
|
||||
@@ -31,6 +32,12 @@ HIST_POLICIES_EXECUTION_TIME = Histogram(
|
||||
)
|
||||
|
||||
|
||||
class BufferedPolicyAccessViewFlag(Flag[bool], key="policies_buffered_access_view"):
|
||||
|
||||
default = False
|
||||
visibility = "public"
|
||||
|
||||
|
||||
class AuthentikPoliciesConfig(ManagedAppConfig):
|
||||
"""authentik policies app config"""
|
||||
|
||||
@@ -38,3 +45,4 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
|
||||
label = "authentik_policies"
|
||||
verbose_name = "authentik Policies"
|
||||
default = True
|
||||
mountpoint = "policy/"
|
||||
|
||||
121
authentik/policies/templates/policies/buffer.html
Normal file
121
authentik/policies/templates/policies/buffer.html
Normal file
@@ -0,0 +1,121 @@
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
let redirecting = false;
|
||||
|
||||
async function checkAuth() {
|
||||
if (redirecting) {
|
||||
console.debug(
|
||||
"authentik/policies/buffer: Already authenticating in another tab. This page will refresh once authentication is completed.",
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const url = "{{ check_auth_url }}";
|
||||
console.debug("authentik/policies/buffer: Checking authentication...");
|
||||
|
||||
return fetch(url, {
|
||||
method: "HEAD",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status >= 400) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.debug("authentik/policies/buffer: Continuing");
|
||||
|
||||
if ("{{ auth_req_method }}" === "post") {
|
||||
document.querySelector("form")?.submit();
|
||||
return true;
|
||||
}
|
||||
|
||||
window.location.assign("{{ continue_url|escapejs }}");
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("authentik/policies/buffer: Error checking authentication.", error);
|
||||
return false;
|
||||
})
|
||||
}
|
||||
|
||||
const offset = 20;
|
||||
|
||||
let timeoutID = -1;
|
||||
let timeout = 100;
|
||||
let attempts = 0;
|
||||
|
||||
async function main() {
|
||||
window.clearTimeout(timeoutID);
|
||||
|
||||
attempts += 1;
|
||||
|
||||
redirecting = await checkAuth();
|
||||
|
||||
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
|
||||
|
||||
timeoutID = window.setTimeout(main, timeout);
|
||||
|
||||
timeout += offset * attempts;
|
||||
|
||||
if (timeout >= 2000) {
|
||||
timeout = 2000;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("visibilitychange", async () => {
|
||||
if (document.hidden) return;
|
||||
|
||||
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
|
||||
|
||||
redirecting = await checkAuth();
|
||||
});
|
||||
|
||||
main();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
{% trans 'Waiting for authentication...' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
|
||||
{% if auth_req_method == "post" %}
|
||||
{% for key, value in auth_req_body.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<div class="pf-c-empty-state__icon">
|
||||
<span class="pf-c-spinner pf-m-xl" role="progressbar">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
|
||||
{% trans "Authenticate in this tab" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,19 +1,29 @@
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, Group, Provider
|
||||
from authentik.core.tests.utils import (
|
||||
RequestFactory,
|
||||
create_test_brand,
|
||||
create_test_flow,
|
||||
create_test_user,
|
||||
)
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.apps import BufferedPolicyAccessViewFlag
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.policies.views import (
|
||||
QS_BUFFER_ID,
|
||||
SESSION_KEY_BUFFER,
|
||||
BufferedPolicyAccessView,
|
||||
BufferView,
|
||||
PolicyAccessView,
|
||||
)
|
||||
from authentik.tenants.flags import patch_flag
|
||||
|
||||
|
||||
class TestPolicyViews(TestCase):
|
||||
@@ -114,3 +124,71 @@ class TestPolicyViews(TestCase):
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertEqual(res.url, "/if/flow/default-authentication-flow/?next=%2F")
|
||||
|
||||
@patch_flag(BufferedPolicyAccessViewFlag, True)
|
||||
def test_pav_buffer(self):
|
||||
"""Test simple policy access view"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
|
||||
class TestView(BufferedPolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/")
|
||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
||||
req.session.save()
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
|
||||
|
||||
@patch_flag(BufferedPolicyAccessViewFlag, True)
|
||||
@apply_blueprint("default/flow-default-authentication-flow.yaml")
|
||||
def test_pav_buffer_skip(self):
|
||||
"""Test simple policy access view (skip buffer)"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
flow = Flow.objects.get(slug="default-authentication-flow")
|
||||
|
||||
class TestView(BufferedPolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/?skip_buffer=true")
|
||||
req.brand = create_test_brand(flow_authentication=flow)
|
||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
||||
req.session.save()
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertTrue(
|
||||
res.url.startswith(reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}))
|
||||
)
|
||||
|
||||
def test_buffer(self):
|
||||
"""Test buffer view"""
|
||||
uid = generate_id()
|
||||
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
|
||||
ts = generate_id()
|
||||
req.session[SESSION_KEY_BUFFER % uid] = {
|
||||
"method": "get",
|
||||
"body": {},
|
||||
"url": f"/{ts}",
|
||||
}
|
||||
req.session.save()
|
||||
|
||||
res = BufferView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn(ts, res.render().content.decode())
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
"""API URLs"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from authentik.policies.api.bindings import PolicyBindingViewSet
|
||||
from authentik.policies.api.policies import PolicyViewSet
|
||||
from authentik.policies.views import BufferView
|
||||
|
||||
urlpatterns = [
|
||||
path("buffer", BufferView.as_view(), name="buffer"),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
("policies/all", PolicyViewSet),
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
"""authentik access helper classes"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.http import Http404, HttpRequest, HttpResponse, QueryDict
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import View
|
||||
from django.views.generic.base import TemplateView, View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.apps import AppAccessWithoutBindings
|
||||
from authentik.core.models import Application, Provider, User
|
||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_APPLICATION,
|
||||
PLAN_CONTEXT_POST,
|
||||
FlowPlan,
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.views.executor import (
|
||||
SESSION_KEY_PLAN,
|
||||
SESSION_KEY_POST,
|
||||
ToDefaultFlow,
|
||||
)
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.policies.apps import BufferedPolicyAccessViewFlag
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
QS_BUFFER_ID = "af_bf_id"
|
||||
QS_SKIP_BUFFER = "skip_buffer"
|
||||
SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s"
|
||||
|
||||
|
||||
class RequestValidationError(SentryIgnoredException):
|
||||
@@ -42,6 +51,12 @@ class RequestValidationError(SentryIgnoredException):
|
||||
self.response = response
|
||||
|
||||
|
||||
class BaseMixin:
|
||||
"""Base Mixin class, used to annotate View Member variables"""
|
||||
|
||||
request: HttpRequest
|
||||
|
||||
|
||||
class PolicyAccessView(AccessMixin, View):
|
||||
"""Mixin class for usage in Authorization views.
|
||||
Provider functions to check application access, etc"""
|
||||
@@ -136,7 +151,6 @@ class PolicyAccessView(AccessMixin, View):
|
||||
policy_engine = PolicyEngine(
|
||||
pbm or self.application, user or self.request.user, self.request
|
||||
)
|
||||
policy_engine.empty_result = AppAccessWithoutBindings.get()
|
||||
policy_engine.use_cache = False
|
||||
policy_engine.request = self.modify_policy_request(policy_engine.request)
|
||||
policy_engine.build()
|
||||
@@ -153,3 +167,66 @@ class PolicyAccessView(AccessMixin, View):
|
||||
for message in result.messages:
|
||||
messages.error(self.request, _(message))
|
||||
return result
|
||||
|
||||
|
||||
def url_with_qs(url: str, **kwargs):
|
||||
"""Update/set querystring of `url` with the parameters in `kwargs`. Original query string
|
||||
parameters are retained"""
|
||||
if "?" not in url:
|
||||
return url + f"?{urlencode(kwargs)}"
|
||||
url, _, qs = url.partition("?")
|
||||
qs = QueryDict(qs, mutable=True)
|
||||
qs.update(kwargs)
|
||||
return url + f"?{urlencode(qs.items())}"
|
||||
|
||||
|
||||
class BufferView(TemplateView):
|
||||
"""Buffer view"""
|
||||
|
||||
template_name = "policies/buffer.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
buf_id = self.request.GET.get(QS_BUFFER_ID)
|
||||
buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id)
|
||||
kwargs["auth_req_method"] = buffer["method"]
|
||||
kwargs["auth_req_body"] = buffer["body"]
|
||||
kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True})
|
||||
kwargs["check_auth_url"] = reverse("authentik_api:user-me")
|
||||
kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id})
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class BufferedPolicyAccessView(PolicyAccessView):
|
||||
"""PolicyAccessView which buffers access requests in case the user is not logged in"""
|
||||
|
||||
def handle_no_permission(self):
|
||||
plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN)
|
||||
if plan:
|
||||
flow = Flow.objects.filter(pk=plan.flow_pk).first()
|
||||
if not flow or flow.designation != FlowDesignation.AUTHENTICATION:
|
||||
LOGGER.debug("Not buffering request, no flow or flow not for authentication")
|
||||
return super().handle_no_permission()
|
||||
if not plan:
|
||||
LOGGER.debug("Not buffering request, no flow plan active")
|
||||
return super().handle_no_permission()
|
||||
if not BufferedPolicyAccessViewFlag.get():
|
||||
return super().handle_no_permission()
|
||||
if self.request.GET.get(QS_SKIP_BUFFER):
|
||||
LOGGER.debug("Not buffering request, explicit skip")
|
||||
return super().handle_no_permission()
|
||||
buffer_id = str(uuid4())
|
||||
LOGGER.debug("Buffering access request", bf_id=buffer_id)
|
||||
self.request.session[SESSION_KEY_BUFFER % buffer_id] = {
|
||||
"body": self.request.POST,
|
||||
"url": self.request.build_absolute_uri(self.request.get_full_path()),
|
||||
"method": self.request.method.lower(),
|
||||
}
|
||||
return redirect(
|
||||
url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id})
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
if QS_BUFFER_ID in self.request.GET:
|
||||
self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None)
|
||||
return response
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
"""Shared logout stages for SAML and OIDC providers"""
|
||||
|
||||
from django.http import HttpResponse
|
||||
from rest_framework.fields import CharField, ListField
|
||||
from rest_framework.fields import CharField, DictField, ListField
|
||||
|
||||
from authentik.common.oauth.constants import PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS
|
||||
|
||||
|
||||
class LogoutURL(PassiveSerializer):
|
||||
"""Data for a single logout URL"""
|
||||
|
||||
url = CharField()
|
||||
provider_name = CharField(required=False, allow_null=True)
|
||||
binding = CharField(required=False, allow_null=True)
|
||||
saml_request = CharField(required=False, allow_null=True)
|
||||
saml_response = CharField(required=False, allow_null=True)
|
||||
saml_relay_state = CharField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class IframeLogoutChallenge(Challenge):
|
||||
"""Challenge for iframe logout"""
|
||||
|
||||
component = CharField(default="ak-provider-iframe-logout")
|
||||
logout_urls = ListField(child=LogoutURL(), default=list)
|
||||
logout_urls = ListField(child=DictField(), default=list)
|
||||
|
||||
|
||||
class IframeLogoutChallengeResponse(ChallengeResponse):
|
||||
|
||||
@@ -17,7 +17,6 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.apps import AppAccessWithoutBindings
|
||||
from authentik.core.models import Application
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
@@ -154,7 +153,6 @@ class LDAPOutpostConfigViewSet(ListModelMixin, GenericViewSet):
|
||||
provider = get_object_or_404(LDAPProvider, pk=pk)
|
||||
application = get_object_or_404(Application, slug=request.query_params["app_slug"])
|
||||
engine = PolicyEngine(application, request.user, request)
|
||||
engine.empty_result = AppAccessWithoutBindings.get()
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
result = engine.result
|
||||
|
||||
@@ -15,7 +15,7 @@ from authentik.common.oauth.constants import (
|
||||
SCOPE_OPENID_PROFILE,
|
||||
TOKEN_TYPE,
|
||||
)
|
||||
from authentik.core.models import USERNAME_MAX_LENGTH, Application, Group, User
|
||||
from authentik.core.models import Application, Group
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.models import PolicyBinding
|
||||
@@ -27,7 +27,7 @@ from authentik.providers.oauth2.models import (
|
||||
)
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
from authentik.providers.oauth2.views.jwks import JWKSView
|
||||
from authentik.sources.oauth.models import OAuthSource, OAuthSourcePropertyMapping
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
|
||||
class TestTokenClientCredentialsJWTSource(OAuthTestCase):
|
||||
@@ -220,10 +220,6 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
user = User.objects.filter(username=f"{self.provider.name}-foo").first()
|
||||
self.assertIsNotNone(user)
|
||||
|
||||
body = loads(response.content.decode())
|
||||
self.assertEqual(body["token_type"], TOKEN_TYPE)
|
||||
_, alg = self.provider.jwt_key
|
||||
@@ -237,54 +233,3 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase):
|
||||
jwt["given_name"], "Autogenerated user from application test (client credentials JWT)"
|
||||
)
|
||||
self.assertEqual(jwt["preferred_username"], "test-foo")
|
||||
|
||||
def test_successful_mapping(self):
|
||||
"""test successful"""
|
||||
test_username = ("mapped-foo" + ("a" * 150))[:USERNAME_MAX_LENGTH]
|
||||
mapping = OAuthSourcePropertyMapping.objects.create(
|
||||
name="test-mapping",
|
||||
expression="""return {
|
||||
"email": oauth_userinfo.get("email"),
|
||||
"name": oauth_userinfo.get("name"),
|
||||
"username": oauth_userinfo.get("username"),
|
||||
}""",
|
||||
)
|
||||
self.source.user_property_mappings.add(mapping)
|
||||
|
||||
token = self.helper_provider.encode(
|
||||
{
|
||||
"sub": "foo",
|
||||
"email": "test-user@example.com",
|
||||
"name": "Mapped Test User",
|
||||
"username": "mapped-foo" + ("a" * 150),
|
||||
"exp": datetime.now() + timedelta(hours=2),
|
||||
}
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
{
|
||||
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
|
||||
"scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
|
||||
"client_id": self.provider.client_id,
|
||||
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
||||
"client_assertion": token,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
user = User.objects.filter(username=test_username).first()
|
||||
self.assertIsNotNone(user)
|
||||
|
||||
body = loads(response.content.decode())
|
||||
self.assertEqual(body["token_type"], TOKEN_TYPE)
|
||||
key_obj, alg = self.provider.jwt_key
|
||||
jwt = decode(
|
||||
body["access_token"],
|
||||
key=key_obj.public_key(),
|
||||
algorithms=[alg],
|
||||
audience=self.provider.client_id,
|
||||
)
|
||||
|
||||
self.assertEqual(jwt["email"], "test-user@example.com")
|
||||
self.assertEqual(jwt["given_name"], "Mapped Test User")
|
||||
self.assertEqual(jwt["preferred_username"], test_username)
|
||||
|
||||
@@ -45,7 +45,7 @@ from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageVie
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.types import PolicyRequest
|
||||
from authentik.policies.views import PolicyAccessView, RequestValidationError
|
||||
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError
|
||||
from authentik.providers.oauth2.errors import (
|
||||
AuthorizeError,
|
||||
ClientIdError,
|
||||
@@ -338,7 +338,7 @@ class OAuthAuthorizationParams:
|
||||
return code
|
||||
|
||||
|
||||
class AuthorizationFlowInitView(PolicyAccessView):
|
||||
class AuthorizationFlowInitView(BufferedPolicyAccessView):
|
||||
"""OAuth2 Flow initializer, checks access to application and starts flow"""
|
||||
|
||||
params: OAuthAuthorizationParams
|
||||
@@ -432,7 +432,7 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||
return response
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args, **kwargs):
|
||||
# Activate language before parsing params (error messages should be localized)
|
||||
# Activate language before parsing params (error messages should be localised)
|
||||
return self.dispatch_with_language(request, *args, **kwargs)
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
|
||||
@@ -33,7 +33,6 @@ from authentik.common.oauth.constants import (
|
||||
SCOPE_OFFLINE_ACCESS,
|
||||
TOKEN_TYPE,
|
||||
)
|
||||
from authentik.core.apps import AppAccessWithoutBindings
|
||||
from authentik.core.middleware import CTX_AUTH_VIA
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_EXPIRES,
|
||||
@@ -46,7 +45,6 @@ from authentik.core.models import (
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.core.sources.mapper import SourceMapper
|
||||
from authentik.events.middleware import audit_ignore
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.signals import get_login_event
|
||||
@@ -148,7 +146,6 @@ class TokenParams:
|
||||
):
|
||||
user = self.user if self.user else get_anonymous_user()
|
||||
engine = PolicyEngine(app, user, request)
|
||||
engine.empty_result = AppAccessWithoutBindings.get()
|
||||
# Don't cache as for client_credentials flows the user will not be set
|
||||
# so we'll get generic cache results
|
||||
engine.use_cache = False
|
||||
@@ -371,7 +368,7 @@ class TokenParams:
|
||||
) -> tuple[dict, OAuthSource] | tuple[None, None]:
|
||||
# Fully decode the JWT without verifying the signature, so we can get access to
|
||||
# the header.
|
||||
# Get the Key ID from the header, and use that to optimize our source query to only find
|
||||
# Get the Key ID from the header, and use that to optimise our source query to only find
|
||||
# sources that have a JWK for that Key ID
|
||||
# The Key ID doesn't have a fixed format, but must match between an issued JWT
|
||||
# and whatever is returned by the JWKS endpoint
|
||||
@@ -479,7 +476,7 @@ class TokenParams:
|
||||
|
||||
self.__check_policy_access(app, request, oauth_jwt=token)
|
||||
if not provider:
|
||||
self.__create_user_from_jwt(token, app, source, request)
|
||||
self.__create_user_from_jwt(token, app, source)
|
||||
|
||||
method_args = {
|
||||
"jwt": token,
|
||||
@@ -533,30 +530,18 @@ class TokenParams:
|
||||
raise TokenError("invalid_grant")
|
||||
self.device_code = code
|
||||
|
||||
def __create_user_from_jwt(
|
||||
self, token: dict[str, Any], app: Application, source: OAuthSource, request: HttpRequest
|
||||
):
|
||||
def __create_user_from_jwt(self, token: dict[str, Any], app: Application, source: OAuthSource):
|
||||
"""Create user from JWT"""
|
||||
with audit_ignore():
|
||||
# Run the JWT payload through the core mapping engine
|
||||
mapped = SourceMapper(source).build_object_properties(
|
||||
User, request=request, info=token, oauth_userinfo=token
|
||||
)
|
||||
|
||||
self.user, created = User.objects.update_or_create(
|
||||
username=mapped.get("username", f"{self.provider.name}-{token.get('sub')}")[
|
||||
:USERNAME_MAX_LENGTH
|
||||
],
|
||||
username=f"{self.provider.name}-{token.get('sub')}",
|
||||
defaults={
|
||||
"last_login": timezone.now(),
|
||||
"name": mapped.get(
|
||||
"name",
|
||||
f"Autogenerated user from application {app.name} (client credentials JWT)",
|
||||
"name": (
|
||||
f"Autogenerated user from application {app.name} (client credentials JWT)"
|
||||
),
|
||||
"email": mapped.get("email", ""),
|
||||
"path": source.get_user_path(),
|
||||
"type": UserTypes.SERVICE_ACCOUNT,
|
||||
"attributes": mapped.get("attributes", {}),
|
||||
},
|
||||
)
|
||||
self.user.attributes[USER_ATTRIBUTE_GENERATED] = True
|
||||
|
||||
@@ -27,6 +27,8 @@ class TraefikMiddlewareSpecForwardAuth:
|
||||
|
||||
trustForwardHeader: bool = field(default=True)
|
||||
|
||||
maxResponseBodySize: int = field(default=1024 * 1024 * 4)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TraefikMiddlewareSpec:
|
||||
@@ -140,6 +142,7 @@ class Traefik3MiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
|
||||
],
|
||||
authResponseHeadersRegex="",
|
||||
trustForwardHeader=True,
|
||||
maxResponseBodySize=1024 * 1024 * 4,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user