mirror of
https://github.com/goauthentik/authentik
synced 2026-05-06 07:02:51 +02:00
Compare commits
12 Commits
next
...
enforce-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc794d927e | ||
|
|
01b91e9dd1 | ||
|
|
6efb3f5fb0 | ||
|
|
4b24a7ddc1 | ||
|
|
b6b423bee9 | ||
|
|
860a43e56a | ||
|
|
c1cbd0623c | ||
|
|
0893031cab | ||
|
|
89868276da | ||
|
|
dd40f60c88 | ||
|
|
73be48c179 | ||
|
|
fc27dfeeb7 |
@@ -1,5 +0,0 @@
|
||||
[alias]
|
||||
t = ["nextest", "run", "--workspace"]
|
||||
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
@@ -1,47 +0,0 @@
|
||||
[licenses]
|
||||
allow = [
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"Apache-2.0",
|
||||
"BSD-3-Clause",
|
||||
"CC0-1.0",
|
||||
"CDLA-Permissive-2.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[licenses.private]
|
||||
ignore = true
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
wildcards = "deny"
|
||||
[bans.workspace-dependencies]
|
||||
duplicates = "deny"
|
||||
include-path-dependencies = true
|
||||
unused = "deny"
|
||||
|
||||
# No non-FIPS compliant dependencies
|
||||
[[bans.deny]]
|
||||
name = "native-tls"
|
||||
[[bans.deny]]
|
||||
name = "openssl"
|
||||
[[bans.deny]]
|
||||
name = "openssl-sys"
|
||||
[[bans.deny]]
|
||||
name = "ring"
|
||||
[[bans.features]]
|
||||
allow = [
|
||||
"alloc",
|
||||
"aws-lc-sys",
|
||||
"default",
|
||||
"fips",
|
||||
"prebuilt-nasm",
|
||||
"ring-io",
|
||||
"ring-sig-verify",
|
||||
]
|
||||
name = "aws-lc-rs"
|
||||
exact = true
|
||||
@@ -1,15 +0,0 @@
|
||||
comment_width = 100
|
||||
format_code_in_doc_comments = true
|
||||
format_strings = true
|
||||
group_imports = "StdExternalCrate"
|
||||
hex_literal_case = "Lower"
|
||||
imports_granularity = "Crate"
|
||||
max_width = 100
|
||||
newline_style = "Unix"
|
||||
normalize_comments = true
|
||||
normalize_doc_attributes = true
|
||||
reorder_impl_items = true
|
||||
style_edition = "2024"
|
||||
use_field_init_shorthand = true
|
||||
use_try_shorthand = true
|
||||
wrap_comments = true
|
||||
@@ -9,5 +9,7 @@ build_docs/**
|
||||
**/*Dockerfile
|
||||
blueprints/local
|
||||
.git
|
||||
!gen-ts-api/node_modules
|
||||
!gen-ts-api/dist/**
|
||||
!gen-go-api/
|
||||
.venv
|
||||
target
|
||||
|
||||
9
.gitattributes
vendored
9
.gitattributes
vendored
@@ -1,9 +0,0 @@
|
||||
packages/client-*/** linguist-generated
|
||||
web/packages/lex/* linguist-vendored
|
||||
web/packages/node-domexception/* linguist-vendored
|
||||
web/packages/formdata-polyfill/* linguist-vendored
|
||||
web/packages/sfe/vendored/* linguist-vendored
|
||||
website/vendored/* linguist-vendored
|
||||
website/docs/** linguist-documentation
|
||||
website/integrations/** linguist-documentation
|
||||
website/api/** linguist-documentation
|
||||
29
.github/actions/cherry-pick/action.yml
vendored
29
.github/actions/cherry-pick/action.yml
vendored
@@ -115,13 +115,20 @@ runs:
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.token }}
|
||||
PR_NUMBER: ${{ steps.should_run.outputs.pr_number }}
|
||||
REASON: ${{ steps.should_run.outputs.reason }}
|
||||
run: |
|
||||
set -e -o pipefail
|
||||
PR_NUMBER="${{ steps.should_run.outputs.pr_number }}"
|
||||
|
||||
# Get PR details
|
||||
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER)
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.user.login')
|
||||
|
||||
echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT
|
||||
echo "pr_author=$PR_AUTHOR" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine which labels to process
|
||||
if [ "${REASON}" = "label_added_to_merged_pr" ]; then
|
||||
if [ "${{ steps.should_run.outputs.reason }}" = "label_added_to_merged_pr" ]; then
|
||||
# Only process the specific label that was just added
|
||||
if [ "${{ github.event_name }}" = "issues" ]; then
|
||||
LABEL_NAME="${{ github.event.label.name }}"
|
||||
@@ -145,13 +152,13 @@ runs:
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.token }}
|
||||
PR_NUMBER: '${{ steps.should_run.outputs.pr_number }}'
|
||||
COMMIT_SHA: '${{ steps.should_run.outputs.merge_commit_sha }}'
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
LABELS: '${{ steps.pr_details.outputs.labels }}'
|
||||
run: |
|
||||
set -e -o pipefail
|
||||
PR_NUMBER='${{ steps.should_run.outputs.pr_number }}'
|
||||
COMMIT_SHA='${{ steps.should_run.outputs.merge_commit_sha }}'
|
||||
PR_TITLE='${{ steps.pr_details.outputs.pr_title }}'
|
||||
PR_AUTHOR='${{ steps.pr_details.outputs.pr_author }}'
|
||||
LABELS='${{ steps.pr_details.outputs.labels }}'
|
||||
|
||||
echo "Processing PR #$PR_NUMBER (reason: ${{ steps.should_run.outputs.reason }})"
|
||||
echo "Found backport labels: $LABELS"
|
||||
@@ -208,9 +215,6 @@ runs:
|
||||
--head "$CHERRY_PICK_BRANCH" \
|
||||
--label "cherry-pick")
|
||||
|
||||
# Assign the PR to the original author
|
||||
gh pr edit "$NEW_PR" --add-assignee "$PR_AUTHOR" || true
|
||||
|
||||
echo "✅ Created cherry-pick PR $NEW_PR for $TARGET_BRANCH"
|
||||
|
||||
# Comment on original PR
|
||||
@@ -250,9 +254,6 @@ runs:
|
||||
--head "$CHERRY_PICK_BRANCH" \
|
||||
--label "cherry-pick")
|
||||
|
||||
# Assign the PR to the original author
|
||||
gh pr edit "$NEW_PR" --add-assignee "$PR_AUTHOR" || true
|
||||
|
||||
echo "⚠️ Created conflict resolution PR $NEW_PR for $TARGET_BRANCH"
|
||||
|
||||
# Comment on original PR
|
||||
|
||||
@@ -54,6 +54,10 @@ outputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: "python"
|
||||
- name: Generate config
|
||||
id: ev
|
||||
shell: bash
|
||||
@@ -64,4 +68,4 @@ runs:
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
python3 ${{ github.action_path }}/push_vars.py
|
||||
uv run python3 ${{ github.action_path }}/push_vars.py
|
||||
|
||||
@@ -2,19 +2,10 @@
|
||||
|
||||
import os
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
from sys import exit as sysexit
|
||||
from time import time
|
||||
from typing import Any
|
||||
|
||||
|
||||
def authentik_version() -> str:
|
||||
init = Path(__file__).parent.parent.parent.parent / "authentik" / "__init__.py"
|
||||
with open(init) as f:
|
||||
content = f.read()
|
||||
locals: dict[str, Any] = {}
|
||||
exec(content, None, locals) # nosec
|
||||
return str(locals["VERSION"])
|
||||
from authentik import authentik_version
|
||||
|
||||
|
||||
def must_or_fail(input: str | None, error: str) -> str:
|
||||
@@ -106,7 +97,6 @@ if os.getenv("RELEASE", "false").lower() == "true":
|
||||
image_build_args = [f"VERSION={os.getenv('REF')}"]
|
||||
else:
|
||||
image_build_args = [f"GIT_BUILD_HASH={sha}"]
|
||||
image_build_args_str = "\n".join(image_build_args)
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||
print(f"shouldPush={str(should_push).lower()}", file=_output)
|
||||
@@ -119,4 +109,4 @@ with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||
print(f"imageMainTag={image_main_tag}", file=_output)
|
||||
print(f"imageMainName={image_tags[0]}", file=_output)
|
||||
print(f"cacheTo={cache_to}", file=_output)
|
||||
print(f"imageBuildArgs={image_build_args_str}", file=_output)
|
||||
print(f"imageBuildArgs={"\n".join(image_build_args)}", file=_output)
|
||||
|
||||
83
.github/actions/setup/action.yml
vendored
83
.github/actions/setup/action.yml
vendored
@@ -4,112 +4,63 @@ 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"
|
||||
steps:
|
||||
- name: Cleanup apt
|
||||
if: ${{ contains(inputs.dependencies, 'system') || contains(inputs.dependencies, 'python') }}
|
||||
shell: bash
|
||||
run: sudo apt-get remove --purge man-db
|
||||
- name: Install apt deps
|
||||
if: ${{ contains(inputs.dependencies, 'system') || contains(inputs.dependencies, 'python') }}
|
||||
uses: gerlero/apt-install@f4fa5265092af9e750549565d28c99aec7189639
|
||||
with:
|
||||
packages: libpq-dev openssl libxmlsec1-dev pkg-config gettext krb5-multidev libkrb5-dev heimdal-multidev libclang-dev krb5-kdc krb5-user krb5-admin-server
|
||||
update: true
|
||||
upgrade: false
|
||||
install-recommends: false
|
||||
- name: Make space on disk
|
||||
if: ${{ contains(inputs.dependencies, 'system') || contains(inputs.dependencies, 'python') }}
|
||||
shell: bash
|
||||
run: |
|
||||
sudo mkdir -p /tmp/empty/
|
||||
sudo rsync -a --delete /tmp/empty/ /usr/local/lib/android/
|
||||
sudo apt-get remove --purge man-db
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
|
||||
- name: Install uv
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v5
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v5
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Setup python
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v5
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
|
||||
with:
|
||||
python-version-file: "${{ inputs.working-directory }}pyproject.toml"
|
||||
python-version-file: "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@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1
|
||||
with:
|
||||
rustflags: ""
|
||||
- name: Setup rust (nightly)
|
||||
if: ${{ contains(inputs.dependencies, 'rust-nightly') }}
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v4
|
||||
with:
|
||||
node-version-file: "${{ inputs.working-directory }}web/package.json"
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: "${{ inputs.working-directory }}web/package-lock.json"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Setup node (root)
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4
|
||||
with:
|
||||
node-version-file: "${{ inputs.working-directory }}package.json"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "${{ inputs.working-directory }}package-lock.json"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Install Node deps
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: npm ci
|
||||
cache-dependency-path: web/package-lock.json
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Setup go
|
||||
if: ${{ contains(inputs.dependencies, 'go') }}
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v5
|
||||
with:
|
||||
go-version-file: "${{ inputs.working-directory }}go.mod"
|
||||
go-version-file: "go.mod"
|
||||
- name: Setup docker cache
|
||||
if: ${{ contains(inputs.dependencies, 'runtime') }}
|
||||
uses: AndreKurait/docker-cache@0fe76702a40db986d9663c24954fc14c6a6031b7
|
||||
with:
|
||||
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
|
||||
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
|
||||
- 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 --wait
|
||||
cd web && npm ci
|
||||
docker compose -f .github/actions/setup/docker-compose.yml up -d
|
||||
cd web && npm i
|
||||
- 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
|
||||
|
||||
@@ -2,20 +2,19 @@ services:
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:${PSQL_TAG:-16}
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql
|
||||
- db-data:/var/lib/postgresql/data
|
||||
command: "-c log_statement=all"
|
||||
environment:
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
POSTGRES_DB: authentik
|
||||
PGDATA: /var/lib/postgresql/data/pgdata
|
||||
ports:
|
||||
- 5432:5432
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB} -h 127.0.0.1"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 60
|
||||
restart: always
|
||||
redis:
|
||||
image: docker.io/library/redis:7
|
||||
ports:
|
||||
- 6379:6379
|
||||
restart: always
|
||||
s3:
|
||||
container_name: s3
|
||||
@@ -28,7 +27,7 @@ services:
|
||||
- 8020:8000
|
||||
volumes:
|
||||
- s3-data:/usr/src/app/localData
|
||||
- s3-metadata:/usr/src/app/localMetadata
|
||||
- s3-metadata:/usr/scr/app/localMetadata
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
12
.github/actions/test-results/action.yml
vendored
12
.github/actions/test-results/action.yml
vendored
@@ -2,29 +2,25 @@ name: "Process test results"
|
||||
description: Convert test results to JUnit, add them to GitHub Actions and codecov
|
||||
|
||||
inputs:
|
||||
files:
|
||||
description: Comma-separated explicit list of files to upload
|
||||
flags:
|
||||
description: Codecov flags
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
||||
with:
|
||||
files: ${{ inputs.files }}
|
||||
flags: ${{ inputs.flags }}
|
||||
use_oidc: true
|
||||
- uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1
|
||||
with:
|
||||
files: ${{ inputs.files }}
|
||||
flags: ${{ inputs.flags }}
|
||||
file: unittest.xml
|
||||
use_oidc: true
|
||||
report_type: test_results
|
||||
- name: PostgreSQL Logs
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ $RUNNER_DEBUG == '1' ]]; then
|
||||
if [[ $ACTIONS_RUNNER_DEBUG == 'true' || $ACTIONS_STEP_DEBUG == 'true' ]]; then
|
||||
docker stop setup-postgresql-1
|
||||
echo "::group::PostgreSQL Logs"
|
||||
docker logs setup-postgresql-1
|
||||
|
||||
3
.github/codecov.yml
vendored
3
.github/codecov.yml
vendored
@@ -8,6 +8,3 @@ coverage:
|
||||
threshold: 1%
|
||||
comment:
|
||||
after_n_builds: 3
|
||||
ignore:
|
||||
- packages/client-rust
|
||||
- packages/client-ts
|
||||
|
||||
1
.github/codespell-dictionary.txt
vendored
Normal file
1
.github/codespell-dictionary.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
authentic->authentik
|
||||
32
.github/codespell-words.txt
vendored
Normal file
32
.github/codespell-words.txt
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
akadmin
|
||||
asgi
|
||||
assertIn
|
||||
authentik
|
||||
authn
|
||||
crate
|
||||
docstrings
|
||||
entra
|
||||
goauthentik
|
||||
gunicorn
|
||||
hass
|
||||
jwe
|
||||
jwks
|
||||
keypair
|
||||
keypairs
|
||||
kubernetes
|
||||
oidc
|
||||
ontext
|
||||
openid
|
||||
passwordless
|
||||
plex
|
||||
saml
|
||||
scim
|
||||
singed
|
||||
slo
|
||||
sso
|
||||
totp
|
||||
traefik
|
||||
# https://github.com/codespell-project/codespell/issues/1224
|
||||
upToDate
|
||||
warmup
|
||||
webauthn
|
||||
90
.github/dependabot.yml
vendored
90
.github/dependabot.yml
vendored
@@ -20,8 +20,6 @@ updates:
|
||||
prefix: "ci:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 3
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -37,56 +35,6 @@ updates:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
exclude:
|
||||
- "golang.org/x/crypto"
|
||||
- "golang.org/x/net"
|
||||
- "github.com/golang-jwt/jwt/*"
|
||||
- "github.com/coreos/go-oidc/*"
|
||||
- "github.com/go-ldap/ldap/*"
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rust
|
||||
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
exclude:
|
||||
- aws-lc-fips-sys
|
||||
- aws-lc-rs
|
||||
- aws-lc-sys
|
||||
- rustls
|
||||
- rustls-pki-types
|
||||
- rustls-platform-verifier
|
||||
- rustls-webpki
|
||||
|
||||
- package-ecosystem: rust-toolchain
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 3
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -105,10 +53,6 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "web:"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
groups:
|
||||
sentry:
|
||||
patterns:
|
||||
@@ -172,10 +116,6 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "core, web:"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
groups:
|
||||
sentry:
|
||||
patterns:
|
||||
@@ -234,10 +174,6 @@ updates:
|
||||
prefix: "website:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
groups:
|
||||
docusaurus:
|
||||
patterns:
|
||||
@@ -276,10 +212,6 @@ updates:
|
||||
prefix: "lifecycle/aws:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -295,18 +227,6 @@ updates:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
semver-patch-days: 3
|
||||
exclude:
|
||||
- "django"
|
||||
- "cryptography"
|
||||
- "pyjwt"
|
||||
- "xmlsec"
|
||||
- "lxml"
|
||||
- "psycopg"
|
||||
- "pyopenssl"
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -314,7 +234,7 @@ updates:
|
||||
|
||||
- package-ecosystem: docker
|
||||
directories:
|
||||
- /lifecycle/container
|
||||
- /
|
||||
- /website
|
||||
schedule:
|
||||
interval: daily
|
||||
@@ -324,14 +244,10 @@ updates:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 3
|
||||
- package-ecosystem: docker-compose
|
||||
directories:
|
||||
- /packages/client-go
|
||||
- /packages/client-rust
|
||||
- /packages/client-ts
|
||||
# - /scripts # Maybe
|
||||
- /scripts/api
|
||||
- /tests/e2e
|
||||
schedule:
|
||||
interval: daily
|
||||
@@ -341,7 +257,5 @@ updates:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
cooldown:
|
||||
default-days: 3
|
||||
|
||||
#endregion
|
||||
|
||||
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -2,10 +2,6 @@
|
||||
👋 Hi there! Welcome.
|
||||
|
||||
Please check the Contributing guidelines: https://docs.goauthentik.io/docs/developer-docs/#how-can-i-contribute
|
||||
|
||||
⚠️ IMPORTANT: Make sure you are opening this PR from a FEATURE BRANCH, not from your main branch!
|
||||
If you opened this PR from your main branch, please close it and create a new feature branch instead.
|
||||
For more information, see: https://docs.goauthentik.io/developer-docs/contributing/#always-use-feature-branches
|
||||
-->
|
||||
|
||||
## Details
|
||||
@@ -26,7 +22,7 @@ REPLACE ME
|
||||
|
||||
If an API change has been made
|
||||
|
||||
- [ ] The API schema and clients have been updated (`make gen`)
|
||||
- [ ] The API schema has been updated (`make gen-build`)
|
||||
|
||||
If changes to the frontend have been made
|
||||
|
||||
|
||||
@@ -42,9 +42,9 @@ jobs:
|
||||
# Needed for checkout
|
||||
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: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -56,23 +56,37 @@ jobs:
|
||||
release: ${{ inputs.release }}
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.registry_ghcr }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: make empty clients
|
||||
if: ${{ inputs.release }}
|
||||
run: |
|
||||
mkdir -p ./gen-ts-api
|
||||
mkdir -p ./gen-go-api
|
||||
- name: Setup node
|
||||
if: ${{ !inputs.release }}
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: generate ts client
|
||||
if: ${{ !inputs.release }}
|
||||
run: make gen-client-ts
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
id: push
|
||||
with:
|
||||
context: .
|
||||
file: lifecycle/container/Dockerfile
|
||||
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
secrets: |
|
||||
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
||||
@@ -83,7 +97,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@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
with:
|
||||
|
||||
12
.github/workflows/_reusable-docker-build.yml
vendored
12
.github/workflows/_reusable-docker-build.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
tags: ${{ steps.ev.outputs.imageTagsJSON }}
|
||||
shouldPush: ${{ steps.ev.outputs.shouldPush }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
matrix:
|
||||
tag: ${{ fromJson(needs.get-tags.outputs.tags) }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -79,25 +79,25 @@ jobs:
|
||||
image-name: ${{ inputs.image_name }}
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.registry_ghcr }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: int128/docker-manifest-create-action@fa55f72001a6c74b0f4997dca65c70d334905180 # v2
|
||||
- uses: int128/docker-manifest-create-action@b60433fd4312d7a64a56d769b76ebe3f45cf36b4 # 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@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
|
||||
66
.github/workflows/api-ts-publish.yml
vendored
Normal file
66
.github/workflows/api-ts-publish.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: API - Publish Typescript client
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "schema.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
# Required for NPM OIDC trusted publisher
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Generate API Client
|
||||
run: make gen-client-ts
|
||||
- name: Publish package
|
||||
working-directory: gen-ts-api/
|
||||
run: |
|
||||
npm i
|
||||
npm publish --tag generated
|
||||
- name: Upgrade /web
|
||||
working-directory: web
|
||||
run: |
|
||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- name: Upgrade /web/packages/sfe
|
||||
working-directory: web/packages/sfe
|
||||
run: |
|
||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: update-web-api-client
|
||||
commit-message: "web: bump API Client version"
|
||||
title: "web: bump API Client version"
|
||||
body: "web: bump API Client version"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
# ID from https://api.github.com/users/authentik-automation[bot]
|
||||
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
|
||||
labels: dependencies
|
||||
- uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
16
.github/workflows/ci-api-docs.yml
vendored
16
.github/workflows/ci-api-docs.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
command:
|
||||
- prettier-check
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Install Dependencies
|
||||
working-directory: website/
|
||||
run: npm ci
|
||||
@@ -32,8 +32,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
- working-directory: website/
|
||||
name: Install Dependencies
|
||||
run: npm ci
|
||||
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
|
||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/website/api/.docusaurus
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: npm run build -w api
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v4
|
||||
with:
|
||||
name: api-docs
|
||||
path: website/api/build
|
||||
@@ -66,12 +66,12 @@ jobs:
|
||||
- lint
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v5
|
||||
with:
|
||||
name: api-docs
|
||||
path: website/api/build
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
|
||||
4
.github/workflows/ci-aws-cfn.yml
vendored
4
.github/workflows/ci-aws-cfn.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
check-changes-applied:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: lifecycle/aws/package.json
|
||||
cache: "npm"
|
||||
|
||||
2
.github/workflows/ci-docs-source.yml
vendored
2
.github/workflows/ci-docs-source.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: generate docs
|
||||
|
||||
30
.github/workflows/ci-docs.yml
vendored
30
.github/workflows/ci-docs.yml
vendored
@@ -15,15 +15,13 @@ on:
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_ENV: production
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
command:
|
||||
- prettier-check
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Install dependencies
|
||||
working-directory: website/
|
||||
run: npm ci
|
||||
@@ -32,11 +30,10 @@ jobs:
|
||||
run: npm run ${{ matrix.command }}
|
||||
build-docs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
@@ -49,11 +46,10 @@ jobs:
|
||||
run: npm run build
|
||||
build-integrations:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
@@ -73,13 +69,13 @@ jobs:
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
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@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -89,14 +85,14 @@ jobs:
|
||||
image-name: ghcr.io/goauthentik/dev-docs
|
||||
- name: Login to Container Registry
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: website/Dockerfile
|
||||
@@ -105,7 +101,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@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
with:
|
||||
|
||||
24
.github/workflows/ci-main-daily.yml
vendored
24
.github/workflows/ci-main-daily.yml
vendored
@@ -6,10 +6,6 @@ on:
|
||||
schedule:
|
||||
# Every night at 3am
|
||||
- cron: "0 3 * * *"
|
||||
pull_request:
|
||||
paths:
|
||||
# Needs to refer to itself
|
||||
- .github/workflows/ci-main-daily.yml
|
||||
|
||||
jobs:
|
||||
test-container:
|
||||
@@ -19,20 +15,14 @@ jobs:
|
||||
matrix:
|
||||
version:
|
||||
- docs
|
||||
- version-2025-12
|
||||
- version-2026-2
|
||||
- version-2025-4
|
||||
- version-2025-2
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- run: |
|
||||
set -euo pipefail
|
||||
current="$(pwd)"
|
||||
dir="/tmp/authentik/${{ matrix.version }}"
|
||||
# 2025.12 still serves the legacy docker-compose filename; newer sites use compose.yml.
|
||||
compose_path="compose.yml"
|
||||
if [ "${{ matrix.version }}" = "version-2025-12" ]; then
|
||||
compose_path="docker-compose.yml"
|
||||
fi
|
||||
mkdir -p "${dir}/lifecycle/container"
|
||||
cd "${dir}"
|
||||
wget "https://${{ matrix.version }}.goauthentik.io/${compose_path}" -O "${dir}/lifecycle/container/compose.yml"
|
||||
"${current}/scripts/test_docker.sh"
|
||||
mkdir -p $dir
|
||||
cd $dir
|
||||
wget https://${{ matrix.version }}.goauthentik.io/docker-compose.yml
|
||||
${current}/scripts/test_docker.sh
|
||||
|
||||
183
.github/workflows/ci-main.yml
vendored
183
.github/workflows/ci-main.yml
vendored
@@ -28,56 +28,24 @@ 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
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: ${{ matrix.deps }}
|
||||
- name: run job
|
||||
run: make ci-lint-${{ matrix.job }}
|
||||
test-gen:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: "system,python,go,node,runtime,rust-nightly"
|
||||
- name: generate schema
|
||||
run: make migrate gen-build
|
||||
- name: generate API clients
|
||||
run: make gen-clients
|
||||
- name: ensure schema is up-to-date
|
||||
run: git diff --exit-code -- schema.yml blueprints/schema.json packages/client-go packages/client-rust packages/client-ts
|
||||
run: uv run make ci-${{ matrix.job }}
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run migrations
|
||||
@@ -103,7 +71,7 @@ jobs:
|
||||
- 18-alpine
|
||||
run_id: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: checkout stable
|
||||
@@ -116,7 +84,7 @@ jobs:
|
||||
# Current version family based on
|
||||
current_version_family=$(cat internal/constants/VERSION | grep -vE -- 'rc[0-9]+$' || true)
|
||||
if [[ -n $current_version_family ]]; then
|
||||
prev_stable="version/${current_version_family}"
|
||||
prev_stable=$current_version_family
|
||||
fi
|
||||
echo "::notice::Checking out ${prev_stable} as stable version..."
|
||||
git checkout ${prev_stable}
|
||||
@@ -127,10 +95,7 @@ jobs:
|
||||
with:
|
||||
postgresql_version: ${{ matrix.psql }}
|
||||
- name: run migrations to stable
|
||||
run: |
|
||||
docker ps
|
||||
docker logs setup-postgresql-1
|
||||
uv run python -m lifecycle.migrate
|
||||
run: uv run python -m lifecycle.migrate
|
||||
- name: checkout current code
|
||||
run: |
|
||||
set -x
|
||||
@@ -171,7 +136,7 @@ jobs:
|
||||
- 18-alpine
|
||||
run_id: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -191,15 +156,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Create k8s Kind Cluster
|
||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0
|
||||
- name: run integration
|
||||
run: |
|
||||
uv run coverage run manage.py test tests/integration
|
||||
uv run coverage combine
|
||||
uv run coverage xml
|
||||
- uses: ./.github/actions/test-results
|
||||
if: ${{ always() }}
|
||||
@@ -215,151 +179,52 @@ jobs:
|
||||
job:
|
||||
- name: proxy
|
||||
glob: tests/e2e/test_provider_proxy*
|
||||
profiles: selenium
|
||||
- name: oauth
|
||||
glob: tests/e2e/test_provider_oauth2* tests/e2e/test_source_oauth*
|
||||
profiles: selenium
|
||||
- name: oauth-oidc
|
||||
glob: tests/e2e/test_provider_oidc*
|
||||
profiles: selenium
|
||||
- name: saml
|
||||
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
||||
profiles: selenium
|
||||
- name: ldap
|
||||
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
||||
- name: rac
|
||||
glob: tests/e2e/test_provider_rac*
|
||||
profiles: selenium
|
||||
- name: ws-fed
|
||||
glob: tests/e2e/test_provider_ws_fed*
|
||||
profiles: selenium
|
||||
- name: radius
|
||||
glob: tests/e2e/test_provider_radius*
|
||||
- name: scim
|
||||
glob: tests/e2e/test_source_scim*
|
||||
- name: flows
|
||||
glob: tests/e2e/test_flows*
|
||||
profiles: selenium
|
||||
- name: endpoints
|
||||
glob: tests/e2e/test_endpoints_*
|
||||
profiles: selenium
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup e2e env
|
||||
env:
|
||||
COMPOSE_PROFILES: ${{ matrix.job.profiles }}
|
||||
run: |
|
||||
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
|
||||
- id: cache-web
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
|
||||
if: contains(matrix.job.profiles, 'selenium')
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
- name: prepare web ui
|
||||
if: steps.cache-web.outputs.cache-hit != 'true' && contains(matrix.job.profiles, 'selenium')
|
||||
working-directory: web
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
npm run build:sfe
|
||||
- name: run e2e
|
||||
run: |
|
||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
uv run coverage combine
|
||||
uv run coverage xml
|
||||
- uses: ./.github/actions/test-results
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
flags: e2e
|
||||
test-openid-conformance:
|
||||
name: test-openid-conformance (${{ matrix.job.name }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- name: oidc_basic
|
||||
glob: tests/openid_conformance/test_oidc_basic.py
|
||||
- name: oidc_implicit
|
||||
glob: tests/openid_conformance/test_oidc_implicit.py
|
||||
- name: oidc_rp-initiated
|
||||
glob: tests/openid_conformance/test_oidc_rp_initiated.py
|
||||
- name: oidc_frontchannel
|
||||
glob: tests/openid_conformance/test_oidc_frontchannel.py
|
||||
- name: oidc_backchannel
|
||||
glob: tests/openid_conformance/test_oidc_backchannel.py
|
||||
- name: ssf_transmitter
|
||||
glob: tests/openid_conformance/test_ssf_transmitter.py
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup e2e env (chrome, etc)
|
||||
env:
|
||||
COMPOSE_PROFILES: selenium
|
||||
run: |
|
||||
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
|
||||
- name: Setup conformance suite
|
||||
run: |
|
||||
docker compose -f tests/openid_conformance/compose.yml up -d --quiet-pull
|
||||
docker compose -f tests/e2e/docker-compose.yml up -d --quiet-pull
|
||||
- id: cache-web
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
- name: prepare web ui
|
||||
if: steps.cache-web.outputs.cache-hit != 'true'
|
||||
working-directory: web
|
||||
run: |
|
||||
npm ci
|
||||
make -C .. gen-client-ts
|
||||
npm run build
|
||||
npm run build:sfe
|
||||
- name: run conformance
|
||||
- name: run e2e
|
||||
run: |
|
||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
uv run coverage combine
|
||||
uv run coverage xml
|
||||
- uses: ./.github/actions/test-results
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
flags: conformance
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: conformance-certification-${{ matrix.job.name }}
|
||||
path: tests/openid_conformance/exports/
|
||||
test-rust:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: rust,runtime
|
||||
- name: run tests
|
||||
run: |
|
||||
cargo llvm-cov --no-report nextest --workspace
|
||||
cargo llvm-cov report --codecov --output-path target/llvm-cov-target/rust.json
|
||||
- uses: ./.github/actions/test-results
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
files: target/llvm-cov-target/rust.json
|
||||
flags: rust
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-rust
|
||||
path: target/llvm-cov-target/rust.json
|
||||
flags: e2e
|
||||
ci-core-mark:
|
||||
if: always()
|
||||
needs:
|
||||
- lint
|
||||
- test-gen
|
||||
- test-migrations
|
||||
- test-migrations-from-stable
|
||||
- test-unittest
|
||||
@@ -395,7 +260,7 @@ jobs:
|
||||
pull-requests: write
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: prepare variables
|
||||
|
||||
36
.github/workflows/ci-outpost.yml
vendored
36
.github/workflows/ci-outpost.yml
vendored
@@ -21,8 +21,8 @@ jobs:
|
||||
lint-golint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Prepare and generate API
|
||||
@@ -31,6 +31,8 @@ jobs:
|
||||
mkdir -p web/dist
|
||||
mkdir -p website/help
|
||||
touch web/dist/test website/help/test
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v8
|
||||
with:
|
||||
@@ -40,12 +42,14 @@ jobs:
|
||||
test-unittest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: prepare database
|
||||
run: |
|
||||
uv run make migrate
|
||||
@@ -82,13 +86,13 @@ jobs:
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
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@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -98,17 +102,19 @@ jobs:
|
||||
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
|
||||
- name: Login to Container Registry
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: lifecycle/container/${{ matrix.type }}.Dockerfile
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||
@@ -116,7 +122,7 @@ jobs:
|
||||
context: .
|
||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
|
||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
with:
|
||||
@@ -139,17 +145,19 @@ jobs:
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
|
||||
18
.github/workflows/ci-web.yml
vendored
18
.github/workflows/ci-web.yml
vendored
@@ -31,8 +31,8 @@ jobs:
|
||||
- command: lit-analyse
|
||||
project: web
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: ${{ matrix.project }}/package.json
|
||||
cache: "npm"
|
||||
@@ -40,20 +40,24 @@ jobs:
|
||||
- working-directory: ${{ matrix.project }}/
|
||||
run: |
|
||||
npm ci
|
||||
- name: Generate API
|
||||
run: make gen-client-ts
|
||||
- name: Lint
|
||||
working-directory: ${{ matrix.project }}/
|
||||
run: npm run ${{ matrix.command }}
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- working-directory: web/
|
||||
run: npm ci
|
||||
- name: Generate API
|
||||
run: make gen-client-ts
|
||||
- name: build
|
||||
working-directory: web/
|
||||
run: npm run build
|
||||
@@ -72,14 +76,16 @@ jobs:
|
||||
- ci-web-mark
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- working-directory: web/
|
||||
run: npm ci
|
||||
- name: Generate API
|
||||
run: make gen-client-ts
|
||||
- name: test
|
||||
working-directory: web/
|
||||
run: npm run test || exit 0
|
||||
|
||||
10
.github/workflows/gen-image-compress.yml
vendored
10
.github/workflows/gen-image-compress.yml
vendored
@@ -29,20 +29,20 @@ jobs:
|
||||
github.event.pull_request.head.repo.full_name == github.repository)
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Compress images
|
||||
id: compress
|
||||
uses: calibreapp/image-actions@e2cc8db5d49c849e00844dfebf01438318e96fa2 # main
|
||||
uses: calibreapp/image-actions@420075c115b26f8785e293c5bd5bef0911c506e5 # main
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
compressOnly: ${{ github.event_name != 'pull_request' }}
|
||||
- uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
|
||||
if: "${{ github.event_name != 'pull_request' && steps.compress.outputs.markdown != '' }}"
|
||||
id: cpr
|
||||
with:
|
||||
|
||||
@@ -16,17 +16,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- run: uv run ak update_webauthn_mds
|
||||
- uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
6
.github/workflows/gh-cherry-pick.yml
vendored
6
.github/workflows/gh-cherry-pick.yml
vendored
@@ -10,14 +10,14 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
if: ${{ env.GH_APP_ID != '' }}
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
env:
|
||||
GH_APP_ID: ${{ secrets.GH_APP_ID }}
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
if: ${{ steps.app-token.outcome != 'skipped' }}
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
2
.github/workflows/gh-gha-cache-cleanup.yml
vendored
2
.github/workflows/gh-gha-cache-cleanup.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
|
||||
4
.github/workflows/gh-ghcr-retention.yml
vendored
4
.github/workflows/gh-ghcr-retention.yml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Delete 'dev' containers older than a week
|
||||
uses: snok/container-retention-policy@3b0972b2276b171b212f8c4efbca59ebba26eceb # v3.0.1
|
||||
with:
|
||||
|
||||
7
.github/workflows/packages-npm-publish.yml
vendored
7
.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
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 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@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
|
||||
with:
|
||||
files: |
|
||||
${{ matrix.package }}/package.json
|
||||
|
||||
2
.github/workflows/qa-codeql.yml
vendored
2
.github/workflows/qa-codeql.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
language: ["go", "javascript", "python"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Initialize CodeQL
|
||||
|
||||
2
.github/workflows/qa-semgrep.yml
vendored
2
.github/workflows/qa-semgrep.yml
vendored
@@ -26,5 +26,5 @@ jobs:
|
||||
image: semgrep/semgrep
|
||||
if: (github.actor != 'dependabot[bot]')
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- run: semgrep ci
|
||||
|
||||
14
.github/workflows/release-branch-off.yml
vendored
14
.github/workflows/release-branch-off.yml
vendored
@@ -29,12 +29,12 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
ref: main
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
@@ -57,12 +57,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
- name: Bump version
|
||||
run: "make bump version=${{ inputs.next_version }}.0-rc1"
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: release-bump-${{ inputs.next_version }}
|
||||
|
||||
2
.github/workflows/release-next-branch.yml
vendored
2
.github/workflows/release-next-branch.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: internal-production
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
ref: main
|
||||
- run: |
|
||||
|
||||
74
.github/workflows/release-publish.yml
vendored
74
.github/workflows/release-publish.yml
vendored
@@ -31,11 +31,11 @@ jobs:
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 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@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -44,21 +44,21 @@ jobs:
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/docs
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: website/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
||||
id: attest
|
||||
if: true
|
||||
with:
|
||||
@@ -83,19 +83,14 @@ jobs:
|
||||
- radius
|
||||
- rac
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 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@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -103,29 +98,33 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/${{ matrix.type }},authentik/${{ matrix.type }}
|
||||
- name: make empty clients
|
||||
run: |
|
||||
mkdir -p ./gen-ts-api
|
||||
mkdir -p ./gen-go-api
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
id: push
|
||||
with:
|
||||
push: true
|
||||
build-args: |
|
||||
VERSION=${{ github.ref }}
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: lifecycle/container/${{ matrix.type }}.Dockerfile
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
@@ -147,22 +146,19 @@ jobs:
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Install web dependencies
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
npm run build-proxy
|
||||
- name: Build outpost
|
||||
run: |
|
||||
@@ -172,7 +168,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 }}
|
||||
@@ -190,8 +186,8 @@ jobs:
|
||||
AWS_REGION: eu-central-1
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5
|
||||
with:
|
||||
role-to-assume: "arn:aws:iam::016170277896:role/github_goauthentik_authentik"
|
||||
aws-region: ${{ env.AWS_REGION }}
|
||||
@@ -206,15 +202,15 @@ jobs:
|
||||
- build-outpost-binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> lifecycle/container/.env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> lifecycle/container/.env
|
||||
docker compose -f lifecycle/container/compose.yml pull -q
|
||||
docker compose -f lifecycle/container/compose.yml up --no-start
|
||||
docker compose -f lifecycle/container/compose.yml start postgresql
|
||||
docker compose -f lifecycle/container/compose.yml run -u root server test-all
|
||||
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
|
||||
docker compose pull -q
|
||||
docker compose up --no-start
|
||||
docker compose start postgresql
|
||||
docker compose run -u root server test-all
|
||||
sentry-release:
|
||||
needs:
|
||||
- build-server
|
||||
@@ -222,7 +218,7 @@ jobs:
|
||||
- build-outpost-binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
@@ -236,7 +232,7 @@ jobs:
|
||||
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
|
||||
docker cp ${container}:web/ .
|
||||
- name: Create a Sentry.io release
|
||||
uses: getsentry/action-release@5657c9e888b4e2cc85f4d29143ea4131fde4a73a # v3
|
||||
uses: getsentry/action-release@128c5058bbbe93c8e02147fe0a9c713f166259a6 # v3
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
41
.github/workflows/release-tag.yml
vendored
41
.github/workflows/release-tag.yml
vendored
@@ -49,14 +49,8 @@ jobs:
|
||||
test:
|
||||
name: Pre-release test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-inputs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
with:
|
||||
ref: "version-${{ needs.check-inputs.outputs.major_version }}"
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- run: make test-docker
|
||||
bump-authentik:
|
||||
name: Bump authentik version
|
||||
@@ -67,16 +61,16 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- id: get-user-id
|
||||
name: Get GitHub app user ID
|
||||
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
ref: "version-${{ needs.check-inputs.outputs.major_version }}"
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
@@ -91,12 +85,11 @@ jobs:
|
||||
# ID from https://api.github.com/users/authentik-automation[bot]
|
||||
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
|
||||
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
|
||||
git pull
|
||||
git commit -a -m "release: ${{ inputs.version }}" --allow-empty
|
||||
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
|
||||
git push --follow-tags
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
tag_name: "version/${{ inputs.version }}"
|
||||
@@ -115,17 +108,17 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
repositories: helm
|
||||
- id: get-user-id
|
||||
name: Get GitHub app user ID
|
||||
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
repository: "${{ github.repository_owner }}/helm"
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
@@ -137,7 +130,7 @@ jobs:
|
||||
sed -E -i 's/[0-9]{4}\.[0-9]{1,2}\.[0-9]+$/${{ inputs.version }}/' charts/authentik/Chart.yaml
|
||||
./scripts/helm-docs.sh
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
branch: bump-${{ inputs.version }}
|
||||
@@ -157,17 +150,17 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
repositories: version
|
||||
- id: get-user-id
|
||||
name: Get GitHub app user ID
|
||||
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
repository: "${{ github.repository_owner }}/version"
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
@@ -175,28 +168,24 @@ jobs:
|
||||
if: "${{ inputs.release_reason == 'feature' }}"
|
||||
run: |
|
||||
changelog_url="https://docs.goauthentik.io/docs/releases/${{ needs.check-inputs.outputs.major_version }}"
|
||||
reason="${{ inputs.release_reason }}"
|
||||
jq \
|
||||
--arg version "${{ inputs.version }}" \
|
||||
--arg changelog "See ${changelog_url}" \
|
||||
--arg changelog_url "${changelog_url}" \
|
||||
--arg reason "${reason}" \
|
||||
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url | .stable.reason = $reason' version.json > version.new.json
|
||||
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url' version.json > version.new.json
|
||||
mv version.new.json version.json
|
||||
- name: Bump version
|
||||
if: "${{ inputs.release_reason != 'feature' }}"
|
||||
run: |
|
||||
changelog_url="https://docs.goauthentik.io/docs/releases/${{ needs.check-inputs.outputs.major_version }}#fixed-in-$(echo -n ${{ inputs.version}} | sed 's/\.//g')"
|
||||
reason="${{ inputs.release_reason }}"
|
||||
jq \
|
||||
--arg version "${{ inputs.version }}" \
|
||||
--arg changelog "See ${changelog_url}" \
|
||||
--arg changelog_url "${changelog_url}" \
|
||||
--arg reason "${reason}" \
|
||||
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url | .stable.reason = $reason' version.json > version.new.json
|
||||
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url' version.json > version.new.json
|
||||
mv version.new.json version.json
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
branch: bump-${{ inputs.version }}
|
||||
|
||||
6
.github/workflows/repo-stale.yml
vendored
6
.github/workflows/repo-stale.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
|
||||
with:
|
||||
repo-token: ${{ steps.generate_token.outputs.token }}
|
||||
days-before-stale: 60
|
||||
|
||||
@@ -21,18 +21,20 @@ jobs:
|
||||
steps:
|
||||
- id: generate_token
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Generate API
|
||||
run: make gen-client-ts
|
||||
- name: run extract
|
||||
run: |
|
||||
uv run make i18n-extract
|
||||
@@ -42,7 +44,7 @@ jobs:
|
||||
make web-check-compile
|
||||
- name: Create Pull Request
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: extract-compile-backend-translation
|
||||
|
||||
30
.gitignore
vendored
30
.gitignore
vendored
@@ -15,9 +15,6 @@ media
|
||||
|
||||
node_modules
|
||||
|
||||
.cspellcache
|
||||
cspell-report.*
|
||||
|
||||
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||
# in your Git repository. Update and uncomment the following line accordingly.
|
||||
# <django-project-name>/staticfiles/
|
||||
@@ -195,24 +192,6 @@ pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
|
||||
# End of https://www.gitignore.io/api/python,django
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/rust
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=rust
|
||||
|
||||
### Rust ###
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust
|
||||
|
||||
/static/
|
||||
local.env.yml
|
||||
|
||||
@@ -220,6 +199,7 @@ media/
|
||||
*mmdb
|
||||
|
||||
.idea/
|
||||
/gen-*/
|
||||
data/
|
||||
|
||||
# Local Netlify folder
|
||||
@@ -229,12 +209,6 @@ source_docs/
|
||||
|
||||
### Golang ###
|
||||
/vendor/
|
||||
server
|
||||
proxy
|
||||
ldap
|
||||
rac
|
||||
radius
|
||||
|
||||
### Docker ###
|
||||
tests/openid_conformance/exports/*.zip
|
||||
compose.override.yml
|
||||
docker-compose.override.yml
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Prettier Ignorefile
|
||||
|
||||
## Static Files
|
||||
CODEOWNERS
|
||||
**/LICENSE
|
||||
|
||||
authentik/stages/**/*
|
||||
|
||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -17,6 +17,6 @@
|
||||
"ms-python.vscode-pylance",
|
||||
"redhat.vscode-yaml",
|
||||
"Tobermory.es6-string-html",
|
||||
"unifiedjs.vscode-mdx"
|
||||
"unifiedjs.vscode-mdx",
|
||||
]
|
||||
}
|
||||
|
||||
28
.vscode/settings.json
vendored
28
.vscode/settings.json
vendored
@@ -14,10 +14,6 @@
|
||||
"[xml]": {
|
||||
"editor.minimap.markSectionHeaderRegex": "<!--\\s*#\\bregion\\s*(?<separator>-?)\\s*(?<label>.*)\\s*-->"
|
||||
},
|
||||
"files.associations": {
|
||||
// The built-in "ignore" language gives us enough syntax highlighting to make these files readable.
|
||||
"**/dictionaries/*.txt": "ignore"
|
||||
},
|
||||
"todo-tree.tree.showCountsInTree": true,
|
||||
"todo-tree.tree.showBadges": true,
|
||||
"yaml.customTags": [
|
||||
@@ -38,10 +34,10 @@
|
||||
"!AtIndex scalar",
|
||||
"!ParseJSON scalar"
|
||||
],
|
||||
"js/ts.preferences.importModuleSpecifier": "non-relative",
|
||||
"js/ts.preferences.importModuleSpecifierEnding": "index",
|
||||
"js/ts.tsdk.path": "./node_modules/typescript/lib",
|
||||
"js/ts.tsdk.promptToUseWorkspaceVersion": true,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "index",
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"yaml.schemas": {
|
||||
"./blueprints/schema.json": "blueprints/**/*.yaml"
|
||||
},
|
||||
@@ -53,17 +49,13 @@
|
||||
"ignoreCase": false
|
||||
}
|
||||
],
|
||||
"go.testFlags": ["-count=1"],
|
||||
"go.testFlags": [
|
||||
"-count=1"
|
||||
],
|
||||
"go.testEnvVars": {
|
||||
"WORKSPACE_DIR": "${workspaceFolder}"
|
||||
},
|
||||
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"],
|
||||
"search.exclude": {
|
||||
"**/*.code-search": true,
|
||||
"**/bower_components": true,
|
||||
"**/node_modules": true,
|
||||
"**/playwright-report/**": true,
|
||||
"**/website/**/build": true,
|
||||
"**/client-*": true
|
||||
}
|
||||
"github-actions.workflows.pinned.workflows": [
|
||||
".github/workflows/ci-main.yml"
|
||||
]
|
||||
}
|
||||
|
||||
23
CODEOWNERS
23
CODEOWNERS
@@ -3,7 +3,6 @@
|
||||
# Backend
|
||||
authentik/ @goauthentik/backend
|
||||
blueprints/ @goauthentik/backend
|
||||
src/ @goauthentik/backend
|
||||
cmd/ @goauthentik/backend
|
||||
internal/ @goauthentik/backend
|
||||
lifecycle/ @goauthentik/backend
|
||||
@@ -12,44 +11,34 @@ scripts/ @goauthentik/backend
|
||||
tests/ @goauthentik/backend
|
||||
pyproject.toml @goauthentik/backend
|
||||
uv.lock @goauthentik/backend
|
||||
Cargo.toml @goauthentik/backend
|
||||
Cargo.lock @goauthentik/backend
|
||||
build.rs @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
|
||||
lifecycle/container/ @goauthentik/infrastructure
|
||||
Dockerfile @goauthentik/infrastructure
|
||||
*Dockerfile @goauthentik/infrastructure
|
||||
.dockerignore @goauthentik/infrastructure
|
||||
docker-compose.yml @goauthentik/infrastructure
|
||||
Makefile @goauthentik/infrastructure
|
||||
.editorconfig @goauthentik/infrastructure
|
||||
CODEOWNERS @goauthentik/infrastructure
|
||||
# Backend packages
|
||||
packages/ak-* @goauthentik/backend
|
||||
packages/client-rust @goauthentik/backend
|
||||
packages/django-channels-postgres @goauthentik/backend
|
||||
packages/django-postgres-cache @goauthentik/backend
|
||||
packages/django-dramatiq-postgres @goauthentik/backend
|
||||
# Web packages
|
||||
tsconfig.json @goauthentik/frontend
|
||||
package.json @goauthentik/frontend
|
||||
package-lock.json @goauthentik/frontend
|
||||
packages/package.json @goauthentik/frontend
|
||||
packages/package-lock.json @goauthentik/frontend
|
||||
packages/client-ts @goauthentik/frontend
|
||||
packages/package.json @goauthentik/backend @goauthentik/frontend
|
||||
packages/package-lock.json @goauthentik/backend @goauthentik/frontend
|
||||
packages/docusaurus-config @goauthentik/frontend
|
||||
packages/esbuild-plugin-live-reload @goauthentik/frontend
|
||||
packages/eslint-config @goauthentik/frontend
|
||||
packages/prettier-config @goauthentik/frontend
|
||||
packages/logger-js @goauthentik/frontend
|
||||
packages/tsconfig @goauthentik/frontend
|
||||
# Web
|
||||
web/ @goauthentik/frontend
|
||||
# Locale
|
||||
/locale/ @goauthentik/backend @goauthentik/frontend
|
||||
locale/ @goauthentik/backend @goauthentik/frontend
|
||||
web/xliff/ @goauthentik/backend @goauthentik/frontend
|
||||
# Docs
|
||||
website/ @goauthentik/docs
|
||||
|
||||
5208
Cargo.lock
generated
5208
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
289
Cargo.toml
289
Cargo.toml
@@ -1,289 +0,0 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"packages/ak-axum",
|
||||
"packages/ak-common",
|
||||
"packages/client-rust",
|
||||
"website/scripts/docsmg",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "2026.5.0-rc1"
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
description = "Making authentication simple."
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
homepage = "https://goauthentik.io"
|
||||
repository = "https://github.com/goauthentik/authentik.git"
|
||||
license-file = "LICENSE"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
arc-swap = "= 1.9.1"
|
||||
argh = "= 0.1.19"
|
||||
axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] }
|
||||
aws-lc-rs = { version = "= 1.16.3", features = ["fips"] }
|
||||
axum = { version = "= 0.8.9", features = ["http2", "macros", "ws"] }
|
||||
clap = { version = "= 4.6.1", features = ["derive", "env"] }
|
||||
client-ip = { version = "0.2.1", features = ["forwarded-header"] }
|
||||
color-eyre = "= 0.6.5"
|
||||
colored = "= 3.1.1"
|
||||
config-rs = { package = "config", version = "= 0.15.22", default-features = false, features = [
|
||||
"json",
|
||||
"yaml",
|
||||
] }
|
||||
console-subscriber = "= 0.5.0"
|
||||
dotenvy = "= 0.15.7"
|
||||
durstr = "= 0.5.1"
|
||||
eyre = "= 0.6.12"
|
||||
forwarded-header-value = "= 0.1.1"
|
||||
futures = "= 0.3.32"
|
||||
glob = "= 0.3.3"
|
||||
hyper-unix-socket = "= 0.6.1"
|
||||
hyper-util = "= 0.1.20"
|
||||
ipnet = { version = "= 2.12.0", features = ["serde"] }
|
||||
json-subscriber = "= 0.2.8"
|
||||
metrics = "= 0.24.5"
|
||||
metrics-exporter-prometheus = { version = "= 0.18.3", default-features = false }
|
||||
nix = { version = "= 0.31.2", features = ["hostname", "signal"] }
|
||||
notify = "= 8.2.0"
|
||||
pin-project-lite = "= 0.2.17"
|
||||
pyo3 = "= 0.28.3"
|
||||
pyo3-build-config = "= 0.28.3"
|
||||
regex = "= 1.12.3"
|
||||
reqwest = { version = "= 0.13.3", features = [
|
||||
"form",
|
||||
"json",
|
||||
"multipart",
|
||||
"query",
|
||||
"rustls",
|
||||
"stream",
|
||||
] }
|
||||
reqwest-middleware = { version = "= 0.5.1", features = [
|
||||
"form",
|
||||
"json",
|
||||
"multipart",
|
||||
"query",
|
||||
"rustls",
|
||||
] }
|
||||
rustls = { version = "= 0.23.40", features = ["fips"] }
|
||||
sentry = { version = "= 0.47.0", default-features = false, features = [
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"debug-images",
|
||||
"panic",
|
||||
"rustls",
|
||||
"reqwest",
|
||||
"tower",
|
||||
"tracing",
|
||||
] }
|
||||
serde = { version = "= 1.0.228", features = ["derive"] }
|
||||
serde_json = "= 1.0.149"
|
||||
serde_repr = "= 0.1.20"
|
||||
serde_with = { version = "= 3.18.0", default-features = false, features = [
|
||||
"base64",
|
||||
] }
|
||||
sqlx = { version = "= 0.8.6", default-features = false, features = [
|
||||
"runtime-tokio",
|
||||
"tls-rustls-aws-lc-rs",
|
||||
"postgres",
|
||||
"derive",
|
||||
"macros",
|
||||
"uuid",
|
||||
"chrono",
|
||||
"ipnet",
|
||||
"json",
|
||||
] }
|
||||
tempfile = "= 3.27.0"
|
||||
thiserror = "= 2.0.18"
|
||||
time = { version = "= 0.3.47", features = ["macros"] }
|
||||
tokio = { version = "= 1.52.1", features = ["full", "tracing"] }
|
||||
tokio-retry2 = "= 0.9.1"
|
||||
tokio-rustls = "= 0.26.4"
|
||||
tokio-util = { version = "= 0.7.18", features = ["full"] }
|
||||
tower = "= 0.5.3"
|
||||
tower-http = { version = "= 0.6.8", features = ["timeout"] }
|
||||
tracing = "= 0.1.44"
|
||||
tracing-error = "= 0.2.1"
|
||||
tracing-subscriber = { version = "= 0.3.23", features = [
|
||||
"env-filter",
|
||||
"json",
|
||||
"local-time",
|
||||
"tracing-log",
|
||||
] }
|
||||
url = "= 2.5.8"
|
||||
uuid = { version = "= 1.23.1", features = ["serde", "v4"] }
|
||||
which = "= 8.0.2"
|
||||
|
||||
ak-axum = { package = "authentik-axum", version = "2026.5.0-rc1", path = "./packages/ak-axum" }
|
||||
ak-client = { package = "authentik-client", version = "2026.5.0-rc1", path = "./packages/client-rust" }
|
||||
ak-common = { package = "authentik-common", version = "2026.5.0-rc1", path = "./packages/ak-common", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
ambiguous_negative_literals = "warn"
|
||||
closure_returning_async_block = "warn"
|
||||
macro_use_extern_crate = "deny"
|
||||
# must_not_suspend = "deny", unstable see https://github.com/rust-lang/rust/issues/83310
|
||||
non_ascii_idents = "deny"
|
||||
redundant_imports = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unit_bindings = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unsafe_code = "deny"
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_macro_rules = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
[workspace.lints.rustdoc]
|
||||
unescaped_backticks = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
### enable all lints
|
||||
cargo = { priority = -1, level = "warn" }
|
||||
complexity = { priority = -1, level = "warn" }
|
||||
correctness = { priority = -1, level = "warn" }
|
||||
nursery = { priority = -1, level = "warn" }
|
||||
pedantic = { priority = -1, level = "warn" }
|
||||
perf = { priority = -1, level = "warn" }
|
||||
# Those are too restrictive and disabled by default, however we enable some below
|
||||
# restriction = { priority = -1, level = "warn" }
|
||||
style = { priority = -1, level = "warn" }
|
||||
suspicious = { priority = -1, level = "warn" }
|
||||
### and disable the ones we don't want
|
||||
### cargo group
|
||||
multiple_crate_versions = "allow"
|
||||
### pedantic group
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
must_use_candidate = "allow"
|
||||
redundant_closure_for_method_calls = "allow"
|
||||
struct_field_names = "allow"
|
||||
too_many_lines = "allow"
|
||||
### nursery
|
||||
missing_const_for_fn = "allow"
|
||||
option_if_let_else = "allow"
|
||||
redundant_pub_crate = "allow"
|
||||
significant_drop_tightening = "allow"
|
||||
### restriction group
|
||||
allow_attributes = "warn"
|
||||
allow_attributes_without_reason = "warn"
|
||||
as_conversions = "warn"
|
||||
as_pointer_underscore = "warn"
|
||||
as_underscore = "warn"
|
||||
assertions_on_result_states = "warn"
|
||||
clone_on_ref_ptr = "warn"
|
||||
create_dir = "warn"
|
||||
dbg_macro = "warn"
|
||||
default_numeric_fallback = "warn"
|
||||
disallowed_script_idents = "warn"
|
||||
empty_drop = "warn"
|
||||
empty_enum_variants_with_brackets = "warn"
|
||||
empty_structs_with_brackets = "warn"
|
||||
error_impl_error = "warn"
|
||||
exit = "warn"
|
||||
filetype_is_file = "warn"
|
||||
float_cmp_const = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
get_unwrap = "warn"
|
||||
if_then_some_else_none = "warn"
|
||||
impl_trait_in_params = "warn"
|
||||
infinite_loop = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
map_with_unused_argument_over_ranges = "warn"
|
||||
mem_forget = "warn"
|
||||
missing_asserts_for_indexing = "warn"
|
||||
missing_trait_methods = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mutex_atomic = "warn"
|
||||
mutex_integer = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
non_zero_suggestions = "warn"
|
||||
panic_in_result_fn = "warn"
|
||||
pathbuf_init_then_push = "warn"
|
||||
print_stdout = "warn"
|
||||
rc_buffer = "warn"
|
||||
redundant_test_prefix = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
ref_patterns = "warn"
|
||||
renamed_function_params = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
return_and_then = "warn"
|
||||
same_name_method = "warn"
|
||||
semicolon_inside_block = "warn"
|
||||
str_to_string = "warn"
|
||||
string_add = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
tests_outside_test_module = "warn"
|
||||
todo = "warn"
|
||||
try_err = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unimplemented = "warn"
|
||||
unnecessary_safety_comment = "warn"
|
||||
unnecessary_safety_doc = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
unneeded_field_pattern = "warn"
|
||||
unseparated_literal_suffix = "warn"
|
||||
unused_result_ok = "warn"
|
||||
unused_trait_names = "warn"
|
||||
unwrap_in_result = "warn"
|
||||
unwrap_used = "warn"
|
||||
verbose_file_reads = "warn"
|
||||
|
||||
[profile.dev.package.backtrace]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
debug = 2
|
||||
lto = "fat"
|
||||
# Because of the async runtime, we want to die straightaway if we panic.
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
[package]
|
||||
name = "authentik"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
readme.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
license-file.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["core", "proxy"]
|
||||
core = ["ak-common/core", "dep:pyo3", "dep:sqlx"]
|
||||
proxy = ["ak-common/proxy"]
|
||||
|
||||
[build-dependencies]
|
||||
pyo3-build-config.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ak-axum.workspace = true
|
||||
ak-common.workspace = true
|
||||
arc-swap.workspace = true
|
||||
argh.workspace = true
|
||||
axum.workspace = true
|
||||
color-eyre.workspace = true
|
||||
eyre.workspace = true
|
||||
hyper-unix-socket.workspace = true
|
||||
hyper-util.workspace = true
|
||||
metrics.workspace = true
|
||||
metrics-exporter-prometheus.workspace = true
|
||||
nix.workspace = true
|
||||
pyo3 = { workspace = true, optional = true }
|
||||
sqlx = { workspace = true, optional = true }
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
which.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage: Build webui
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24-trixie-slim@sha256:735dd688da64d22ebd9dd374b3e7e5a874635668fd2a6ec20ca1f99264294086 AS node-builder
|
||||
# Stage 1: Build webui
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24-trixie-slim@sha256:45babd1b4ce0349fb12c4e24bf017b90b96d52806db32e001e3013f341bef0fe AS node-builder
|
||||
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
@@ -9,10 +9,6 @@ ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /work/web
|
||||
|
||||
# These files need to be copied and cannot be mounted as `npm ci` will build the client's typescript
|
||||
COPY ./packages /work/packages
|
||||
COPY ./web/packages /work/web/packages
|
||||
|
||||
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
||||
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
||||
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
|
||||
@@ -24,12 +20,13 @@ COPY ./package.json /work
|
||||
COPY ./web /work/web/
|
||||
# TODO: Update this after moving website to docs
|
||||
COPY ./website /work/website/
|
||||
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
||||
|
||||
RUN npm run build && \
|
||||
npm run build:sfe
|
||||
|
||||
# Stage: Build go proxy
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.26.2-trixie@sha256:4a7137ea573f79c86ae451ff05817ed762ef5597fcf732259e97abeb3108d873 AS go-builder
|
||||
# Stage 2: Build go proxy
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:4f9d98ebaa759f776496d850e0439c48948d587b191fc3949b5f5e4667abef90 AS go-builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -42,9 +39,8 @@ WORKDIR /go/src/goauthentik.io
|
||||
|
||||
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
||||
dpkg --add-architecture arm64 && \
|
||||
dpkg --add-architecture amd64 && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends crossbuild-essential-arm64 gcc-aarch64-linux-gnu crossbuild-essential-amd64 gcc-x86-64-linux-gnu
|
||||
apt-get install -y --no-install-recommends crossbuild-essential-arm64 gcc-aarch64-linux-gnu
|
||||
|
||||
RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
|
||||
--mount=type=bind,target=/go/src/goauthentik.io/go.sum,src=./go.sum \
|
||||
@@ -59,16 +55,14 @@ COPY --from=node-builder /work/web/security.txt /go/src/goauthentik.io/web/secur
|
||||
COPY ./internal /go/src/goauthentik.io/internal
|
||||
COPY ./go.mod /go/src/goauthentik.io/go.mod
|
||||
COPY ./go.sum /go/src/goauthentik.io/go.sum
|
||||
COPY ./packages/client-go /go/src/goauthentik.io/packages/client-go
|
||||
|
||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||
if [ "$TARGETARCH" = "arm64" ] && [ "$(uname -m)" != "aarch64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||
if [ "$TARGETARCH" = "amd64" ] && [ "$(uname -m)" != "x86_64" ]; then export CC=x86_64-linux-gnu-gcc; fi && \
|
||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
||||
go build -o /go/authentik-server ./cmd/server
|
||||
go build -o /go/authentik ./cmd/server
|
||||
|
||||
# Stage: MaxMind GeoIP
|
||||
# Stage 3: MaxMind GeoIP
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.1@sha256:faecdca22579730ab0b7dea5aa9af350bb3c93cb9d39845c173639ead30346d2 AS geoip
|
||||
|
||||
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
|
||||
@@ -81,32 +75,10 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
mkdir -p /usr/share/GeoIP && \
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage: download Rust toolchain
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341 AS rust-toolchain
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
|
||||
RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
apt-get update && \
|
||||
# Required for installing pip packages
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Build essentials
|
||||
build-essential && \
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain none && \
|
||||
rustup install && \
|
||||
rustup default "$(sed -n 's/channel = "\(.*\)"/\1/p' rust-toolchain.toml)" && \
|
||||
rustc --version && \
|
||||
cargo --version
|
||||
|
||||
RUN cat /root/.rustup/settings.toml
|
||||
|
||||
# Stage: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.11.5@sha256:555ac94f9a22e656fc5f2ce5dfee13b04e94d099e46bb8dd3a73ec7263f2e484 AS uv
|
||||
# Stage: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.14.3-slim-trixie-fips@sha256:bf45eb77a010d76fe6abd7ae137d1b0c44b6227cd984945042135fdf05ebf8d9 AS python-base
|
||||
# Stage 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.9.15@sha256:4c1ad814fe658851f50ff95ecd6948673fffddb0d7994bdb019dcb58227abd52 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
@@ -119,58 +91,17 @@ WORKDIR /ak-root/
|
||||
|
||||
COPY --from=uv /uv /uvx /bin/
|
||||
|
||||
# Stage: build rust binary
|
||||
FROM python-base AS rust-builder
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
COPY --from=rust-toolchain /root/.rustup /root/.rustup
|
||||
COPY --from=rust-toolchain /root/.cargo /root/.cargo
|
||||
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
||||
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# common dependencies
|
||||
build-essential \
|
||||
# aws-lc deps
|
||||
cmake clang golang
|
||||
# See https://github.com/aws/aws-lc-rs/issues/569
|
||||
ENV AWS_LC_FIPS_SYS_CC=clang
|
||||
|
||||
RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
--mount=type=bind,target=Cargo.toml,src=Cargo.toml \
|
||||
--mount=type=bind,target=Cargo.lock,src=Cargo.lock \
|
||||
--mount=type=bind,target=.cargo/,src=.cargo/ \
|
||||
--mount=type=bind,target=build.rs,src=build.rs \
|
||||
--mount=type=bind,target=src/,src=src/ \
|
||||
--mount=type=bind,target=packages/ak-axum,src=packages/ak-axum \
|
||||
--mount=type=bind,target=packages/ak-common,src=packages/ak-common \
|
||||
--mount=type=bind,target=packages/client-rust,src=packages/client-rust \
|
||||
--mount=type=bind,target=authentik/lib/default.yml,src=authentik/lib/default.yml \
|
||||
# Required otherwise workspace discovery fails
|
||||
--mount=type=bind,target=website/scripts/docsmg/,src=website/scripts/docsmg/ \
|
||||
--mount=type=cache,id=cargo-git-db-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/git/db/ \
|
||||
--mount=type=cache,id=cargo-registry-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/registry/ \
|
||||
--mount=type=cache,id=rust-target-$TARGETARCH$TARGETVARIANT,target=/build/target/ \
|
||||
cargo build --package authentik --no-default-features --features core --locked --release && \
|
||||
cp ./target/release/authentik /bin/authentik
|
||||
|
||||
|
||||
# Stage: Python dependencies
|
||||
# Stage 6: Python dependencies
|
||||
FROM python-base AS python-deps
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
|
||||
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
||||
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
apt-get update && \
|
||||
# Required for installing pip packages
|
||||
apt-get install -y --no-install-recommends \
|
||||
@@ -183,26 +114,24 @@ RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/v
|
||||
# postgresql
|
||||
libpq-dev \
|
||||
# python-kadmin-rs
|
||||
krb5-multidev libkrb5-dev heimdal-multidev libclang-dev \
|
||||
clang libkrb5-dev sccache \
|
||||
# xmlsec
|
||||
libltdl-dev
|
||||
libltdl-dev && \
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
COPY --from=rust-toolchain /root/.rustup /root/.rustup
|
||||
COPY --from=rust-toolchain /root/.cargo /root/.cargo
|
||||
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec" \
|
||||
# https://github.com/rust-lang/rustup/issues/2949
|
||||
# Fixes issues where the rust version in the build cache is older than latest
|
||||
# and rustup tries to update it, which fails
|
||||
RUSTUP_PERMIT_COPY_RENAME="true"
|
||||
|
||||
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec"
|
||||
RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
|
||||
--mount=type=bind,target=uv.lock,src=uv.lock \
|
||||
--mount=type=bind,target=packages/ak-guardian,src=packages/ak-guardian \
|
||||
--mount=type=bind,target=packages/django-channels-postgres,src=packages/django-channels-postgres \
|
||||
--mount=type=bind,target=packages/django-dramatiq-postgres,src=packages/django-dramatiq-postgres \
|
||||
--mount=type=bind,target=packages/django-postgres-cache,src=packages/django-postgres-cache \
|
||||
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
--mount=type=cache,id=uv-python-deps-$TARGETARCH$TARGETVARIANT,target=/root/.cache/uv \
|
||||
--mount=type=bind,target=packages,src=packages \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-install-project --no-dev
|
||||
|
||||
# Stage: Run
|
||||
# Stage 7: Run
|
||||
FROM python-base AS final-image
|
||||
|
||||
ARG VERSION
|
||||
@@ -215,6 +144,7 @@ LABEL org.opencontainers.image.authors="Authentik Security Inc." \
|
||||
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
|
||||
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
|
||||
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
|
||||
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
|
||||
org.opencontainers.image.title="authentik server image" \
|
||||
org.opencontainers.image.url="https://goauthentik.io" \
|
||||
org.opencontainers.image.vendor="Authentik Security Inc." \
|
||||
@@ -226,11 +156,7 @@ WORKDIR /
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
# Required for runtime
|
||||
apt-get install -y --no-install-recommends \
|
||||
libpq5 libmaxminddb0 ca-certificates \
|
||||
krb5-multidev libkrb5-3 libkdb5-10 libkadm5clnt-mit12 \
|
||||
heimdal-multidev libkadm5clnt7t64-heimdal \
|
||||
libltdl7 libxslt1.1 && \
|
||||
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 libltdl7 libxslt1.1 && \
|
||||
# Required for bootstrap & healtcheck
|
||||
apt-get install -y --no-install-recommends runit && \
|
||||
pip3 install --no-cache-dir --upgrade pip && \
|
||||
@@ -253,8 +179,7 @@ COPY ./manage.py /
|
||||
COPY ./blueprints /blueprints
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
|
||||
COPY --from=rust-builder /bin/authentik /bin/authentik
|
||||
COPY --from=go-builder /go/authentik-server /bin/authentik-server
|
||||
COPY --from=go-builder /go/authentik /bin/authentik
|
||||
COPY ./packages/ /ak-root/packages
|
||||
RUN ln -s /ak-root/packages /packages
|
||||
COPY --from=python-deps /ak-root/.venv /ak-root/.venv
|
||||
288
Makefile
288
Makefile
@@ -5,53 +5,32 @@ SHELL := /usr/bin/env bash
|
||||
PWD = $(shell pwd)
|
||||
UID = $(shell id -u)
|
||||
GID = $(shell id -g)
|
||||
NPM_VERSION = $(shell python -m scripts.generate_semver)
|
||||
PY_SOURCES = authentik packages tests scripts lifecycle .github
|
||||
DOCKER_IMAGE ?= "authentik:test"
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE = sed -i ''
|
||||
else
|
||||
SED_INPLACE = sed -i
|
||||
endif
|
||||
GEN_API_TS = gen-ts-api
|
||||
GEN_API_PY = gen-py-api
|
||||
GEN_API_GO = gen-go-api
|
||||
|
||||
BREW_LDFLAGS :=
|
||||
BREW_CPPFLAGS :=
|
||||
BREW_PKG_CONFIG_PATH :=
|
||||
|
||||
CARGO := cargo
|
||||
UV := uv
|
||||
pg_user := $(shell uv run python -m authentik.lib.config postgresql.user 2>/dev/null)
|
||||
pg_host := $(shell uv run python -m authentik.lib.config postgresql.host 2>/dev/null)
|
||||
pg_name := $(shell uv run python -m authentik.lib.config postgresql.name 2>/dev/null)
|
||||
|
||||
# For macOS users, add the libxml2 installed from brew libxmlsec1 to the build path
|
||||
# to prevent SAML-related tests from failing and ensure correct pip dependency compilation
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
# Only add for brew users who installed libxmlsec1
|
||||
BREW_EXISTS := $(shell command -v brew 2> /dev/null)
|
||||
ifdef BREW_EXISTS
|
||||
LIBXML2_EXISTS := $(shell brew list libxml2 2> /dev/null)
|
||||
ifdef LIBXML2_EXISTS
|
||||
_xml_pref := $(shell brew --prefix libxml2)
|
||||
BREW_LDFLAGS += -L${_xml_pref}/lib
|
||||
BREW_CPPFLAGS += -I${_xml_pref}/include
|
||||
BREW_PKG_CONFIG_PATH = ${_xml_pref}/lib/pkgconfig:$(PKG_CONFIG_PATH)
|
||||
endif
|
||||
KRB5_EXISTS := $(shell brew list krb5 2> /dev/null)
|
||||
ifdef KRB5_EXISTS
|
||||
_krb5_pref := $(shell brew --prefix krb5)
|
||||
BREW_LDFLAGS += -L${_krb5_pref}/lib
|
||||
BREW_CPPFLAGS += -I${_krb5_pref}/include
|
||||
BREW_PKG_CONFIG_PATH = ${_krb5_pref}/lib/pkgconfig:$(PKG_CONFIG_PATH)
|
||||
endif
|
||||
UV := LDFLAGS="$(BREW_LDFLAGS)" CPPFLAGS="$(BREW_CPPFLAGS)" PKG_CONFIG_PATH="$(BREW_PKG_CONFIG_PATH)" uv
|
||||
endif
|
||||
endif
|
||||
# These functions are only evaluated when called in specific targets
|
||||
LIBXML2_EXISTS = $(shell brew list libxml2 2> /dev/null)
|
||||
KRB5_EXISTS = $(shell brew list krb5 2> /dev/null)
|
||||
|
||||
NPM_VERSION :=
|
||||
UV_EXISTS := $(shell command -v uv 2> /dev/null)
|
||||
ifdef UV_EXISTS
|
||||
NPM_VERSION := $(shell $(UV) run python -m scripts.generate_semver)
|
||||
else
|
||||
NPM_VERSION = $(shell python -m scripts.generate_semver)
|
||||
LIBXML2_LDFLAGS = -L$(shell brew --prefix libxml2)/lib $(LDFLAGS)
|
||||
LIBXML2_CPPFLAGS = -I$(shell brew --prefix libxml2)/include $(CPPFLAGS)
|
||||
LIBXML2_PKG_CONFIG = $(shell brew --prefix libxml2)/lib/pkgconfig:$(PKG_CONFIG_PATH)
|
||||
|
||||
KRB_PATH =
|
||||
|
||||
ifneq ($(KRB5_EXISTS),)
|
||||
KRB_PATH = PATH="$(shell brew --prefix krb5)/sbin:$(shell brew --prefix krb5)/bin:$$PATH"
|
||||
endif
|
||||
|
||||
all: lint-fix lint gen web test ## Lint, build, and test everything
|
||||
@@ -66,103 +45,85 @@ help: ## Show this help
|
||||
sort
|
||||
@echo ""
|
||||
|
||||
go-test: ## Run the golang tests
|
||||
go-test:
|
||||
go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
rust-test: ## Run the Rust tests
|
||||
$(CARGO) nextest run --workspace
|
||||
|
||||
test: ## Run the server tests and produce a coverage report (locally)
|
||||
$(UV) run coverage run manage.py test --keepdb $(or $(filter-out $@,$(MAKECMDGOALS)),authentik)
|
||||
$(UV) run coverage combine
|
||||
$(UV) run coverage html
|
||||
$(UV) run coverage report
|
||||
$(KRB_PATH) uv run coverage run manage.py test --keepdb $(or $(filter-out $@,$(MAKECMDGOALS)),authentik)
|
||||
uv run coverage html
|
||||
uv run coverage report
|
||||
|
||||
lint-fix-rust:
|
||||
$(CARGO) +nightly fmt --all -- --config-path "${PWD}/.cargo/rustfmt.toml"
|
||||
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||
uv run black $(PY_SOURCES)
|
||||
uv run ruff check --fix $(PY_SOURCES)
|
||||
|
||||
lint-fix: lint-fix-rust ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||
$(UV) run black $(PY_SOURCES)
|
||||
$(UV) run ruff check --fix $(PY_SOURCES)
|
||||
lint-codespell: ## Reports spelling errors.
|
||||
uv run codespell -w
|
||||
|
||||
lint-spellcheck: ## Reports spelling errors.
|
||||
npm run lint:spellcheck
|
||||
|
||||
lint: ci-lint-bandit ci-lint-mypy ci-lint-cargo-deny ci-lint-cargo-machete ## Lint the python and golang sources
|
||||
lint: ## Lint the python and golang sources
|
||||
uv run bandit -c pyproject.toml -r $(PY_SOURCES)
|
||||
golangci-lint run -v
|
||||
|
||||
core-install:
|
||||
ifdef ($(BREW_EXISTS))
|
||||
ifneq ($(LIBXML2_EXISTS),)
|
||||
# Clear cache to ensure fresh compilation
|
||||
$(UV) cache clean
|
||||
uv cache clean
|
||||
# Force compilation from source for lxml and xmlsec with correct environment
|
||||
$(UV) sync --frozen --reinstall-package lxml --reinstall-package xmlsec --no-binary-package lxml --no-binary-package xmlsec
|
||||
LDFLAGS="$(LIBXML2_LDFLAGS)" CPPFLAGS="$(LIBXML2_CPPFLAGS)" PKG_CONFIG_PATH="$(LIBXML2_PKG_CONFIG)" uv sync --frozen --reinstall-package lxml --reinstall-package xmlsec --no-binary-package lxml --no-binary-package xmlsec
|
||||
else
|
||||
$(UV) sync --frozen
|
||||
uv sync --frozen
|
||||
endif
|
||||
|
||||
migrate: ## Run the Authentik Django server's migrations
|
||||
$(UV) run python -m lifecycle.migrate
|
||||
uv run python -m lifecycle.migrate
|
||||
|
||||
i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that require translation into files to send to a translation service
|
||||
|
||||
aws-cfn:
|
||||
cd lifecycle/aws && npm i && $(UV) run npm run aws-cfn
|
||||
cd lifecycle/aws && npm i && uv run npm run aws-cfn
|
||||
|
||||
run: ## Run the main authentik server and worker processes
|
||||
$(UV) run ak allinone
|
||||
run-server: ## Run the main authentik server process
|
||||
uv run ak server
|
||||
|
||||
run-watch: ## Run the authentik server and worker, with auto reloading
|
||||
watchexec --on-busy-update=restart --stop-signal=SIGINT --exts py,rs,go --no-meta --notify -- $(UV) run ak allinone
|
||||
run-worker: ## Run the main authentik worker process
|
||||
uv run ak worker
|
||||
|
||||
core-i18n-extract:
|
||||
$(UV) run ak makemessages \
|
||||
uv run ak makemessages \
|
||||
--add-location file \
|
||||
--no-obsolete \
|
||||
--ignore web \
|
||||
--ignore internal \
|
||||
--ignore packages/client-ts \
|
||||
--ignore ${GEN_API_TS} \
|
||||
--ignore ${GEN_API_GO} \
|
||||
--ignore website \
|
||||
-l en
|
||||
|
||||
install: node-install docs-install core-install ## Install all requires dependencies for `node`, `docs` and `core`
|
||||
|
||||
dev-drop-db:
|
||||
$(eval pg_user := $(shell $(UV) run python -m authentik.lib.config postgresql.user 2>/dev/null))
|
||||
$(eval pg_host := $(shell $(UV) run python -m authentik.lib.config postgresql.host 2>/dev/null))
|
||||
$(eval pg_name := $(shell $(UV) run python -m authentik.lib.config postgresql.name 2>/dev/null))
|
||||
dropdb -U ${pg_user} -h ${pg_host} ${pg_name} || true
|
||||
# Also remove the test-db if it exists
|
||||
dropdb -U ${pg_user} -h ${pg_host} test_${pg_name} || true
|
||||
|
||||
dev-create-db:
|
||||
$(eval pg_user := $(shell $(UV) run python -m authentik.lib.config postgresql.user 2>/dev/null))
|
||||
$(eval pg_host := $(shell $(UV) run python -m authentik.lib.config postgresql.host 2>/dev/null))
|
||||
$(eval pg_name := $(shell $(UV) run python -m authentik.lib.config postgresql.name 2>/dev/null))
|
||||
createdb -U ${pg_user} -h ${pg_host} ${pg_name}
|
||||
|
||||
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
|
||||
|
||||
update-test-mmdb: ## Update test GeoIP and ASN Databases
|
||||
curl \
|
||||
-L \
|
||||
-o ${PWD}/tests/geoip/GeoLite2-ASN-Test.mmdb \
|
||||
https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb
|
||||
curl \
|
||||
-L \
|
||||
-o ${PWD}/tests/geoip/GeoLite2-City-Test.mmdb \
|
||||
https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb
|
||||
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb
|
||||
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb
|
||||
|
||||
bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
|
||||
ifndef version
|
||||
$(error Usage: make bump version=20xx.xx.xx )
|
||||
endif
|
||||
$(eval current_version := $(shell cat ${PWD}/internal/constants/VERSION))
|
||||
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' ${PWD}/pyproject.toml
|
||||
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' ${PWD}/authentik/__init__.py
|
||||
$(SED_INPLACE) "s/version = \"${current_version}\"/version = \"$(version)\"" ${PWD}/Cargo.toml ${PWD}/Cargo.lock
|
||||
sed -i 's/^version = ".*"/version = "$(version)"/' pyproject.toml
|
||||
sed -i 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
|
||||
$(MAKE) gen-build gen-compose aws-cfn
|
||||
$(SED_INPLACE) "s/\"${current_version}\"/\"$(version)\"/" ${PWD}/package.json ${PWD}/package-lock.json ${PWD}/web/package.json ${PWD}/web/package-lock.json
|
||||
npm version --no-git-tag-version --allow-same-version $(version)
|
||||
cd ${PWD}/web && npm version --no-git-tag-version --allow-same-version $(version)
|
||||
echo -n $(version) > ${PWD}/internal/constants/VERSION
|
||||
|
||||
#########################
|
||||
@@ -173,56 +134,84 @@ gen-build: ## Extract the schema from the database
|
||||
AUTHENTIK_DEBUG=true \
|
||||
AUTHENTIK_TENANTS__ENABLED=true \
|
||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||
$(UV) run ak build_schema
|
||||
uv run ak make_blueprint_schema --file blueprints/schema.json
|
||||
AUTHENTIK_DEBUG=true \
|
||||
AUTHENTIK_TENANTS__ENABLED=true \
|
||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||
uv run ak spectacular --file schema.yml
|
||||
|
||||
gen-compose:
|
||||
$(UV) run scripts/generate_compose.py
|
||||
uv run scripts/generate_docker_compose.py
|
||||
|
||||
gen-changelog: ## (Release) generate the changelog based from the commits since the last version
|
||||
# These are best-effort guesses based on commit messages
|
||||
$(eval last_version := $(shell git tag --list 'version/*' --sort 'version:refname' | grep -vE 'rc\d+$$' | tail -1))
|
||||
$(eval current_commit := $(shell git rev-parse HEAD))
|
||||
git log --pretty=format:"- %s" $(shell git merge-base ${last_version} ${current_commit})...${current_commit} > merged_to_current
|
||||
git log --pretty=format:"- %s" $(shell git merge-base ${last_version} ${current_commit})...${last_version} > merged_to_last
|
||||
grep -Eo 'cherry-pick (#\d+)' merged_to_last | cut -d ' ' -f 2 | sed 's/.*/(&)$$/' > cherry_picked_to_last
|
||||
grep -vf cherry_picked_to_last merged_to_current | sort > changelog.md
|
||||
rm merged_to_current
|
||||
rm merged_to_last
|
||||
rm cherry_picked_to_last
|
||||
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
|
||||
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
|
||||
npx prettier --write changelog.md
|
||||
|
||||
gen-diff: ## (Release) generate the changelog diff between the current schema and the last version
|
||||
$(eval last_version := $(shell git tag --list 'version/*' --sort 'version:refname' | grep -vE 'rc\d+$$' | tail -1))
|
||||
git show ${last_version}:schema.yml > schema-old.yml
|
||||
docker compose -f scripts/compose.yml run --rm --user "${UID}:${GID}" diff \
|
||||
gen-diff: ## (Release) generate the changelog diff between the current schema and the last tag
|
||||
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > schema-old.yml
|
||||
docker compose -f scripts/api/docker-compose.yml run --rm --user "${UID}:${GID}" diff \
|
||||
--markdown \
|
||||
/local/diff.md \
|
||||
/local/schema-old.yml \
|
||||
/local/schema.yml
|
||||
rm schema-old.yml
|
||||
$(SED_INPLACE) 's/{/{/g' diff.md
|
||||
$(SED_INPLACE) 's/}/}/g' diff.md
|
||||
sed -i 's/{/{/g' diff.md
|
||||
sed -i 's/}/}/g' diff.md
|
||||
npx prettier --write diff.md
|
||||
|
||||
gen-clean-ts: ## Remove generated API client for TypeScript
|
||||
rm -rf ${PWD}/${GEN_API_TS}/
|
||||
rm -rf ${PWD}/web/node_modules/@goauthentik/api/
|
||||
|
||||
gen-clean-py: ## Remove generated API client for Python
|
||||
rm -rf ${PWD}/${GEN_API_PY}
|
||||
|
||||
gen-clean-go: ## Remove generated API client for Go
|
||||
rm -rf ${PWD}/${GEN_API_GO}
|
||||
|
||||
gen-clean: gen-clean-ts gen-clean-go gen-clean-py ## Remove generated API clients
|
||||
|
||||
gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescript into the authentik UI Application
|
||||
docker compose -f scripts/api/docker-compose.yml run --rm --user "${UID}:${GID}" gen \
|
||||
generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/${GEN_API_TS} \
|
||||
-c /local/scripts/api/ts-config.yaml \
|
||||
--additional-properties=npmVersion=${NPM_VERSION} \
|
||||
--git-repo-id authentik \
|
||||
--git-user-id goauthentik
|
||||
|
||||
cd ${PWD}/${GEN_API_TS} && npm i
|
||||
cd ${PWD}/${GEN_API_TS} && npm link
|
||||
cd ${PWD}/web && npm link @goauthentik/api
|
||||
|
||||
gen-client-py: gen-clean-py ## Build and install the authentik API for Python
|
||||
mkdir -p ${PWD}/${GEN_API_PY}
|
||||
ifeq ($(wildcard ${PWD}/${GEN_API_PY}/.*),)
|
||||
git clone --depth 1 https://github.com/goauthentik/client-python.git ${PWD}/${GEN_API_PY}
|
||||
else
|
||||
cd ${PWD}/${GEN_API_PY} && git pull
|
||||
endif
|
||||
cp ${PWD}/schema.yml ${PWD}/${GEN_API_PY}
|
||||
make -C ${PWD}/${GEN_API_PY} build version=${NPM_VERSION}
|
||||
|
||||
gen-client-go: ## Build and install the authentik API for Golang
|
||||
$(UV) run make -C "${PWD}/packages/client-go" build
|
||||
|
||||
gen-client-rust: ## Build and install the authentik API for Rust
|
||||
$(UV) run make -C "${PWD}/packages/client-rust" build version=${NPM_VERSION}
|
||||
make lint-fix-rust
|
||||
|
||||
gen-client-ts: ## Build and install the authentik API for Typescript into the authentik UI Application
|
||||
make -C "${PWD}/packages/client-ts" build
|
||||
npm --prefix web install
|
||||
|
||||
_gen-clients: gen-client-go gen-client-rust gen-client-ts
|
||||
gen-clients: ## Build and install API clients used by authentik
|
||||
$(MAKE) _gen-clients -j
|
||||
|
||||
gen: gen-build gen-clients ## Build and install API schema and clients used by authentik
|
||||
mkdir -p ${PWD}/${GEN_API_GO}
|
||||
ifeq ($(wildcard ${PWD}/${GEN_API_GO}/.*),)
|
||||
git clone --depth 1 https://github.com/goauthentik/client-go.git ${PWD}/${GEN_API_GO}
|
||||
else
|
||||
cd ${PWD}/${GEN_API_GO} && git reset --hard
|
||||
cd ${PWD}/${GEN_API_GO} && git pull
|
||||
endif
|
||||
cp ${PWD}/schema.yml ${PWD}/${GEN_API_GO}
|
||||
make -C ${PWD}/${GEN_API_GO} build
|
||||
go mod edit -replace goauthentik.io/api/v3=./${GEN_API_GO}
|
||||
|
||||
gen-dev-config: ## Generate a local development config file
|
||||
$(UV) run scripts/generate_config.py
|
||||
uv run scripts/generate_config.py
|
||||
|
||||
gen: gen-build gen-client-ts
|
||||
|
||||
#########################
|
||||
## Node.js
|
||||
@@ -271,7 +260,7 @@ docs: docs-lint-fix docs-build ## Automatically fix formatting issues in the Au
|
||||
docs-install:
|
||||
npm ci --prefix website
|
||||
|
||||
docs-lint-fix: lint-spellcheck
|
||||
docs-lint-fix: lint-codespell
|
||||
npm run --prefix website prettier
|
||||
|
||||
docs-build:
|
||||
@@ -292,7 +281,7 @@ docs-api-build:
|
||||
npm run --prefix website -w api build
|
||||
|
||||
docs-api-watch: ## Build and watch the API documentation
|
||||
npm run --prefix website -w api generate
|
||||
npm run --prefix website -w api build:api
|
||||
npm run --prefix website -w api start
|
||||
|
||||
docs-api-clean: ## Clean generated API documentation
|
||||
@@ -303,7 +292,8 @@ docs-api-clean: ## Clean generated API documentation
|
||||
#########################
|
||||
|
||||
docker: ## Build a docker image of the current source tree
|
||||
DOCKER_BUILDKIT=1 docker build . -f lifecycle/container/Dockerfile --progress plain --tag ${DOCKER_IMAGE}
|
||||
mkdir -p ${GEN_API_TS}
|
||||
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
|
||||
|
||||
test-docker:
|
||||
BUILD=true ${PWD}/scripts/test_docker.sh
|
||||
@@ -315,42 +305,28 @@ test-docker:
|
||||
# which makes the YAML File a lot smaller
|
||||
|
||||
ci--meta-debug:
|
||||
$(UV) run python -V || echo "No python installed"
|
||||
$(CARGO) --version || echo "No rust installed"
|
||||
node --version || echo "No node installed"
|
||||
python -V
|
||||
node --version
|
||||
|
||||
ci-lint-mypy: ci--meta-debug
|
||||
$(UV) run mypy --strict $(PY_SOURCES)
|
||||
ci-mypy: ci--meta-debug
|
||||
uv run mypy --strict $(PY_SOURCES)
|
||||
|
||||
ci-lint-black: ci--meta-debug
|
||||
$(UV) run black --check $(PY_SOURCES)
|
||||
ci-black: ci--meta-debug
|
||||
uv run black --check $(PY_SOURCES)
|
||||
|
||||
ci-lint-ruff: ci--meta-debug
|
||||
$(UV) run ruff check $(PY_SOURCES)
|
||||
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
|
||||
$(UV) run bandit -c pyproject.toml -r $(PY_SOURCES) -iii
|
||||
ci-bandit: ci--meta-debug
|
||||
uv run bandit -r $(PY_SOURCES)
|
||||
|
||||
ci-lint-pending-migrations: ci--meta-debug
|
||||
$(UV) run ak makemigrations --check
|
||||
|
||||
ci-lint-cargo-deny: ci--meta-debug
|
||||
$(CARGO) deny --locked --workspace check --config "${PWD}/.cargo/deny.toml"
|
||||
|
||||
ci-lint-cargo-machete: ci--meta-debug
|
||||
$(CARGO) machete
|
||||
|
||||
ci-lint-rustfmt: ci--meta-debug
|
||||
$(CARGO) +nightly fmt --all --check -- --config-path "${PWD}/.cargo/rustfmt.toml"
|
||||
|
||||
ci-lint-clippy: ci--meta-debug
|
||||
$(CARGO) clippy --workspace -- -D warnings
|
||||
ci-pending-migrations: ci--meta-debug
|
||||
uv run ak makemigrations --check
|
||||
|
||||
ci-test: ci--meta-debug
|
||||
$(UV) run coverage run manage.py test --keepdb --parallel auto authentik
|
||||
$(UV) run coverage combine
|
||||
$(UV) run coverage report
|
||||
$(UV) run coverage xml
|
||||
uv run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
||||
uv run coverage report
|
||||
uv run coverage xml
|
||||
|
||||
42
SECURITY.md
42
SECURITY.md
@@ -18,10 +18,10 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
(.x being the latest patch release for each version)
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | --------- |
|
||||
| 2025.12.x | ✅ |
|
||||
| 2026.2.x | ✅ |
|
||||
| Version | Supported |
|
||||
| ---------- | ---------- |
|
||||
| 2025.8.x | ✅ |
|
||||
| 2025.10.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
@@ -60,40 +60,6 @@ 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.
|
||||
|
||||
- Outgoing network requests are not filtered.
|
||||
|
||||
The destinations of outgoing network requests (HTTP, TCP, etc.) made by authentik to configurable endpoints through objects such as OAuth Sources, SSO Providers, and others are not validated. Depending on your threat model, these requests should be restricted at the network level using appropriate firewall or network policies.
|
||||
|
||||
## 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 = "2025.12.0-rc1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.lib.api import Models
|
||||
from authentik.lib.utils.reflection import get_apps
|
||||
from authentik.policies.event_matcher.models import model_choices
|
||||
|
||||
|
||||
class AppSerializer(PassiveSerializer):
|
||||
@@ -42,6 +42,6 @@ class ModelViewSet(ViewSet):
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Read-only view list all installed models"""
|
||||
data = []
|
||||
for name, label in Models.choices:
|
||||
for name, label in model_choices():
|
||||
data.append({"name": name, "label": label})
|
||||
return Response(AppSerializer(data, many=True).data)
|
||||
|
||||
@@ -18,6 +18,7 @@ from rest_framework.views import APIView
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.reflection import get_env
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
@@ -25,15 +26,6 @@ from authentik.outposts.models import Outpost
|
||||
from authentik.rbac.permissions import HasPermission
|
||||
|
||||
|
||||
def fips_enabled():
|
||||
try:
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
|
||||
return backend._fips_enabled if LicenseKey.get_total().status().is_valid else None
|
||||
except ModuleNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
class RuntimeDict(TypedDict):
|
||||
"""Runtime information"""
|
||||
|
||||
@@ -88,7 +80,9 @@ class SystemInfoSerializer(PassiveSerializer):
|
||||
"architecture": platform.machine(),
|
||||
"authentik_version": authentik_full_version(),
|
||||
"environment": get_env(),
|
||||
"openssl_fips_enabled": fips_enabled(),
|
||||
"openssl_fips_enabled": (
|
||||
backend._fips_enabled if LicenseKey.get_total().status().is_valid else None
|
||||
),
|
||||
"openssl_version": OPENSSL_VERSION,
|
||||
"platform": platform.platform(),
|
||||
"python_version": python_version,
|
||||
|
||||
@@ -37,7 +37,7 @@ class VersionSerializer(PassiveSerializer):
|
||||
|
||||
def get_version_latest(self, _) -> str:
|
||||
"""Get latest version from cache"""
|
||||
if get_current_tenant().schema_name != get_public_schema_name():
|
||||
if get_current_tenant().schema_name == get_public_schema_name():
|
||||
return authentik_version()
|
||||
version_in_cache = cache.get(VERSION_CACHE_KEY)
|
||||
if not version_in_cache: # pragma: no cover
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import mimetypes
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.utils import extend_schema
|
||||
@@ -10,14 +12,13 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik.admin.files.backends.base import get_content_type
|
||||
from authentik.admin.files.fields import FileField as AkFileField
|
||||
from authentik.admin.files.manager import get_file_manager
|
||||
from authentik.admin.files.usage import FileApiUsage
|
||||
from authentik.admin.files.validation import validate_upload_file_name
|
||||
from authentik.api.validation import validate
|
||||
from authentik.core.api.used_by import DeleteAction, UsedBySerializer
|
||||
from authentik.core.api.utils import PassiveSerializer, ThemedUrlsSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.utils.reflection import get_apps
|
||||
from authentik.rbac.permissions import HasPermission
|
||||
@@ -25,6 +26,11 @@ from authentik.rbac.permissions import HasPermission
|
||||
MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024 # 25MB
|
||||
|
||||
|
||||
def get_mime_from_filename(filename: str) -> str:
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
return mime_type or "application/octet-stream"
|
||||
|
||||
|
||||
class FileView(APIView):
|
||||
pagination_class = None
|
||||
parser_classes = [MultiPartParser]
|
||||
@@ -47,7 +53,6 @@ class FileView(APIView):
|
||||
name = CharField()
|
||||
mime_type = CharField()
|
||||
url = CharField()
|
||||
themed_urls = ThemedUrlsSerializer(required=False, allow_null=True)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[FileListParameters],
|
||||
@@ -75,9 +80,8 @@ class FileView(APIView):
|
||||
FileView.FileListSerializer(
|
||||
data={
|
||||
"name": file,
|
||||
"url": manager.file_url(file, request),
|
||||
"mime_type": get_content_type(file),
|
||||
"themed_urls": manager.themed_urls(file, request),
|
||||
"url": manager.file_url(file),
|
||||
"mime_type": get_mime_from_filename(file),
|
||||
}
|
||||
)
|
||||
for file in files
|
||||
@@ -146,7 +150,7 @@ class FileView(APIView):
|
||||
"pk": name,
|
||||
"name": name,
|
||||
"usage": usage.value,
|
||||
"mime_type": get_content_type(name),
|
||||
"mime_type": get_mime_from_filename(name),
|
||||
},
|
||||
).from_http(request)
|
||||
|
||||
@@ -236,9 +240,7 @@ class FileUsedByView(APIView):
|
||||
for field in fields:
|
||||
q |= Q(**{field: params.get("name")})
|
||||
|
||||
objs = get_objects_for_user(
|
||||
request.user, f"{app}.view_{model_name}", model.objects.all()
|
||||
)
|
||||
objs = get_objects_for_user(request.user, f"{app}.view_{model_name}", model)
|
||||
objs = objs.filter(q)
|
||||
for obj in objs:
|
||||
serializer = UsedBySerializer(
|
||||
|
||||
@@ -1,42 +1,12 @@
|
||||
import mimetypes
|
||||
from collections.abc import Callable, Generator, Iterator
|
||||
from typing import cast
|
||||
from collections.abc import Generator, Iterator
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.http.request import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
|
||||
CACHE_PREFIX = "goauthentik.io/admin/files"
|
||||
LOGGER = get_logger()
|
||||
|
||||
# Theme variable placeholder for theme-specific files like logo-%(theme)s.png
|
||||
THEME_VARIABLE = "%(theme)s"
|
||||
|
||||
|
||||
def get_content_type(name: str) -> str:
|
||||
"""Get MIME type for a file based on its extension."""
|
||||
content_type, _ = mimetypes.guess_type(name)
|
||||
return content_type or "application/octet-stream"
|
||||
|
||||
|
||||
def get_valid_themes() -> list[str]:
|
||||
"""Get valid themes that can be substituted for %(theme)s."""
|
||||
from authentik.brands.api import Themes
|
||||
|
||||
return [t.value for t in Themes if t != Themes.AUTOMATIC]
|
||||
|
||||
|
||||
def has_theme_variable(name: str) -> bool:
|
||||
"""Check if filename contains %(theme)s variable."""
|
||||
return THEME_VARIABLE in name
|
||||
|
||||
|
||||
def substitute_theme(name: str, theme: str) -> str:
|
||||
"""Replace %(theme)s with the given theme."""
|
||||
return name.replace(THEME_VARIABLE, theme)
|
||||
|
||||
|
||||
class Backend:
|
||||
"""
|
||||
@@ -83,49 +53,19 @@ class Backend:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def file_url(
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> str:
|
||||
def file_url(self, name: str, request: HttpRequest | None = None) -> str:
|
||||
"""
|
||||
Get URL for accessing the file.
|
||||
|
||||
Args:
|
||||
file_path: Relative file path
|
||||
request: Optional Django HttpRequest for fully qualified URL building
|
||||
use_cache: whether to retrieve the URL from cache
|
||||
request: Optional Django HttpRequest for fully qualifed URL building
|
||||
|
||||
Returns:
|
||||
URL to access the file (may be relative or absolute depending on backend)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def themed_urls(
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> dict[str, str] | None:
|
||||
"""
|
||||
Get URLs for each theme variant when filename contains %(theme)s.
|
||||
|
||||
Args:
|
||||
name: File path potentially containing %(theme)s
|
||||
request: Optional Django HttpRequest for URL building
|
||||
|
||||
Returns:
|
||||
Dict mapping theme to URL if %(theme)s present, None otherwise
|
||||
"""
|
||||
if not has_theme_variable(name):
|
||||
return None
|
||||
|
||||
return {
|
||||
theme: self.file_url(substitute_theme(name, theme), request, use_cache=use_cache)
|
||||
for theme in get_valid_themes()
|
||||
}
|
||||
|
||||
|
||||
class ManageableBackend(Backend):
|
||||
"""
|
||||
@@ -192,22 +132,3 @@ class ManageableBackend(Backend):
|
||||
True if file exists, False otherwise
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _cache_get_or_set(
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None,
|
||||
default: Callable[[str, HttpRequest | None], str],
|
||||
timeout: int,
|
||||
) -> str:
|
||||
timeout_ignore = 60
|
||||
timeout = int(timeout * 0.67)
|
||||
if timeout < timeout_ignore:
|
||||
timeout = 0
|
||||
|
||||
request_key = "None"
|
||||
if request is not None:
|
||||
request_key = f"{request.build_absolute_uri('/')}"
|
||||
cache_key = f"{CACHE_PREFIX}/{self.name}/{self.usage}/{request_key}/{name}"
|
||||
|
||||
return cast(str, cache.get_or_set(cache_key, lambda: default(name, request), timeout))
|
||||
|
||||
@@ -45,13 +45,8 @@ class FileBackend(ManageableBackend):
|
||||
|
||||
@property
|
||||
def manageable(self) -> bool:
|
||||
# Check _base_dir (the mount point, e.g. /data) rather than base_path
|
||||
# (which includes usage/schema subdirs, e.g. /data/media/public).
|
||||
# The subdirectories are created on first file write via mkdir(parents=True)
|
||||
# in save_file(), so requiring them to exist beforehand would prevent
|
||||
# file creation on fresh installs.
|
||||
return (
|
||||
self._base_dir.exists()
|
||||
self.base_path.exists()
|
||||
and (self._base_dir.is_mount() or (self._base_dir / self.usage.value).is_mount())
|
||||
or (settings.DEBUG or settings.TEST)
|
||||
)
|
||||
@@ -68,12 +63,7 @@ class FileBackend(ManageableBackend):
|
||||
rel_path = full_path.relative_to(self.base_path)
|
||||
yield str(rel_path)
|
||||
|
||||
def file_url(
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> str:
|
||||
def file_url(self, name: str, request: HttpRequest | None = None) -> str:
|
||||
"""Get URL for accessing the file."""
|
||||
expires_in = timedelta_from_string(
|
||||
CONFIG.get(
|
||||
@@ -82,28 +72,21 @@ class FileBackend(ManageableBackend):
|
||||
)
|
||||
)
|
||||
|
||||
def _file_url(name: str, request: HttpRequest | None) -> str:
|
||||
prefix = CONFIG.get("web.path", "/")[:-1]
|
||||
path = f"{self.usage.value}/{connection.schema_name}/{name}"
|
||||
token = jwt.encode(
|
||||
payload={
|
||||
"path": path,
|
||||
"exp": now() + expires_in,
|
||||
"nbf": now() - timedelta(seconds=15),
|
||||
},
|
||||
key=sha256(f"{settings.SECRET_KEY}:{self.usage}".encode()).hexdigest(),
|
||||
algorithm="HS256",
|
||||
)
|
||||
url = f"{prefix}/files/{path}?token={token}"
|
||||
if request is None:
|
||||
return url
|
||||
return request.build_absolute_uri(url)
|
||||
|
||||
if use_cache:
|
||||
timeout = int(expires_in.total_seconds())
|
||||
return self._cache_get_or_set(name, request, _file_url, timeout)
|
||||
else:
|
||||
return _file_url(name, request)
|
||||
prefix = CONFIG.get("web.path", "/")[:-1]
|
||||
path = f"{self.usage.value}/{connection.schema_name}/{name}"
|
||||
token = jwt.encode(
|
||||
payload={
|
||||
"path": path,
|
||||
"exp": now() + expires_in,
|
||||
"nbf": now() - timedelta(seconds=15),
|
||||
},
|
||||
key=sha256(f"{settings.SECRET_KEY}:{self.usage}".encode()).hexdigest(),
|
||||
algorithm="HS256",
|
||||
)
|
||||
url = f"{prefix}/files/{path}?token={token}"
|
||||
if request is None:
|
||||
return url
|
||||
return request.build_absolute_uri(url)
|
||||
|
||||
def save_file(self, name: str, content: bytes) -> None:
|
||||
"""Save file to local filesystem."""
|
||||
|
||||
@@ -38,34 +38,6 @@ class PassthroughBackend(Backend):
|
||||
"""External files cannot be listed."""
|
||||
yield from []
|
||||
|
||||
def file_url(
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> str:
|
||||
def file_url(self, name: str, request: HttpRequest | None = None) -> str:
|
||||
"""Return the URL as-is for passthrough files."""
|
||||
return name
|
||||
|
||||
def themed_urls(
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> dict[str, str] | None:
|
||||
"""Support themed URLs for external URLs with %(theme)s placeholder.
|
||||
|
||||
If the external URL contains %(theme)s, substitute it for each theme.
|
||||
We can't verify that themed variants exist at the external location,
|
||||
but we trust the user to provide valid URLs.
|
||||
"""
|
||||
from authentik.admin.files.backends.base import (
|
||||
get_valid_themes,
|
||||
has_theme_variable,
|
||||
substitute_theme,
|
||||
)
|
||||
|
||||
if not has_theme_variable(name):
|
||||
return None
|
||||
|
||||
return {theme: substitute_theme(name, theme) for theme in get_valid_themes()}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections.abc import Generator, Iterator
|
||||
from contextlib import contextmanager
|
||||
from tempfile import SpooledTemporaryFile
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
@@ -9,7 +9,7 @@ from botocore.exceptions import ClientError
|
||||
from django.db import connection
|
||||
from django.http.request import HttpRequest
|
||||
|
||||
from authentik.admin.files.backends.base import ManageableBackend, get_content_type
|
||||
from authentik.admin.files.backends.base import ManageableBackend
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
@@ -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
|
||||
@@ -142,87 +130,44 @@ class S3Backend(ManageableBackend):
|
||||
if rel_path: # Skip if it's just the directory itself
|
||||
yield rel_path
|
||||
|
||||
def file_url(
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> str:
|
||||
def file_url(self, name: str, request: HttpRequest | None = None) -> str:
|
||||
"""Generate presigned URL for file access."""
|
||||
use_https = CONFIG.get_bool(
|
||||
f"storage.{self.usage.value}.{self.name}.secure_urls",
|
||||
CONFIG.get_bool(f"storage.{self.name}.secure_urls", True),
|
||||
)
|
||||
|
||||
expires_in = int(
|
||||
timedelta_from_string(
|
||||
CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.url_expiry",
|
||||
CONFIG.get(f"storage.{self.name}.url_expiry", "minutes=15"),
|
||||
)
|
||||
).total_seconds()
|
||||
params = {
|
||||
"Bucket": self.bucket_name,
|
||||
"Key": f"{self.base_path}/{name}",
|
||||
}
|
||||
|
||||
expires_in = timedelta_from_string(
|
||||
CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.url_expiry",
|
||||
CONFIG.get(f"storage.{self.name}.url_expiry", "minutes=15"),
|
||||
)
|
||||
)
|
||||
|
||||
def _file_url(name: str, request: HttpRequest | None) -> str:
|
||||
client = self.client
|
||||
params = {
|
||||
"Bucket": self.bucket_name,
|
||||
"Key": f"{self.base_path}/{name}",
|
||||
}
|
||||
url = self.client.generate_presigned_url(
|
||||
"get_object",
|
||||
Params=params,
|
||||
ExpiresIn=expires_in.total_seconds(),
|
||||
HttpMethod="GET",
|
||||
)
|
||||
|
||||
operation_name = "GetObject"
|
||||
operation_model = client.meta.service_model.operation_model(operation_name)
|
||||
request_dict = client._convert_to_request_dict(
|
||||
params,
|
||||
operation_model,
|
||||
endpoint_url=client.meta.endpoint_url,
|
||||
context={"is_presign_request": True},
|
||||
)
|
||||
# Support custom domain for S3-compatible storage (so not AWS)
|
||||
# Well, can't you do custom domains on AWS as well?
|
||||
custom_domain = CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.custom_domain",
|
||||
CONFIG.get(f"storage.{self.name}.custom_domain", None),
|
||||
)
|
||||
if custom_domain:
|
||||
parsed = urlsplit(url)
|
||||
scheme = "https" if use_https else "http"
|
||||
url = f"{scheme}://{custom_domain}{parsed.path}?{parsed.query}"
|
||||
|
||||
# Support custom domain for S3-compatible storage (so not AWS)
|
||||
# Well, can't you do custom domains on AWS as well?
|
||||
custom_domain = CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.custom_domain",
|
||||
CONFIG.get(f"storage.{self.name}.custom_domain", None),
|
||||
)
|
||||
if custom_domain:
|
||||
scheme = "https" if use_https else "http"
|
||||
path = request_dict["url_path"]
|
||||
|
||||
# When using path-style addressing, the presigned URL contains the bucket
|
||||
# name in the path (e.g., /bucket-name/key). Since custom_domain must
|
||||
# include the bucket name (per docs), strip it from the path to avoid
|
||||
# duplication. See: https://github.com/goauthentik/authentik/issues/19521
|
||||
# Check with trailing slash to ensure exact bucket name match
|
||||
if path.startswith(f"/{self.bucket_name}/"):
|
||||
path = path.removeprefix(f"/{self.bucket_name}")
|
||||
|
||||
# Normalize to avoid double slashes
|
||||
custom_domain = custom_domain.rstrip("/")
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
|
||||
custom_base = urlsplit(f"{scheme}://{custom_domain}")
|
||||
|
||||
# Sign the final public URL instead of signing the internal S3 endpoint and
|
||||
# rewriting it afterwards. Presigned SigV4 URLs include the host header in the
|
||||
# canonical request, so post-sign host changes break strict backends like RustFS.
|
||||
public_path = f"{custom_base.path.rstrip('/')}{path}" if custom_base.path else path
|
||||
request_dict["url_path"] = public_path
|
||||
request_dict["url"] = urlunsplit(
|
||||
(custom_base.scheme, custom_base.netloc, public_path, "", "")
|
||||
)
|
||||
|
||||
return client._request_signer.generate_presigned_url(
|
||||
request_dict,
|
||||
operation_name,
|
||||
expires_in=expires_in,
|
||||
)
|
||||
|
||||
if use_cache:
|
||||
return self._cache_get_or_set(name, request, _file_url, expires_in)
|
||||
else:
|
||||
return _file_url(name, request)
|
||||
return url
|
||||
|
||||
def save_file(self, name: str, content: bytes) -> None:
|
||||
"""Save file to S3."""
|
||||
@@ -231,7 +176,6 @@ class S3Backend(ManageableBackend):
|
||||
Key=f"{self.base_path}/{name}",
|
||||
Body=content,
|
||||
ACL="private",
|
||||
ContentType=get_content_type(name),
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
@@ -247,7 +191,6 @@ class S3Backend(ManageableBackend):
|
||||
Key=f"{self.base_path}/{name}",
|
||||
ExtraArgs={
|
||||
"ACL": "private",
|
||||
"ContentType": get_content_type(name),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -44,12 +44,7 @@ class StaticBackend(Backend):
|
||||
if file_path.is_file() and (file_path.suffix in STATIC_FILE_EXTENSIONS):
|
||||
yield f"{STATIC_PATH_PREFIX}/dist/{dir}/{file_path.name}"
|
||||
|
||||
def file_url(
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> str:
|
||||
def file_url(self, name: str, request: HttpRequest | None = None) -> str:
|
||||
"""Get URL for static file."""
|
||||
prefix = CONFIG.get("web.path", "/")[:-1]
|
||||
url = f"{prefix}{name}"
|
||||
|
||||
@@ -165,31 +165,3 @@ class TestFileBackend(FileTestFileBackendMixin, TestCase):
|
||||
def test_file_exists_false(self):
|
||||
"""Test file_exists returns False for nonexistent file"""
|
||||
self.assertFalse(self.backend.file_exists("does_not_exist.txt"))
|
||||
|
||||
def test_themed_urls_without_theme_variable(self):
|
||||
"""Test themed_urls returns None when filename has no %(theme)s"""
|
||||
file_name = "logo.png"
|
||||
result = self.backend.themed_urls(file_name)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_themed_urls_with_theme_variable(self):
|
||||
"""Test themed_urls returns dict of URLs for each theme"""
|
||||
file_name = "logo-%(theme)s.png"
|
||||
result = self.backend.themed_urls(file_name)
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("light", result)
|
||||
self.assertIn("dark", result)
|
||||
|
||||
# Check URLs contain the substituted theme
|
||||
self.assertIn("logo-light.png", result["light"])
|
||||
self.assertIn("logo-dark.png", result["dark"])
|
||||
|
||||
def test_themed_urls_multiple_theme_variables(self):
|
||||
"""Test themed_urls with multiple %(theme)s in path"""
|
||||
file_name = "%(theme)s/logo-%(theme)s.svg"
|
||||
result = self.backend.themed_urls(file_name)
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("light/logo-light.svg", result["light"])
|
||||
self.assertIn("dark/logo-dark.svg", result["dark"])
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
from unittest import skipUnless
|
||||
from urllib.parse import parse_qs, urlsplit
|
||||
|
||||
from botocore.exceptions import UnsupportedSignatureVersionError
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.admin.files.tests.utils import FileTestS3BackendMixin, s3_test_server_available
|
||||
from authentik.admin.files.tests.utils import FileTestS3BackendMixin
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
@skipUnless(s3_test_server_available(), "S3 test server not available")
|
||||
class TestS3Backend(FileTestS3BackendMixin, TestCase):
|
||||
"""Test S3 backend functionality"""
|
||||
|
||||
@@ -83,27 +78,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"""
|
||||
@@ -133,144 +107,3 @@ class TestS3Backend(FileTestS3BackendMixin, TestCase):
|
||||
"""Test S3Backend with REPORTS usage"""
|
||||
self.assertEqual(self.reports_s3_backend.usage, FileUsage.REPORTS)
|
||||
self.assertEqual(self.reports_s3_backend.base_path, "reports/public")
|
||||
|
||||
@CONFIG.patch("storage.s3.secure_urls", True)
|
||||
@CONFIG.patch("storage.s3.addressing_style", "path")
|
||||
def test_file_url_custom_domain_with_bucket_no_duplicate(self):
|
||||
"""Test file_url doesn't duplicate bucket name when custom_domain includes bucket.
|
||||
|
||||
Regression test for https://github.com/goauthentik/authentik/issues/19521
|
||||
|
||||
When using:
|
||||
- Path-style addressing (bucket name goes in URL path, not subdomain)
|
||||
- Custom domain that includes the bucket name (e.g., s3.example.com/bucket-name)
|
||||
|
||||
The bucket name should NOT appear twice in the final URL.
|
||||
|
||||
Example of the bug:
|
||||
- custom_domain = "s3.example.com/authentik-media"
|
||||
- boto3 presigned URL = "http://s3.example.com/authentik-media/media/public/file.png?..."
|
||||
- Buggy result = "https://s3.example.com/authentik-media/authentik-media/media/public/file.png?..."
|
||||
"""
|
||||
bucket_name = self.media_s3_bucket_name
|
||||
|
||||
# Custom domain includes the bucket name
|
||||
custom_domain = f"localhost:8020/{bucket_name}"
|
||||
|
||||
with CONFIG.patch("storage.media.s3.custom_domain", custom_domain):
|
||||
url = self.media_s3_backend.file_url("application-icons/test.svg", use_cache=False)
|
||||
|
||||
# The bucket name should appear exactly once in the URL path, not twice
|
||||
bucket_occurrences = url.count(bucket_name)
|
||||
self.assertEqual(
|
||||
bucket_occurrences,
|
||||
1,
|
||||
f"Bucket name '{bucket_name}' appears {bucket_occurrences} times in URL, expected 1. "
|
||||
f"URL: {url}",
|
||||
)
|
||||
|
||||
@CONFIG.patch("storage.s3.secure_urls", False)
|
||||
@CONFIG.patch("storage.s3.addressing_style", "path")
|
||||
def test_file_url_custom_domain_resigns_for_custom_host(self):
|
||||
"""Test presigned URLs are signed for the custom domain host.
|
||||
|
||||
Host-changing custom domains must produce a signature query string for
|
||||
the public host, not reuse the internal endpoint signature.
|
||||
"""
|
||||
bucket_name = self.media_s3_bucket_name
|
||||
key_name = "application-icons/test.svg"
|
||||
custom_domain = f"files.example.test:8020/{bucket_name}"
|
||||
|
||||
endpoint_signed_url = self.media_s3_backend.client.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={
|
||||
"Bucket": bucket_name,
|
||||
"Key": f"{self.media_s3_backend.base_path}/{key_name}",
|
||||
},
|
||||
ExpiresIn=900,
|
||||
HttpMethod="GET",
|
||||
)
|
||||
|
||||
with CONFIG.patch("storage.media.s3.custom_domain", custom_domain):
|
||||
custom_url = self.media_s3_backend.file_url(key_name, use_cache=False)
|
||||
|
||||
endpoint_parts = urlsplit(endpoint_signed_url)
|
||||
custom_parts = urlsplit(custom_url)
|
||||
|
||||
self.assertEqual(custom_parts.scheme, "http")
|
||||
self.assertEqual(custom_parts.netloc, "files.example.test:8020")
|
||||
self.assertEqual(parse_qs(custom_parts.query)["X-Amz-SignedHeaders"], ["host"])
|
||||
self.assertNotEqual(
|
||||
custom_parts.query,
|
||||
endpoint_parts.query,
|
||||
"Custom-domain URLs must be signed for the public host, not reuse the endpoint "
|
||||
"signature query string.",
|
||||
)
|
||||
|
||||
def test_themed_urls_without_theme_variable(self):
|
||||
"""Test themed_urls returns None when filename has no %(theme)s"""
|
||||
result = self.media_s3_backend.themed_urls("logo.png")
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_themed_urls_with_theme_variable(self):
|
||||
"""Test themed_urls returns dict of presigned URLs for each theme"""
|
||||
result = self.media_s3_backend.themed_urls("logo-%(theme)s.png")
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("light", result)
|
||||
self.assertIn("dark", result)
|
||||
|
||||
# Check URLs are valid presigned URLs with correct file paths
|
||||
self.assertIn("logo-light.png", result["light"])
|
||||
self.assertIn("logo-dark.png", result["dark"])
|
||||
self.assertIn("X-Amz-Signature=", result["light"])
|
||||
self.assertIn("X-Amz-Signature=", result["dark"])
|
||||
|
||||
def test_themed_urls_multiple_theme_variables(self):
|
||||
"""Test themed_urls with multiple %(theme)s in path"""
|
||||
result = self.media_s3_backend.themed_urls("%(theme)s/logo-%(theme)s.svg")
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("light/logo-light.svg", result["light"])
|
||||
self.assertIn("dark/logo-dark.svg", result["dark"])
|
||||
|
||||
def test_save_file_sets_content_type_svg(self):
|
||||
"""Test save_file sets correct ContentType for SVG files"""
|
||||
self.media_s3_backend.save_file("test.svg", b"<svg></svg>")
|
||||
|
||||
response = self.media_s3_backend.client.head_object(
|
||||
Bucket=self.media_s3_bucket_name,
|
||||
Key="media/public/test.svg",
|
||||
)
|
||||
self.assertEqual(response["ContentType"], "image/svg+xml")
|
||||
|
||||
def test_save_file_sets_content_type_png(self):
|
||||
"""Test save_file sets correct ContentType for PNG files"""
|
||||
self.media_s3_backend.save_file("test.png", b"\x89PNG\r\n\x1a\n")
|
||||
|
||||
response = self.media_s3_backend.client.head_object(
|
||||
Bucket=self.media_s3_bucket_name,
|
||||
Key="media/public/test.png",
|
||||
)
|
||||
self.assertEqual(response["ContentType"], "image/png")
|
||||
|
||||
def test_save_file_stream_sets_content_type(self):
|
||||
"""Test save_file_stream sets correct ContentType"""
|
||||
with self.media_s3_backend.save_file_stream("test.css") as f:
|
||||
f.write(b"body { color: red; }")
|
||||
|
||||
response = self.media_s3_backend.client.head_object(
|
||||
Bucket=self.media_s3_bucket_name,
|
||||
Key="media/public/test.css",
|
||||
)
|
||||
self.assertEqual(response["ContentType"], "text/css")
|
||||
|
||||
def test_save_file_unknown_extension_octet_stream(self):
|
||||
"""Test save_file sets octet-stream for unknown extensions"""
|
||||
self.media_s3_backend.save_file("test.unknownext123", b"data")
|
||||
|
||||
response = self.media_s3_backend.client.head_object(
|
||||
Bucket=self.media_s3_bucket_name,
|
||||
Key="media/public/test.unknownext123",
|
||||
)
|
||||
self.assertEqual(response["ContentType"], "application/octet-stream")
|
||||
|
||||
@@ -70,14 +70,9 @@ class FileManager:
|
||||
self,
|
||||
name: str | None,
|
||||
request: HttpRequest | Request | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Get URL for accessing the file.
|
||||
|
||||
Set ``use_cache=False`` when the caller needs a fresh signed URL instead
|
||||
of a cached one, for example when serializing flow/login payloads that
|
||||
may be refreshed after the previous JWT has expired.
|
||||
"""
|
||||
if not name:
|
||||
return ""
|
||||
@@ -87,37 +82,11 @@ class FileManager:
|
||||
|
||||
for backend in self.backends:
|
||||
if backend.supports_file(name):
|
||||
return backend.file_url(name, request, use_cache=use_cache)
|
||||
return backend.file_url(name, request)
|
||||
|
||||
LOGGER.warning(f"Could not find file backend for file: {name}")
|
||||
return ""
|
||||
|
||||
def themed_urls(
|
||||
self,
|
||||
name: str | None,
|
||||
request: HttpRequest | Request | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> dict[str, str] | None:
|
||||
"""
|
||||
Get URLs for each theme variant when filename contains %(theme)s.
|
||||
|
||||
``use_cache`` has the same semantics as ``file_url()`` and allows
|
||||
callers to force regeneration of expiring signed URLs.
|
||||
|
||||
Returns dict mapping theme to URL if %(theme)s present, None otherwise.
|
||||
"""
|
||||
if not name:
|
||||
return None
|
||||
|
||||
if isinstance(request, Request):
|
||||
request = request._request
|
||||
|
||||
for backend in self.backends:
|
||||
if backend.supports_file(name):
|
||||
return backend.themed_urls(name, request, use_cache=use_cache)
|
||||
|
||||
return None
|
||||
|
||||
def _check_manageable(self) -> None:
|
||||
if not self.manageable:
|
||||
raise ImproperlyConfigured("No file management backend configured.")
|
||||
|
||||
@@ -5,6 +5,7 @@ from io import BytesIO
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.admin.files.api import get_mime_from_filename
|
||||
from authentik.admin.files.manager import FileManager
|
||||
from authentik.admin.files.tests.utils import FileTestFileBackendMixin
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
@@ -93,9 +94,8 @@ class TestFileAPI(FileTestFileBackendMixin, TestCase):
|
||||
self.assertIn(
|
||||
{
|
||||
"name": "/static/authentik/sources/ldap.png",
|
||||
"url": "http://testserver/static/authentik/sources/ldap.png",
|
||||
"url": "/static/authentik/sources/ldap.png",
|
||||
"mime_type": "image/png",
|
||||
"themed_urls": None,
|
||||
},
|
||||
response.data,
|
||||
)
|
||||
@@ -129,9 +129,8 @@ class TestFileAPI(FileTestFileBackendMixin, TestCase):
|
||||
self.assertIn(
|
||||
{
|
||||
"name": "/static/authentik/sources/ldap.png",
|
||||
"url": "http://testserver/static/authentik/sources/ldap.png",
|
||||
"url": "/static/authentik/sources/ldap.png",
|
||||
"mime_type": "image/png",
|
||||
"themed_urls": None,
|
||||
},
|
||||
response.data,
|
||||
)
|
||||
@@ -201,64 +200,30 @@ class TestFileAPI(FileTestFileBackendMixin, TestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("field is required", str(response.data))
|
||||
|
||||
def test_list_files_includes_themed_urls_none(self):
|
||||
"""Test listing files includes themed_urls as None for non-themed files"""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
file_name = "test-no-theme.png"
|
||||
manager.save_file(file_name, b"test content")
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:files", query={"search": file_name, "manageableOnly": "true"})
|
||||
)
|
||||
class TestGetMimeFromFilename(TestCase):
|
||||
"""Test get_mime_from_filename function"""
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
file_entry = next((f for f in response.data if f["name"] == file_name), None)
|
||||
self.assertIsNotNone(file_entry)
|
||||
self.assertIn("themed_urls", file_entry)
|
||||
self.assertIsNone(file_entry["themed_urls"])
|
||||
def test_image_png(self):
|
||||
"""Test PNG image MIME type"""
|
||||
self.assertEqual(get_mime_from_filename("test.png"), "image/png")
|
||||
|
||||
manager.delete_file(file_name)
|
||||
def test_image_jpeg(self):
|
||||
"""Test JPEG image MIME type"""
|
||||
self.assertEqual(get_mime_from_filename("test.jpg"), "image/jpeg")
|
||||
|
||||
def test_list_files_includes_themed_urls_dict(self):
|
||||
"""Test listing files includes themed_urls as dict for themed files"""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
file_name = "logo-%(theme)s.svg"
|
||||
manager.save_file("logo-light.svg", b"<svg>light</svg>")
|
||||
manager.save_file("logo-dark.svg", b"<svg>dark</svg>")
|
||||
manager.save_file(file_name, b"<svg>placeholder</svg>")
|
||||
def test_image_svg(self):
|
||||
"""Test SVG image MIME type"""
|
||||
self.assertEqual(get_mime_from_filename("test.svg"), "image/svg+xml")
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:files", query={"search": "%(theme)s", "manageableOnly": "true"})
|
||||
)
|
||||
def test_text_plain(self):
|
||||
"""Test text file MIME type"""
|
||||
self.assertEqual(get_mime_from_filename("test.txt"), "text/plain")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
file_entry = next((f for f in response.data if f["name"] == file_name), None)
|
||||
self.assertIsNotNone(file_entry)
|
||||
self.assertIn("themed_urls", file_entry)
|
||||
self.assertIsInstance(file_entry["themed_urls"], dict)
|
||||
self.assertIn("light", file_entry["themed_urls"])
|
||||
self.assertIn("dark", file_entry["themed_urls"])
|
||||
def test_unknown_extension(self):
|
||||
"""Test unknown extension returns octet-stream"""
|
||||
self.assertEqual(get_mime_from_filename("test.unknown"), "application/octet-stream")
|
||||
|
||||
manager.delete_file(file_name)
|
||||
manager.delete_file("logo-light.svg")
|
||||
manager.delete_file("logo-dark.svg")
|
||||
|
||||
def test_upload_file_with_theme_variable(self):
|
||||
"""Test uploading file with %(theme)s in name"""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
file_name = "brand-logo-%(theme)s.svg"
|
||||
file_content = b"<svg></svg>"
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:files"),
|
||||
{
|
||||
"file": BytesIO(file_content),
|
||||
"name": file_name,
|
||||
"usage": FileUsage.MEDIA.value,
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(manager.file_exists(file_name))
|
||||
manager.delete_file(file_name)
|
||||
def test_no_extension(self):
|
||||
"""Test no extension returns octet-stream"""
|
||||
self.assertEqual(get_mime_from_filename("test"), "application/octet-stream")
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
"""Test file service layer"""
|
||||
|
||||
from unittest import skipUnless
|
||||
from unittest.mock import Mock
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.admin.files.manager import FileManager
|
||||
from authentik.admin.files.tests.utils import (
|
||||
FileTestFileBackendMixin,
|
||||
FileTestS3BackendMixin,
|
||||
s3_test_server_available,
|
||||
)
|
||||
from authentik.admin.files.tests.utils import FileTestFileBackendMixin, FileTestS3BackendMixin
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
@@ -54,19 +46,6 @@ class TestResolveFileUrlBasic(TestCase):
|
||||
result = manager.file_url("/static/authentik/sources/icon.svg")
|
||||
self.assertEqual(result, "/static/authentik/sources/icon.svg")
|
||||
|
||||
def test_file_url_forwards_use_cache(self):
|
||||
"""Test file_url forwards use_cache to backend."""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
backend = Mock()
|
||||
backend.supports_file.return_value = True
|
||||
backend.file_url.return_value = "/files/media/public/test.png?token=fresh"
|
||||
manager.backends = [backend]
|
||||
|
||||
result = manager.file_url("test.png", use_cache=False)
|
||||
|
||||
self.assertEqual(result, "/files/media/public/test.png?token=fresh")
|
||||
backend.file_url.assert_called_once_with("test.png", None, use_cache=False)
|
||||
|
||||
|
||||
class TestResolveFileUrlFileBackend(FileTestFileBackendMixin, TestCase):
|
||||
def test_resolve_storage_file(self):
|
||||
@@ -102,7 +81,6 @@ class TestResolveFileUrlFileBackend(FileTestFileBackendMixin, TestCase):
|
||||
self.assertEqual(result, "http://example.com/files/media/public/test.png")
|
||||
|
||||
|
||||
@skipUnless(s3_test_server_available(), "S3 test server not available")
|
||||
class TestResolveFileUrlS3Backend(FileTestS3BackendMixin, TestCase):
|
||||
@CONFIG.patch("storage.media.s3.custom_domain", "s3.test:8080/test")
|
||||
@CONFIG.patch("storage.media.s3.secure_urls", False)
|
||||
@@ -119,71 +97,3 @@ class TestResolveFileUrlS3Backend(FileTestS3BackendMixin, TestCase):
|
||||
|
||||
# S3 URLs should be returned as-is (already absolute)
|
||||
self.assertTrue(result.startswith("http://s3.test:8080/test"))
|
||||
|
||||
|
||||
class TestThemedUrls(FileTestFileBackendMixin, TestCase):
|
||||
"""Test FileManager.themed_urls method"""
|
||||
|
||||
def test_themed_urls_none_path(self):
|
||||
"""Test themed_urls returns None for None path"""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
result = manager.themed_urls(None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_themed_urls_empty_path(self):
|
||||
"""Test themed_urls returns None for empty path"""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
result = manager.themed_urls("")
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_themed_urls_no_theme_variable(self):
|
||||
"""Test themed_urls returns None when no %(theme)s in path"""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
result = manager.themed_urls("logo.png")
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_themed_urls_with_theme_variable(self):
|
||||
"""Test themed_urls returns dict of URLs for each theme"""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
result = manager.themed_urls("logo-%(theme)s.png")
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("light", result)
|
||||
self.assertIn("dark", result)
|
||||
self.assertIn("logo-light.png", result["light"])
|
||||
self.assertIn("logo-dark.png", result["dark"])
|
||||
|
||||
def test_themed_urls_with_request(self):
|
||||
"""Test themed_urls builds absolute URLs with request"""
|
||||
mock_request = HttpRequest()
|
||||
mock_request.META = {
|
||||
"HTTP_HOST": "example.com",
|
||||
"SERVER_NAME": "example.com",
|
||||
}
|
||||
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
result = manager.themed_urls("logo-%(theme)s.svg", mock_request)
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
light_url = urlparse(result["light"])
|
||||
dark_url = urlparse(result["dark"])
|
||||
self.assertEqual(light_url.scheme, "http")
|
||||
self.assertEqual(light_url.netloc, "example.com")
|
||||
self.assertEqual(dark_url.scheme, "http")
|
||||
self.assertEqual(dark_url.netloc, "example.com")
|
||||
|
||||
def test_themed_urls_passthrough_with_theme_variable(self):
|
||||
"""Test themed_urls returns dict for passthrough URLs with %(theme)s"""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
# External URLs with %(theme)s should return themed URLs
|
||||
result = manager.themed_urls("https://example.com/logo-%(theme)s.png")
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(result["light"], "https://example.com/logo-light.png")
|
||||
self.assertEqual(result["dark"], "https://example.com/logo-dark.png")
|
||||
|
||||
def test_themed_urls_passthrough_without_theme_variable(self):
|
||||
"""Test themed_urls returns None for passthrough URLs without %(theme)s"""
|
||||
manager = FileManager(FileUsage.MEDIA)
|
||||
# External URLs without %(theme)s should return None
|
||||
result = manager.themed_urls("https://example.com/logo.png")
|
||||
self.assertIsNone(result)
|
||||
|
||||
@@ -62,10 +62,10 @@ class TestSanitizeFilePath(TestCase):
|
||||
"test@file.png", # @
|
||||
"test#file.png", # #
|
||||
"test$file.png", # $
|
||||
"test%file.png", # % (but %(theme)s is allowed)
|
||||
"test%file.png", # %
|
||||
"test&file.png", # &
|
||||
"test*file.png", # *
|
||||
"test(file).png", # parentheses (but %(theme)s is allowed)
|
||||
"test(file).png", # parentheses
|
||||
"test[file].png", # brackets
|
||||
"test{file}.png", # braces
|
||||
]
|
||||
@@ -108,30 +108,3 @@ class TestSanitizeFilePath(TestCase):
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
validate_file_name(path)
|
||||
|
||||
def test_sanitize_theme_variable_valid(self):
|
||||
"""Test sanitizing filename with %(theme)s variable"""
|
||||
# These should all be valid
|
||||
validate_file_name("logo-%(theme)s.png")
|
||||
validate_file_name("brand/logo-%(theme)s.svg")
|
||||
validate_file_name("images/icon-%(theme)s.png")
|
||||
validate_file_name("%(theme)s/logo.png")
|
||||
validate_file_name("brand/%(theme)s/logo.png")
|
||||
|
||||
def test_sanitize_theme_variable_multiple(self):
|
||||
"""Test sanitizing filename with multiple %(theme)s variables"""
|
||||
validate_file_name("%(theme)s/logo-%(theme)s.png")
|
||||
|
||||
def test_sanitize_theme_variable_invalid_format(self):
|
||||
"""Test that partial or malformed theme variables are rejected"""
|
||||
invalid_paths = [
|
||||
"test%(theme.png", # missing )s
|
||||
"test%theme)s.png", # missing (
|
||||
"test%(themes).png", # wrong variable name
|
||||
"test%(THEME)s.png", # wrong case
|
||||
"test%()s.png", # empty variable name
|
||||
]
|
||||
|
||||
for path in invalid_paths:
|
||||
with self.assertRaises(ValidationError):
|
||||
validate_file_name(path)
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
import shutil
|
||||
import socket
|
||||
from tempfile import mkdtemp
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from authentik.admin.files.backends.s3 import S3Backend
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
from authentik.lib.config import CONFIG, UNSET
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
S3_TEST_ENDPOINT = "http://localhost:8020"
|
||||
|
||||
|
||||
def s3_test_server_available() -> bool:
|
||||
"""Check if the S3 test server is reachable."""
|
||||
|
||||
parsed = urlparse(S3_TEST_ENDPOINT)
|
||||
try:
|
||||
with socket.create_connection((parsed.hostname, parsed.port), timeout=2):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
class FileTestFileBackendMixin:
|
||||
def setUp(self):
|
||||
@@ -72,7 +57,7 @@ class FileTestS3BackendMixin:
|
||||
for key in s3_config_keys:
|
||||
self.original_media_s3_settings[key] = CONFIG.get(f"storage.media.s3.{key}", UNSET)
|
||||
self.media_s3_bucket_name = f"authentik-test-{generate_id(10)}".lower()
|
||||
CONFIG.set("storage.media.s3.endpoint", S3_TEST_ENDPOINT)
|
||||
CONFIG.set("storage.media.s3.endpoint", "http://localhost:8020")
|
||||
CONFIG.set("storage.media.s3.access_key", "accessKey1")
|
||||
CONFIG.set("storage.media.s3.secret_key", "secretKey1")
|
||||
CONFIG.set("storage.media.s3.bucket_name", self.media_s3_bucket_name)
|
||||
@@ -85,7 +70,7 @@ class FileTestS3BackendMixin:
|
||||
for key in s3_config_keys:
|
||||
self.original_reports_s3_settings[key] = CONFIG.get(f"storage.reports.s3.{key}", UNSET)
|
||||
self.reports_s3_bucket_name = f"authentik-test-{generate_id(10)}".lower()
|
||||
CONFIG.set("storage.reports.s3.endpoint", S3_TEST_ENDPOINT)
|
||||
CONFIG.set("storage.reports.s3.endpoint", "http://localhost:8020")
|
||||
CONFIG.set("storage.reports.s3.access_key", "accessKey1")
|
||||
CONFIG.set("storage.reports.s3.secret_key", "secretKey1")
|
||||
CONFIG.set("storage.reports.s3.bucket_name", self.reports_s3_bucket_name)
|
||||
|
||||
@@ -4,7 +4,6 @@ from pathlib import PurePosixPath
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from authentik.admin.files.backends.base import THEME_VARIABLE
|
||||
from authentik.admin.files.backends.passthrough import PassthroughBackend
|
||||
from authentik.admin.files.backends.static import StaticBackend
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
@@ -40,17 +39,12 @@ def validate_upload_file_name(
|
||||
if not name:
|
||||
raise ValidationError(_("File name cannot be empty"))
|
||||
|
||||
# Allow %(theme)s placeholder for theme-specific files
|
||||
# Replace with placeholder for validation, then check the result
|
||||
name_for_validation = name.replace(THEME_VARIABLE, "theme")
|
||||
|
||||
# Same regex is used in the frontend as well (with %(theme)s handling)
|
||||
if not re.match(r"^[a-zA-Z0-9._/-]+$", name_for_validation):
|
||||
# Same regex is used in the frontend as well
|
||||
if not re.match(r"^[a-zA-Z0-9._/-]+$", name):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"File name can only contain letters (a-z, A-Z), numbers (0-9), "
|
||||
"dots (.), hyphens (-), underscores (_), forward slashes (/), "
|
||||
"and the placeholder %(theme)s for theme-specific files"
|
||||
"dots (.), hyphens (-), underscores (_), and forward slashes (/)"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework.request import Request
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.common.oauth.constants import SCOPE_AUTHENTIK_API
|
||||
from authentik.core.middleware import CTX_AUTH_VIA
|
||||
from authentik.core.models import Token, TokenIntents, User, UserTypes
|
||||
from authentik.outposts.models import Outpost
|
||||
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
|
||||
|
||||
LOGGER = get_logger()
|
||||
_tmp = Path(gettempdir())
|
||||
@@ -70,9 +70,6 @@ class IPCUser(AnonymousUser):
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
def all_roles(self):
|
||||
return []
|
||||
|
||||
|
||||
class TokenAuthentication(BaseAuthentication):
|
||||
"""Token-based authentication using HTTP Bearer authentication"""
|
||||
@@ -106,14 +103,14 @@ class TokenAuthentication(BaseAuthentication):
|
||||
if not auth_credentials:
|
||||
return None
|
||||
# first, check traditional tokens
|
||||
key_token = Token.objects.filter(
|
||||
key_token = Token.filter_not_expired(
|
||||
key=auth_credentials, intent=TokenIntents.INTENT_API
|
||||
).first()
|
||||
if key_token:
|
||||
CTX_AUTH_VIA.set("api_token")
|
||||
return key_token.user, key_token
|
||||
# then try to auth via JWT
|
||||
jwt_token = AccessToken.objects.filter(
|
||||
jwt_token = AccessToken.filter_not_expired(
|
||||
token=auth_credentials, _scope__icontains=SCOPE_AUTHENTIK_API
|
||||
).first()
|
||||
if jwt_token:
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
from json import dumps
|
||||
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
from drf_spectacular.drainage import GENERATOR_STATS
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
from drf_spectacular.renderers import OpenApiYamlRenderer
|
||||
from drf_spectacular.validation import validate_schema
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.blueprints.v1.schema import SchemaBuilder
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.logger = get_logger()
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--blueprint-file", type=str, default="blueprints/schema.json")
|
||||
parser.add_argument("--api-file", type=str, default="schema.yml")
|
||||
|
||||
@no_translations
|
||||
def handle(self, *args, blueprint_file: str, api_file: str, **options):
|
||||
self.build_blueprint(blueprint_file)
|
||||
self.build_api(api_file)
|
||||
|
||||
def build_blueprint(self, file: str):
|
||||
self.logger.debug("Building blueprint schema...", file=file)
|
||||
blueprint_builder = SchemaBuilder()
|
||||
blueprint_builder.build()
|
||||
with open(file, "w") as _schema:
|
||||
_schema.write(
|
||||
dumps(blueprint_builder.schema, indent=4, default=SchemaBuilder.json_default)
|
||||
)
|
||||
|
||||
def build_api(self, file: str):
|
||||
self.logger.debug("Building API schema...", file=file)
|
||||
generator = SchemaGenerator()
|
||||
schema = generator.get_schema(request=None, public=True)
|
||||
GENERATOR_STATS.emit_summary()
|
||||
validate_schema(schema)
|
||||
output = OpenApiYamlRenderer().render(schema, renderer_context={})
|
||||
with open(file, "wb") as f:
|
||||
f.write(output)
|
||||
@@ -1,18 +1,10 @@
|
||||
"""Pagination which includes total pages and current page"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from drf_spectacular.plumbing import build_object_type
|
||||
from rest_framework import pagination
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentik.api.search.ql import QLSearch
|
||||
from authentik.api.v3.schema.pagination import PAGINATION
|
||||
from authentik.api.v3.schema.search import AUTOCOMPLETE_SCHEMA
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models import QuerySet
|
||||
from rest_framework.request import Request
|
||||
from authentik.api.v3.schema.response import PAGINATION
|
||||
|
||||
|
||||
class Pagination(pagination.PageNumberPagination):
|
||||
@@ -21,14 +13,7 @@ class Pagination(pagination.PageNumberPagination):
|
||||
page_query_param = "page"
|
||||
page_size_query_param = "page_size"
|
||||
|
||||
def get_page_size(self, request: Request) -> int:
|
||||
if self.page_size_query_param in request.query_params:
|
||||
page_size = super().get_page_size(request)
|
||||
if page_size is not None:
|
||||
return min(super().get_page_size(request), request.tenant.pagination_max_page_size)
|
||||
return request.tenant.pagination_default_page_size
|
||||
|
||||
def get_paginated_response(self, data) -> Response:
|
||||
def get_paginated_response(self, data):
|
||||
previous_page_number = 0
|
||||
if self.page.has_previous():
|
||||
previous_page_number = self.page.previous_page_number()
|
||||
@@ -47,33 +32,16 @@ class Pagination(pagination.PageNumberPagination):
|
||||
"end_index": self.page.end_index(),
|
||||
},
|
||||
"results": data,
|
||||
"autocomplete": self.get_autocomplete(),
|
||||
}
|
||||
)
|
||||
|
||||
def paginate_queryset(self, queryset: QuerySet, request: Request, view=None):
|
||||
self.view = view
|
||||
return super().paginate_queryset(queryset, request, view)
|
||||
|
||||
def get_autocomplete(self):
|
||||
schema = QLSearch().get_schema(self.request, self.view)
|
||||
introspections = {}
|
||||
if hasattr(self.view, "get_ql_fields"):
|
||||
from authentik.api.search.schema import AKQLSchemaSerializer
|
||||
|
||||
introspections = AKQLSchemaSerializer().serialize(
|
||||
schema(self.page.paginator.object_list.model)
|
||||
)
|
||||
return introspections
|
||||
|
||||
def get_paginated_response_schema(self, schema):
|
||||
return build_object_type(
|
||||
properties={
|
||||
"pagination": PAGINATION.ref,
|
||||
"results": schema,
|
||||
"autocomplete": AUTOCOMPLETE_SCHEMA.ref,
|
||||
},
|
||||
required=["pagination", "results", "autocomplete"],
|
||||
required=["pagination", "results"],
|
||||
)
|
||||
|
||||
|
||||
|
||||
103
authentik/api/schema.py
Normal file
103
authentik/api/schema.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
from drf_spectacular.plumbing import ResolvedComponent
|
||||
from drf_spectacular.renderers import OpenApiJsonRenderer
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.apps import AuthentikAPIConfig
|
||||
from authentik.api.v3.schema.query import QUERY_PARAMS
|
||||
from authentik.api.v3.schema.response import (
|
||||
GENERIC_ERROR,
|
||||
GENERIC_ERROR_RESPONSE,
|
||||
PAGINATION,
|
||||
VALIDATION_ERROR,
|
||||
VALIDATION_ERROR_RESPONSE,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def preprocess_schema_exclude_non_api(endpoints: list[tuple[str, Any, Any, Callable]], **kwargs):
|
||||
"""Filter out all API Views which are not mounted under /api"""
|
||||
return [
|
||||
(path, path_regex, method, callback)
|
||||
for path, path_regex, method, callback in endpoints
|
||||
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
|
||||
]
|
||||
|
||||
|
||||
def postprocess_schema_register(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Register custom schema components"""
|
||||
LOGGER.debug("Registering custom schemas")
|
||||
generator.registry.register_on_missing(PAGINATION)
|
||||
generator.registry.register_on_missing(GENERIC_ERROR)
|
||||
generator.registry.register_on_missing(GENERIC_ERROR_RESPONSE)
|
||||
generator.registry.register_on_missing(VALIDATION_ERROR)
|
||||
generator.registry.register_on_missing(VALIDATION_ERROR_RESPONSE)
|
||||
for query in QUERY_PARAMS.values():
|
||||
generator.registry.register_on_missing(query)
|
||||
return result
|
||||
|
||||
|
||||
def postprocess_schema_responses(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Default error responses"""
|
||||
LOGGER.debug("Adding default error responses")
|
||||
for path in result["paths"].values():
|
||||
for method in path.values():
|
||||
method["responses"].setdefault("400", VALIDATION_ERROR_RESPONSE.ref)
|
||||
method["responses"].setdefault("403", GENERIC_ERROR_RESPONSE.ref)
|
||||
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
|
||||
# This is a workaround for authentik/stages/prompt/stage.py
|
||||
# since the serializer PromptChallengeResponse
|
||||
# accepts dynamic keys
|
||||
for component in result["components"]["schemas"]:
|
||||
if component == "PromptChallengeResponseRequest":
|
||||
comp = result["components"]["schemas"][component]
|
||||
comp["additionalProperties"] = {}
|
||||
return result
|
||||
|
||||
|
||||
def postprocess_schema_query_params(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Optimise pagination parameters, instead of redeclaring parameters for each endpoint
|
||||
declare them globally and refer to them"""
|
||||
LOGGER.debug("Deduplicating query parameters")
|
||||
for path in result["paths"].values():
|
||||
for method in path.values():
|
||||
for idx, param in enumerate(method.get("parameters", [])):
|
||||
if param["name"] not in QUERY_PARAMS:
|
||||
continue
|
||||
method["parameters"][idx] = QUERY_PARAMS[param["name"]].ref
|
||||
return result
|
||||
|
||||
|
||||
def postprocess_schema_remove_unused(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Remove unused components"""
|
||||
# To check if the schema is used, render it to JSON and then substring check that
|
||||
# less efficient than walking through the tree but a lot simpler and no
|
||||
# possibility that we miss something
|
||||
raw = OpenApiJsonRenderer().render(result, renderer_context={}).decode()
|
||||
count = 0
|
||||
for key in result["components"][ResolvedComponent.SCHEMA].keys():
|
||||
schema_usages = raw.count(f"#/components/{ResolvedComponent.SCHEMA}/{key}")
|
||||
if schema_usages >= 1:
|
||||
continue
|
||||
del generator.registry[(key, ResolvedComponent.SCHEMA)]
|
||||
count += 1
|
||||
LOGGER.debug("Removing unused components", count=count)
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
return result
|
||||
@@ -11,12 +11,12 @@ from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
from authentik.api.authentication import IPCUser, TokenAuthentication
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.common.oauth.constants import SCOPE_AUTHENTIK_API
|
||||
from authentik.core.models import Token, TokenIntents, UserTypes
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.models import Outpost
|
||||
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
|
||||
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
"""Schema generation tests"""
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
from yaml import safe_load
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
class TestSchemaGeneration(APITestCase):
|
||||
"""Generic admin tests"""
|
||||
@@ -28,17 +21,3 @@ class TestSchemaGeneration(APITestCase):
|
||||
reverse("authentik_api:schema-browser"),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_build_schema(self):
|
||||
"""Test schema build command"""
|
||||
tmp = Path(gettempdir())
|
||||
blueprint_file = tmp / f"{str(uuid4())}.json"
|
||||
api_file = tmp / f"{str(uuid4())}.yml"
|
||||
with (
|
||||
CONFIG.patch("debug", True),
|
||||
CONFIG.patch("tenants.enabled", True),
|
||||
CONFIG.patch("outposts.disable_embedded_outpost", True),
|
||||
):
|
||||
call_command("build_schema", blueprint_file=blueprint_file, api_file=api_file)
|
||||
self.assertTrue(blueprint_file.exists())
|
||||
self.assertTrue(api_file.exists())
|
||||
|
||||
@@ -1,73 +1,31 @@
|
||||
"""authentik API Modelviewset tests"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.test import TestCase
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.admin.api.version_history import VersionHistoryViewSet
|
||||
from authentik.api.v3.urls import router
|
||||
from authentik.core.tests.utils import RequestFactory, create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.tenants.api.domains import DomainViewSet
|
||||
from authentik.tenants.api.tenants import TenantViewSet
|
||||
from authentik.tenants.utils import get_current_tenant
|
||||
|
||||
|
||||
class TestModelViewSets(TestCase):
|
||||
"""Test Viewset"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_test_admin_user()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
|
||||
def viewset_tester_factory(test_viewset: type[ModelViewSet], full=True) -> dict[str, Callable]:
|
||||
def viewset_tester_factory(test_viewset: type[ModelViewSet]) -> Callable:
|
||||
"""Test Viewset"""
|
||||
|
||||
def test_attrs(self: TestModelViewSets) -> None:
|
||||
"""Test attributes we require on all viewsets"""
|
||||
self.assertIsNotNone(getattr(test_viewset, "ordering", None))
|
||||
def tester(self: TestModelViewSets):
|
||||
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
|
||||
self.assertIsNotNone(getattr(test_viewset, "ordering", None))
|
||||
filterset_class = getattr(test_viewset, "filterset_class", None)
|
||||
if not filterset_class:
|
||||
self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None))
|
||||
|
||||
def test_ordering(self: TestModelViewSets) -> None:
|
||||
"""Test that all ordering fields are correct"""
|
||||
view = test_viewset.as_view({"get": "list"})
|
||||
for ordering_field in test_viewset.ordering:
|
||||
with self.subTest(ordering_field):
|
||||
req = self.factory.get(
|
||||
f"/?{urlencode({'ordering': ordering_field}, doseq=True)}", user=self.user
|
||||
)
|
||||
req.tenant = get_current_tenant()
|
||||
res = view(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_search(self: TestModelViewSets) -> None:
|
||||
"""Test that search fields are correct"""
|
||||
view = test_viewset.as_view({"get": "list"})
|
||||
req = self.factory.get(
|
||||
f"/?{urlencode({'search': generate_id()}, doseq=True)}", user=self.user
|
||||
)
|
||||
req.tenant = get_current_tenant()
|
||||
res = view(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
cases = {
|
||||
"attrs": test_attrs,
|
||||
}
|
||||
if full:
|
||||
cases["ordering"] = test_ordering
|
||||
cases["search"] = test_search
|
||||
return cases
|
||||
return tester
|
||||
|
||||
|
||||
for _, viewset, _ in router.registry:
|
||||
if not issubclass(viewset, ModelViewSet | ReadOnlyModelViewSet):
|
||||
continue
|
||||
full = viewset not in [VersionHistoryViewSet, DomainViewSet, TenantViewSet]
|
||||
for test, case in viewset_tester_factory(viewset, full=full).items():
|
||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}_{test}", case)
|
||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}", viewset_tester_factory(viewset))
|
||||
|
||||
@@ -31,7 +31,6 @@ class Capabilities(models.TextChoices):
|
||||
"""Define capabilities which influence which APIs can/should be used"""
|
||||
|
||||
CAN_SAVE_MEDIA = "can_save_media"
|
||||
CAN_SAVE_REPORTS = "can_save_reports"
|
||||
CAN_GEO_IP = "can_geo_ip"
|
||||
CAN_ASN = "can_asn"
|
||||
CAN_IMPERSONATE = "can_impersonate"
|
||||
@@ -71,8 +70,6 @@ class ConfigView(APIView):
|
||||
caps = []
|
||||
if get_file_manager(FileUsage.MEDIA).manageable:
|
||||
caps.append(Capabilities.CAN_SAVE_MEDIA)
|
||||
if get_file_manager(FileUsage.REPORTS).manageable:
|
||||
caps.append(Capabilities.CAN_SAVE_REPORTS)
|
||||
for processor in get_context_processors():
|
||||
if cap := processor.capability():
|
||||
caps.append(cap)
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from drf_spectacular.contrib.django_filters import (
|
||||
DjangoFilterExtension as BaseDjangoFilterExtension,
|
||||
)
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
from drf_spectacular.plumbing import (
|
||||
ResolvedComponent,
|
||||
follow_field_source,
|
||||
)
|
||||
from drf_spectacular.renderers import OpenApiJsonRenderer
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.apps import AuthentikAPIConfig
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def preprocess_schema_exclude_non_api(endpoints: list[tuple[str, Any, Any, Callable]], **kwargs):
|
||||
"""Filter out all API Views which are not mounted under /api"""
|
||||
return [
|
||||
(path, path_regex, method, callback)
|
||||
for path, path_regex, method, callback in endpoints
|
||||
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
|
||||
]
|
||||
|
||||
|
||||
def postprocess_schema_remove_unused(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Remove unused components"""
|
||||
# To check if the schema is used, render it to JSON and then substring check that
|
||||
# less efficient than walking through the tree but a lot simpler and no
|
||||
# possibility that we miss something
|
||||
raw = OpenApiJsonRenderer().render(result, renderer_context={}).decode()
|
||||
count = 0
|
||||
for key in result["components"][ResolvedComponent.SCHEMA].keys():
|
||||
schema_usages = raw.count(f"#/components/{ResolvedComponent.SCHEMA}/{key}")
|
||||
if schema_usages >= 1:
|
||||
continue
|
||||
del generator.registry[(key, ResolvedComponent.SCHEMA)]
|
||||
count += 1
|
||||
LOGGER.debug("Removing unused components", count=count)
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
return result
|
||||
|
||||
|
||||
class DjangoFilterExtension(BaseDjangoFilterExtension):
|
||||
"""
|
||||
From https://github.com/netbox-community/netbox/pull/21521:
|
||||
|
||||
Overrides drf-spectacular's DjangoFilterExtension to fix a regression in v0.29.0 where
|
||||
_get_model_field() incorrectly double-appends to_field_name when field_name already ends
|
||||
with that value (e.g. field_name='tags__slug', to_field_name='slug' produces the invalid
|
||||
path ['tags', 'slug', 'slug']). This caused hundreds of spurious warnings during schema
|
||||
generation for filters such as TagFilter, TenancyFilterSet.tenant, and OwnerFilterMixin.owner.
|
||||
|
||||
See: https://github.com/netbox-community/netbox/issues/20787
|
||||
https://github.com/tfranzel/drf-spectacular/issues/1475
|
||||
"""
|
||||
|
||||
priority = 1
|
||||
|
||||
def _get_model_field(self, filter_field, model):
|
||||
if not filter_field.field_name:
|
||||
return None
|
||||
path = filter_field.field_name.split("__")
|
||||
to_field_name = filter_field.extra.get("to_field_name")
|
||||
if to_field_name is not None and path[-1] != to_field_name:
|
||||
path.append(to_field_name)
|
||||
return follow_field_source(model, path, emit_warnings=False)
|
||||
@@ -1,287 +0,0 @@
|
||||
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from enum import Enum
|
||||
|
||||
from django.db.models import Choices
|
||||
from django.utils.translation import get_language
|
||||
from drf_spectacular.drainage import error, warn
|
||||
from drf_spectacular.hooks import postprocess_schema_enum_id_removal
|
||||
from drf_spectacular.plumbing import (
|
||||
ResolvedComponent,
|
||||
deep_import_string,
|
||||
list_hash,
|
||||
safe_ref,
|
||||
)
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from inflection import camelize
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
# See https://github.com/tfranzel/drf-spectacular/blob/master/drf_spectacular/hooks.py
|
||||
# and https://github.com/tfranzel/drf-spectacular/issues/520
|
||||
def postprocess_schema_enums(result, generator, **kwargs): # noqa: PLR0912, PLR0915
|
||||
"""
|
||||
simple replacement of Enum/Choices that globally share the same name and have
|
||||
the same choices. Aids client generation to not generate a separate enum for
|
||||
every occurrence. only takes effect when replacement is guaranteed to be correct.
|
||||
"""
|
||||
|
||||
def is_enum_prop(prop_schema):
|
||||
return (
|
||||
"enum" in prop_schema
|
||||
or prop_schema.get("type") == "array"
|
||||
and "enum" in prop_schema.get("items", {})
|
||||
)
|
||||
|
||||
def iter_field_schemas():
|
||||
def iter_prop_containers(schema, component_name=None):
|
||||
if not component_name:
|
||||
for _component_name, _schema in schema.items():
|
||||
if spectacular_settings.COMPONENT_SPLIT_PATCH:
|
||||
_component_name = re.sub("^Patched(.+)", r"\1", _component_name)
|
||||
if spectacular_settings.COMPONENT_SPLIT_REQUEST:
|
||||
_component_name = re.sub("(.+)Request$", r"\1", _component_name)
|
||||
yield from iter_prop_containers(_schema, _component_name)
|
||||
elif isinstance(schema, list):
|
||||
for item in schema:
|
||||
yield from iter_prop_containers(item, component_name)
|
||||
elif isinstance(schema, dict):
|
||||
if schema.get("properties"):
|
||||
yield component_name, schema["properties"]
|
||||
yield from iter_prop_containers(schema.get("oneOf", []), component_name)
|
||||
yield from iter_prop_containers(schema.get("allOf", []), component_name)
|
||||
yield from iter_prop_containers(schema.get("anyOf", []), component_name)
|
||||
|
||||
def iter_path_parameters():
|
||||
for path in result.get("paths", {}).values():
|
||||
for operation in path.values():
|
||||
for parameter in operation.get("parameters", []):
|
||||
parameter_schema = parameter.get("schema", {})
|
||||
if is_enum_prop(parameter_schema):
|
||||
# Move description into enum schema
|
||||
if "description" in parameter:
|
||||
parameter_schema["description"] = parameter.pop("description")
|
||||
if "name" not in parameter:
|
||||
continue
|
||||
yield "", {parameter["name"]: parameter_schema}
|
||||
|
||||
component_schemas = result.get("components", {}).get("schemas", {})
|
||||
|
||||
yield from iter_prop_containers(component_schemas)
|
||||
yield from iter_path_parameters()
|
||||
|
||||
def create_enum_component(name, schema):
|
||||
component = ResolvedComponent(
|
||||
name=name,
|
||||
type=ResolvedComponent.SCHEMA,
|
||||
schema=schema,
|
||||
object=name,
|
||||
)
|
||||
generator.registry.register_on_missing(component)
|
||||
return component
|
||||
|
||||
def extract_hash(schema):
|
||||
if "x-spec-enum-id" in schema:
|
||||
# try to use the injected enum hash first as it generated from (name, value) tuples,
|
||||
# which prevents collisions on choice sets only differing in labels not values.
|
||||
return schema["x-spec-enum-id"]
|
||||
else:
|
||||
# fall back to actual list hashing when we encounter enums not generated by us.
|
||||
# remove blank/null entry for hashing. will be reconstructed in the last step
|
||||
return list_hash([(i, i) for i in schema["enum"] if i not in ("", None)])
|
||||
|
||||
overrides = load_enum_name_overrides()
|
||||
|
||||
prop_hash_mapping = defaultdict(set)
|
||||
hash_name_mapping = defaultdict(set)
|
||||
# collect all enums, their names and choice sets
|
||||
for component_name, props in iter_field_schemas():
|
||||
for prop_name, prop_schema in props.items():
|
||||
_prop_schema = prop_schema
|
||||
if prop_schema.get("type") == "array":
|
||||
_prop_schema = prop_schema.get("items", {})
|
||||
if "enum" not in _prop_schema:
|
||||
continue
|
||||
|
||||
prop_enum_cleaned_hash = extract_hash(_prop_schema)
|
||||
prop_hash_mapping[prop_name].add(prop_enum_cleaned_hash)
|
||||
hash_name_mapping[prop_enum_cleaned_hash].add((component_name, prop_name))
|
||||
|
||||
# get the suffix to be used for enums from settings
|
||||
enum_suffix = spectacular_settings.ENUM_SUFFIX
|
||||
|
||||
# traverse all enum properties and generate a name for the choice set. naming collisions
|
||||
# are resolved and a warning is emitted. giving a choice set multiple names is technically
|
||||
# correct but potentially unwanted. also emit a warning there to make the user aware.
|
||||
enum_name_mapping = {}
|
||||
for prop_name, prop_hash_set in prop_hash_mapping.items():
|
||||
for prop_hash in prop_hash_set:
|
||||
if prop_hash in overrides:
|
||||
enum_name = overrides[prop_hash]
|
||||
elif len(prop_hash_set) == 1:
|
||||
# prop_name has been used exclusively for one choice set (best case)
|
||||
enum_name = f"{camelize(prop_name)}{enum_suffix}"
|
||||
elif len(hash_name_mapping[prop_hash]) == 1:
|
||||
# prop_name has multiple choice sets, but each one limited to one component only
|
||||
component_name, _ = next(iter(hash_name_mapping[prop_hash]))
|
||||
enum_name = f"{camelize(component_name)}{camelize(prop_name)}{enum_suffix}"
|
||||
else:
|
||||
enum_name = f"{camelize(prop_name)}{prop_hash[:3].capitalize()}{enum_suffix}"
|
||||
warn(
|
||||
f"enum naming encountered a non-optimally resolvable collision for fields "
|
||||
f'named "{prop_name}". The same name has been used for multiple choice sets '
|
||||
f'in multiple components. The collision was resolved with "{enum_name}". '
|
||||
f"add an entry to ENUM_NAME_OVERRIDES to fix the naming."
|
||||
)
|
||||
if enum_name_mapping.get(prop_hash, enum_name) != enum_name:
|
||||
warn(
|
||||
f"encountered multiple names for the same choice set ({enum_name}). This "
|
||||
f"may be unwanted even though the generated schema is technically correct. "
|
||||
f"Add an entry to ENUM_NAME_OVERRIDES to fix the naming."
|
||||
)
|
||||
del enum_name_mapping[prop_hash]
|
||||
else:
|
||||
enum_name_mapping[prop_hash] = enum_name
|
||||
enum_name_mapping[(prop_hash, prop_name)] = enum_name
|
||||
|
||||
# replace all enum occurrences with a enum schema component. cut out the
|
||||
# enum, replace it with a reference and add a corresponding component.
|
||||
for _, props in iter_field_schemas():
|
||||
for prop_name, _prop_schema in props.items():
|
||||
prop_schema = _prop_schema
|
||||
is_array = prop_schema.get("type") == "array"
|
||||
if is_array:
|
||||
prop_schema = prop_schema.get("items", {})
|
||||
|
||||
if "enum" not in prop_schema:
|
||||
continue
|
||||
|
||||
prop_enum_original_list = prop_schema["enum"]
|
||||
prop_schema["enum"] = [i for i in prop_schema["enum"] if i not in ["", None]]
|
||||
prop_hash = extract_hash(prop_schema)
|
||||
# when choice sets are reused under multiple names, the generated name cannot be
|
||||
# resolved from the hash alone. fall back to prop_name and hash for resolution.
|
||||
enum_name = enum_name_mapping.get(prop_hash) or enum_name_mapping[prop_hash, prop_name]
|
||||
|
||||
# split property into remaining property and enum component parts
|
||||
enum_schema = {k: v for k, v in prop_schema.items() if k in ["type", "enum"]}
|
||||
prop_schema = {
|
||||
k: v for k, v in prop_schema.items() if k not in ["type", "enum", "x-spec-enum-id"]
|
||||
}
|
||||
|
||||
# separate actual description from name-value tuples
|
||||
if spectacular_settings.ENUM_GENERATE_CHOICE_DESCRIPTION:
|
||||
if prop_schema.get("description", "").startswith("*"):
|
||||
enum_schema["description"] = prop_schema.pop("description")
|
||||
elif "\n\n*" in prop_schema.get("description", ""):
|
||||
_, _, post = prop_schema["description"].partition("\n\n*")
|
||||
enum_schema["description"] = "*" + post
|
||||
|
||||
components = [create_enum_component(enum_name, schema=enum_schema)]
|
||||
if spectacular_settings.ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE:
|
||||
if "" in prop_enum_original_list:
|
||||
components.append(
|
||||
create_enum_component(f"Blank{enum_suffix}", schema={"enum": [""]})
|
||||
)
|
||||
if None in prop_enum_original_list:
|
||||
if spectacular_settings.OAS_VERSION.startswith("3.1"):
|
||||
components.append(
|
||||
create_enum_component(f"Null{enum_suffix}", schema={"type": "null"})
|
||||
)
|
||||
else:
|
||||
components.append(
|
||||
create_enum_component(f"Null{enum_suffix}", schema={"enum": [None]})
|
||||
)
|
||||
|
||||
# undo OAS 3.1 type list NULL construction as we cover
|
||||
# this in a separate component already
|
||||
if spectacular_settings.OAS_VERSION.startswith("3.1") and isinstance(
|
||||
enum_schema["type"], list
|
||||
):
|
||||
enum_schema["type"] = [t for t in enum_schema["type"] if t != "null"][0]
|
||||
|
||||
if len(components) == 1:
|
||||
prop_schema.update(components[0].ref)
|
||||
else:
|
||||
prop_schema.update({"oneOf": [c.ref for c in components]})
|
||||
|
||||
patch_target = props[prop_name] # noqa: PLR1733
|
||||
if is_array:
|
||||
patch_target = patch_target["items"]
|
||||
|
||||
# Replace existing schema information with reference
|
||||
patch_target.clear()
|
||||
patch_target.update(safe_ref(prop_schema))
|
||||
|
||||
# sort again with additional components
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
|
||||
# remove remaining ids that were not part of this hook (operation parameters mainly)
|
||||
postprocess_schema_enum_id_removal(result, generator)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Fixed version of `load_enum_name_overrides()` with a LRU cache based on language
|
||||
# *and* enum overrides.
|
||||
# Without this, API generation breaks if there is more than 1 API present (such as in split APIs)
|
||||
# Original source: drf-spectacular/drf_spectacular/plumbing.py
|
||||
def load_enum_name_overrides():
|
||||
cache_key = get_language() or ""
|
||||
|
||||
for k, v in sorted(spectacular_settings.ENUM_NAME_OVERRIDES.items()):
|
||||
cache_key += f";{k}:{v}"
|
||||
|
||||
return _load_enum_name_overrides(cache_key)
|
||||
|
||||
|
||||
# Original source: drf-spectacular/drf_spectacular/plumbing.py
|
||||
# Only change: cache_key argument instead of language.
|
||||
@functools.lru_cache
|
||||
def _load_enum_name_overrides(cache_key):
|
||||
overrides = {}
|
||||
for name, _choices in spectacular_settings.ENUM_NAME_OVERRIDES.items():
|
||||
choices = _choices
|
||||
if isinstance(choices, str):
|
||||
choices = deep_import_string(choices)
|
||||
if not choices:
|
||||
warn(
|
||||
f"unable to load choice override for {name} from ENUM_NAME_OVERRIDES. "
|
||||
f"please check module path string."
|
||||
)
|
||||
continue
|
||||
if inspect.isclass(choices) and issubclass(choices, Choices):
|
||||
choices = choices.choices
|
||||
if inspect.isclass(choices) and issubclass(choices, Enum):
|
||||
choices = [(c.value, c.name) for c in choices]
|
||||
normalized_choices = []
|
||||
for choice in choices:
|
||||
# Allow None values in the simple values list case
|
||||
if isinstance(choice, str) or choice is None:
|
||||
# TODO warning
|
||||
normalized_choices.append((choice, choice)) # simple choice list
|
||||
elif isinstance(choice[1], (list, tuple)):
|
||||
normalized_choices.extend(choice[1]) # categorized nested choices
|
||||
else:
|
||||
normalized_choices.append(choice) # normal 2-tuple form
|
||||
|
||||
# Get all of choice values that should be used in the hash, blank and
|
||||
# None values get excluded in the post-processing hook for enum overrides,
|
||||
# so we do the same here to ensure the hashes match
|
||||
hashable_values = [
|
||||
(value, label) for value, label in normalized_choices if value not in ["", None]
|
||||
]
|
||||
overrides[list_hash(hashable_values)] = name
|
||||
|
||||
if len(spectacular_settings.ENUM_NAME_OVERRIDES) != len(overrides):
|
||||
error(
|
||||
"ENUM_NAME_OVERRIDES has duplication issues. Encountered multiple names "
|
||||
"for the same choice set. Enum naming might be unexpected."
|
||||
)
|
||||
return overrides
|
||||
@@ -1,32 +0,0 @@
|
||||
from drf_spectacular.plumbing import (
|
||||
ResolvedComponent,
|
||||
build_basic_type,
|
||||
build_object_type,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
PAGINATION = ResolvedComponent(
|
||||
name="Pagination",
|
||||
type=ResolvedComponent.SCHEMA,
|
||||
object="Pagination",
|
||||
schema=build_object_type(
|
||||
properties={
|
||||
"next": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"previous": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"count": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"current": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"total_pages": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"start_index": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"end_index": build_basic_type(OpenApiTypes.NUMBER),
|
||||
},
|
||||
required=[
|
||||
"next",
|
||||
"previous",
|
||||
"count",
|
||||
"current",
|
||||
"total_pages",
|
||||
"start_index",
|
||||
"end_index",
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -1,17 +1,10 @@
|
||||
from typing import Any
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
from drf_spectacular.plumbing import (
|
||||
ResolvedComponent,
|
||||
build_basic_type,
|
||||
build_parameter_type,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
QUERY_PARAMS = {
|
||||
"ordering": ResolvedComponent(
|
||||
@@ -70,18 +63,3 @@ QUERY_PARAMS = {
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def postprocess_schema_query_params(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Optimize pagination parameters, instead of redeclaring parameters for each endpoint
|
||||
declare them globally and refer to them"""
|
||||
LOGGER.debug("Deduplicating query parameters")
|
||||
for path in result["paths"].values():
|
||||
for method in path.values():
|
||||
for idx, param in enumerate(method.get("parameters", [])):
|
||||
if param["name"] not in QUERY_PARAMS:
|
||||
continue
|
||||
method["parameters"][idx] = QUERY_PARAMS[param["name"]].ref
|
||||
return result
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
from typing import Any
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
from drf_spectacular.plumbing import (
|
||||
ResolvedComponent,
|
||||
build_array_type,
|
||||
build_basic_type,
|
||||
build_object_type,
|
||||
)
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework.settings import api_settings
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.v3.schema.pagination import PAGINATION
|
||||
from authentik.api.v3.schema.query import QUERY_PARAMS
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
GENERIC_ERROR = ResolvedComponent(
|
||||
name="GenericError",
|
||||
@@ -67,40 +57,28 @@ VALIDATION_ERROR_RESPONSE = ResolvedComponent(
|
||||
"description": "",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def postprocess_schema_register(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Register custom schema components"""
|
||||
LOGGER.debug("Registering custom schemas")
|
||||
generator.registry.register_on_missing(PAGINATION)
|
||||
generator.registry.register_on_missing(GENERIC_ERROR)
|
||||
generator.registry.register_on_missing(GENERIC_ERROR_RESPONSE)
|
||||
generator.registry.register_on_missing(VALIDATION_ERROR)
|
||||
generator.registry.register_on_missing(VALIDATION_ERROR_RESPONSE)
|
||||
for query in QUERY_PARAMS.values():
|
||||
generator.registry.register_on_missing(query)
|
||||
return result
|
||||
|
||||
|
||||
def postprocess_schema_responses(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Default error responses"""
|
||||
LOGGER.debug("Adding default error responses")
|
||||
for path in result["paths"].values():
|
||||
for method in path.values():
|
||||
method["responses"].setdefault("400", VALIDATION_ERROR_RESPONSE.ref)
|
||||
method["responses"].setdefault("403", GENERIC_ERROR_RESPONSE.ref)
|
||||
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
|
||||
# This is a workaround for authentik/stages/prompt/stage.py
|
||||
# since the serializer PromptChallengeResponse
|
||||
# accepts dynamic keys
|
||||
for component in result["components"]["schemas"]:
|
||||
if component == "PromptChallengeResponseRequest":
|
||||
comp = result["components"]["schemas"][component]
|
||||
comp["additionalProperties"] = {}
|
||||
return result
|
||||
PAGINATION = ResolvedComponent(
|
||||
name="Pagination",
|
||||
type=ResolvedComponent.SCHEMA,
|
||||
object="Pagination",
|
||||
schema=build_object_type(
|
||||
properties={
|
||||
"next": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"previous": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"count": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"current": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"total_pages": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"start_index": build_basic_type(OpenApiTypes.NUMBER),
|
||||
"end_index": build_basic_type(OpenApiTypes.NUMBER),
|
||||
},
|
||||
required=[
|
||||
"next",
|
||||
"previous",
|
||||
"count",
|
||||
"current",
|
||||
"total_pages",
|
||||
"start_index",
|
||||
"end_index",
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from drf_spectacular.plumbing import ResolvedComponent, build_object_type
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
|
||||
|
||||
AUTOCOMPLETE_SCHEMA = ResolvedComponent(
|
||||
name="Autocomplete",
|
||||
object="Autocomplete",
|
||||
type=ResolvedComponent.SCHEMA,
|
||||
schema=build_object_type(additionalProperties={}),
|
||||
)
|
||||
|
||||
|
||||
def postprocess_schema_search_autocomplete(result, generator: SchemaGenerator, **kwargs):
|
||||
generator.registry.register_on_missing(AUTOCOMPLETE_SCHEMA)
|
||||
|
||||
return result
|
||||
@@ -1,60 +1,24 @@
|
||||
"""Serializer mixin for managed models"""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
DateTimeField,
|
||||
FileField,
|
||||
)
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, DateTimeField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ListSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.validation import validate
|
||||
from authentik.blueprints.models import BlueprintInstance
|
||||
from authentik.blueprints.v1.common import Blueprint
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.blueprints.v1.oci import OCI_PREFIX
|
||||
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.events.logs import LogEventSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
|
||||
def get_blueprints():
|
||||
if settings.DEBUG:
|
||||
return blueprints_find_dict()
|
||||
return blueprints_find_dict.send().get_result(block=True)
|
||||
|
||||
|
||||
class BlueprintUploadSerializer(PassiveSerializer):
|
||||
"""Serializer to upload file"""
|
||||
|
||||
file = FileField(required=False)
|
||||
path = CharField(required=False)
|
||||
|
||||
def validate_path(self, path: str) -> str:
|
||||
"""Ensure the path (if set) specified is retrievable"""
|
||||
if path == "":
|
||||
return path
|
||||
files: list[dict] = get_blueprints()
|
||||
if path not in [file["path"] for file in files]:
|
||||
raise ValidationError(_("Blueprint file does not exist"))
|
||||
return path
|
||||
|
||||
|
||||
class ManagedSerializer:
|
||||
"""Managed Serializer"""
|
||||
|
||||
@@ -75,7 +39,7 @@ class BlueprintInstanceSerializer(ModelSerializer):
|
||||
"""Ensure the path (if set) specified is retrievable"""
|
||||
if path == "" or path.startswith(OCI_PREFIX):
|
||||
return path
|
||||
files: list[dict] = get_blueprints()
|
||||
files: list[dict] = blueprints_find_dict.send().get_result(block=True)
|
||||
if path not in [file["path"] for file in files]:
|
||||
raise ValidationError(_("Blueprint file does not exist"))
|
||||
return path
|
||||
@@ -124,33 +88,6 @@ class BlueprintInstanceSerializer(ModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
def check_blueprint_perms(blueprint: Blueprint, user: User, explicit_action: str | None = None):
|
||||
"""Check for individual permissions for each model in a blueprint"""
|
||||
for entry in blueprint.iter_entries():
|
||||
full_model = entry.get_model(blueprint)
|
||||
app, __, model = full_model.partition(".")
|
||||
perms = [
|
||||
f"{app}.add_{model}",
|
||||
f"{app}.change_{model}",
|
||||
f"{app}.delete_{model}",
|
||||
]
|
||||
if explicit_action:
|
||||
perms = [f"{app}.{explicit_action}_{model}"]
|
||||
for perm in perms:
|
||||
if not user.has_perm(perm):
|
||||
raise PermissionDenied(
|
||||
{
|
||||
entry.id: _(
|
||||
"User lacks permission to create {model}".format_map(
|
||||
{
|
||||
"model": full_model,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Blueprint instances"""
|
||||
|
||||
@@ -160,12 +97,6 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
filterset_fields = ["name", "path"]
|
||||
ordering = ["name"]
|
||||
|
||||
class BlueprintImportResultSerializer(PassiveSerializer):
|
||||
"""Logs of an attempted blueprint import"""
|
||||
|
||||
logs = LogEventSerializer(many=True, read_only=True)
|
||||
success = BooleanField(read_only=True)
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: ListSerializer(
|
||||
@@ -184,7 +115,7 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def available(self, request: Request) -> Response:
|
||||
"""Get blueprints"""
|
||||
files: list[dict] = get_blueprints()
|
||||
files: list[dict] = blueprints_find_dict.send().get_result(block=True)
|
||||
return Response(files)
|
||||
|
||||
@permission_required("authentik_blueprints.view_blueprintinstance")
|
||||
@@ -200,53 +131,3 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
blueprint = self.get_object()
|
||||
apply_blueprint.send_with_options(args=(blueprint.pk,), rel_obj=blueprint)
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
@extend_schema(
|
||||
request={"multipart/form-data": BlueprintUploadSerializer},
|
||||
responses={
|
||||
204: BlueprintImportResultSerializer,
|
||||
400: BlueprintImportResultSerializer,
|
||||
},
|
||||
)
|
||||
@action(url_path="import", detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
|
||||
@validate(
|
||||
BlueprintUploadSerializer,
|
||||
)
|
||||
def import_(self, request: Request, body: BlueprintUploadSerializer) -> Response:
|
||||
"""Import blueprint from .yaml file and apply it once, without creating an instance"""
|
||||
string_contents = ""
|
||||
if body.validated_data.get("file"):
|
||||
file = cast(InMemoryUploadedFile, body.validated_data["file"])
|
||||
string_contents = file.read().decode()
|
||||
elif body.validated_data.get("path"):
|
||||
string_contents = BlueprintInstance(
|
||||
path=body.validated_data.get("path")
|
||||
).retrieve_file()
|
||||
else:
|
||||
raise ValidationError("Either path or file must be set")
|
||||
importer = Importer.from_string(string_contents)
|
||||
|
||||
check_blueprint_perms(importer.blueprint, request.user)
|
||||
|
||||
valid, logs = importer.validate()
|
||||
|
||||
import_response = self.BlueprintImportResultSerializer(
|
||||
data={
|
||||
"logs": [],
|
||||
"success": False,
|
||||
}
|
||||
)
|
||||
import_response.is_valid(raise_exception=True)
|
||||
|
||||
import_response.initial_data["logs"] = [LogEventSerializer(log).data for log in logs]
|
||||
import_response.initial_data["success"] = valid
|
||||
import_response.is_valid()
|
||||
if not valid:
|
||||
return Response(data=import_response.initial_data, status=200)
|
||||
|
||||
successful = importer.apply()
|
||||
import_response.initial_data["success"] = successful
|
||||
import_response.is_valid()
|
||||
if not successful:
|
||||
return Response(data=import_response.initial_data, status=200)
|
||||
return Response(data=import_response.initial_data, status=200)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""authentik Blueprints app"""
|
||||
|
||||
import traceback
|
||||
from collections.abc import Callable
|
||||
from importlib import import_module
|
||||
from inspect import ismethod
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.db import DatabaseError, InternalError, ProgrammingError
|
||||
from dramatiq.broker import get_broker
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
@@ -45,21 +44,8 @@ class ManagedAppConfig(AppConfig):
|
||||
module_name = f"{self.name}.{rel_module}"
|
||||
import_module(module_name)
|
||||
self.logger.info("Imported related module", module=module_name)
|
||||
except ModuleNotFoundError as exc:
|
||||
if settings.DEBUG:
|
||||
# This is a heuristic for determining whether the exception was caused
|
||||
# "directly" by the `import_module` call or whether the initial import
|
||||
# succeeded and a later import (within the existing module) failed.
|
||||
# 1. <the calling function>
|
||||
# 2. importlib.import_module
|
||||
# 3. importlib._bootstrap._gcd_import
|
||||
# 4. importlib._bootstrap._find_and_load
|
||||
# 5. importlib._bootstrap._find_and_load_unlocked
|
||||
STACK_LENGTH_HEURISTIC = 5
|
||||
|
||||
stack_length = len(traceback.extract_tb(exc.__traceback__))
|
||||
if stack_length > STACK_LENGTH_HEURISTIC:
|
||||
raise
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
import_relative("checks")
|
||||
import_relative("tasks")
|
||||
@@ -71,19 +57,12 @@ class ManagedAppConfig(AppConfig):
|
||||
|
||||
def _reconcile(self, prefix: str) -> None:
|
||||
for meth_name in dir(self):
|
||||
# Check the attribute on the class to avoid evaluating @property descriptors.
|
||||
# Using getattr(self, ...) on a @property would evaluate it, which can trigger
|
||||
# expensive side effects (e.g. tenant_schedule_specs iterating all providers
|
||||
# and running PolicyEngine queries for every user).
|
||||
class_attr = getattr(type(self), meth_name, None)
|
||||
if class_attr is None or isinstance(class_attr, property):
|
||||
meth = getattr(self, meth_name)
|
||||
if not ismethod(meth):
|
||||
continue
|
||||
if not callable(class_attr):
|
||||
continue
|
||||
category = getattr(class_attr, "_authentik_managed_reconcile", None)
|
||||
category = getattr(meth, "_authentik_managed_reconcile", None)
|
||||
if category != prefix:
|
||||
continue
|
||||
meth = getattr(self, meth_name)
|
||||
name = meth_name.replace(prefix, "")
|
||||
try:
|
||||
self.logger.debug("Starting reconciler", name=name)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Apply blueprint from commandline"""
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from sys import exit as sys_exit
|
||||
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
@@ -32,5 +31,5 @@ class Command(BaseCommand):
|
||||
sys_exit(1)
|
||||
importer.apply()
|
||||
|
||||
def add_arguments(self, parser: ArgumentParser):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("blueprints", nargs="+", type=str)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Generate JSON Schema for blueprints"""
|
||||
|
||||
from json import dumps
|
||||
from typing import Any
|
||||
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
from django.db.models import Model, fields
|
||||
from django.db.models.fields.related import OneToOneField
|
||||
from drf_jsonschema_serializer.convert import converter, field_to_converter
|
||||
from rest_framework.fields import Field, JSONField, UUIDField
|
||||
from rest_framework.relations import PrimaryKeyRelatedField
|
||||
@@ -31,19 +32,18 @@ class PrimaryKeyRelatedFieldConverter:
|
||||
def convert(self, field: PrimaryKeyRelatedField):
|
||||
model: Model = field.queryset.model
|
||||
pk_field = model._meta.pk
|
||||
if isinstance(pk_field, OneToOneField):
|
||||
pk_field = pk_field.related_fields[0][1]
|
||||
if isinstance(pk_field, fields.UUIDField):
|
||||
return {"type": "string", "format": "uuid"}
|
||||
return {"type": "integer"}
|
||||
|
||||
|
||||
class SchemaBuilder:
|
||||
class Command(BaseCommand):
|
||||
"""Generate JSON Schema for blueprints"""
|
||||
|
||||
schema: dict
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
@@ -90,6 +90,16 @@ class SchemaBuilder:
|
||||
"$defs": {"blueprint_entry": {"oneOf": []}},
|
||||
}
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--file", type=str)
|
||||
|
||||
@no_translations
|
||||
def handle(self, *args, file: str, **options):
|
||||
"""Generate JSON Schema for blueprints"""
|
||||
self.build()
|
||||
with open(file, "w") as _schema:
|
||||
_schema.write(dumps(self.schema, indent=4, default=Command.json_default))
|
||||
|
||||
@staticmethod
|
||||
def json_default(value: Any) -> Any:
|
||||
"""Helper that handles gettext_lazy strings that JSON doesn't handle"""
|
||||
@@ -111,7 +121,7 @@ class SchemaBuilder:
|
||||
try:
|
||||
serializer_class = model_instance.serializer
|
||||
except NotImplementedError as exc:
|
||||
raise ValueError(f"SerializerModel not implemented by {model}") from exc
|
||||
raise NotImplementedError(model_instance) from exc
|
||||
serializer = serializer_class(
|
||||
context={
|
||||
SERIALIZER_CONTEXT_BLUEPRINT: False,
|
||||
@@ -8,62 +8,45 @@ metadata:
|
||||
- Application (icon)
|
||||
- Source (icon)
|
||||
- Flow (background)
|
||||
- Endpoint Enrollment token (key)
|
||||
entries:
|
||||
token:
|
||||
- model: authentik_core.token
|
||||
identifiers:
|
||||
identifier: "%(uid)s-token"
|
||||
attrs:
|
||||
key: "%(uid)s"
|
||||
user: "%(user)s"
|
||||
intent: api
|
||||
app:
|
||||
- model: authentik_core.application
|
||||
identifiers:
|
||||
slug: "%(uid)s-app"
|
||||
attrs:
|
||||
name: "%(uid)s-app"
|
||||
icon: https://goauthentik.io/img/icon.png
|
||||
source:
|
||||
- model: authentik_sources_oauth.oauthsource
|
||||
identifiers:
|
||||
slug: "%(uid)s-source"
|
||||
attrs:
|
||||
name: "%(uid)s-source"
|
||||
provider_type: azuread
|
||||
consumer_key: "%(uid)s"
|
||||
consumer_secret: "%(uid)s"
|
||||
icon: https://goauthentik.io/img/icon.png
|
||||
flow:
|
||||
- model: authentik_flows.flow
|
||||
identifiers:
|
||||
slug: "%(uid)s-flow"
|
||||
attrs:
|
||||
name: "%(uid)s-flow"
|
||||
title: "%(uid)s-flow"
|
||||
designation: authentication
|
||||
background: https://goauthentik.io/img/icon.png
|
||||
user:
|
||||
- model: authentik_core.user
|
||||
identifiers:
|
||||
username: "%(uid)s"
|
||||
attrs:
|
||||
name: "%(uid)s"
|
||||
password: "%(uid)s"
|
||||
- model: authentik_core.user
|
||||
identifiers:
|
||||
username: "%(uid)s-no-password"
|
||||
attrs:
|
||||
name: "%(uid)s"
|
||||
endpoint:
|
||||
- model: authentik_endpoints_connectors_agent.agentconnector
|
||||
id: connector
|
||||
identifiers:
|
||||
name: "%(uid)s"
|
||||
- model: authentik_endpoints_connectors_agent.enrollmenttoken
|
||||
identifiers:
|
||||
name: "%(uid)s"
|
||||
attrs:
|
||||
key: "%(uid)s"
|
||||
connector: !KeyOf connector
|
||||
- model: authentik_core.token
|
||||
identifiers:
|
||||
identifier: "%(uid)s-token"
|
||||
attrs:
|
||||
key: "%(uid)s"
|
||||
user: "%(user)s"
|
||||
intent: api
|
||||
- model: authentik_core.application
|
||||
identifiers:
|
||||
slug: "%(uid)s-app"
|
||||
attrs:
|
||||
name: "%(uid)s-app"
|
||||
icon: https://goauthentik.io/img/icon.png
|
||||
- model: authentik_sources_oauth.oauthsource
|
||||
identifiers:
|
||||
slug: "%(uid)s-source"
|
||||
attrs:
|
||||
name: "%(uid)s-source"
|
||||
provider_type: azuread
|
||||
consumer_key: "%(uid)s"
|
||||
consumer_secret: "%(uid)s"
|
||||
icon: https://goauthentik.io/img/icon.png
|
||||
- model: authentik_flows.flow
|
||||
identifiers:
|
||||
slug: "%(uid)s-flow"
|
||||
attrs:
|
||||
name: "%(uid)s-flow"
|
||||
title: "%(uid)s-flow"
|
||||
designation: authentication
|
||||
background: https://goauthentik.io/img/icon.png
|
||||
- model: authentik_core.user
|
||||
identifiers:
|
||||
username: "%(uid)s"
|
||||
attrs:
|
||||
name: "%(uid)s"
|
||||
password: "%(uid)s"
|
||||
- model: authentik_core.user
|
||||
identifiers:
|
||||
username: "%(uid)s-no-password"
|
||||
attrs:
|
||||
name: "%(uid)s"
|
||||
|
||||
@@ -18,7 +18,7 @@ entries:
|
||||
name: foo
|
||||
title: foo
|
||||
permissions:
|
||||
- permission: authentik_flows.view_flow
|
||||
- permission: view_flow
|
||||
user: !KeyOf user
|
||||
- permission: authentik_flows.view_flow
|
||||
- permission: view_flow
|
||||
role: !KeyOf role
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.test import TransactionTestCase
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.core.models import Token, User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.endpoints.connectors.agent.models import EnrollmentToken
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
|
||||
@@ -30,18 +29,12 @@ class TestBlueprintsV1ConditionalFields(TransactionTestCase):
|
||||
|
||||
def test_user(self):
|
||||
"""Test user"""
|
||||
user = User.objects.filter(username=self.uid).first()
|
||||
user: User = User.objects.filter(username=self.uid).first()
|
||||
self.assertIsNotNone(user)
|
||||
self.assertTrue(user.check_password(self.uid))
|
||||
|
||||
def test_user_null(self):
|
||||
"""Test user"""
|
||||
user = User.objects.filter(username=f"{self.uid}-no-password").first()
|
||||
user: User = User.objects.filter(username=f"{self.uid}-no-password").first()
|
||||
self.assertIsNotNone(user)
|
||||
self.assertFalse(user.has_usable_password())
|
||||
|
||||
def test_enrollment_token(self):
|
||||
"""Test endpoint enrollment token"""
|
||||
token = EnrollmentToken.objects.filter(name=self.uid).first()
|
||||
self.assertIsNotNone(token)
|
||||
self.assertEqual(token.key, self.uid)
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"""Test blueprints v1"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
@@ -45,45 +42,3 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
|
||||
# Ensure objects do not exist
|
||||
self.assertFalse(Flow.objects.filter(slug=flow_slug1))
|
||||
self.assertFalse(Flow.objects.filter(slug=flow_slug2))
|
||||
|
||||
def test_enterprise_license_context_unlicensed(self):
|
||||
"""Test enterprise license context defaults to a false boolean when unlicensed."""
|
||||
license_key = LicenseKey("test", 0, "Test license", 0, 0)
|
||||
|
||||
with patch("authentik.enterprise.license.LicenseKey.get_total", return_value=license_key):
|
||||
importer = Importer.from_string("""
|
||||
version: 1
|
||||
entries:
|
||||
- identifiers:
|
||||
name: enterprise-test
|
||||
slug: enterprise-test
|
||||
model: authentik_flows.flow
|
||||
conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
designation: stage_configuration
|
||||
title: foo
|
||||
""")
|
||||
|
||||
self.assertIs(importer.blueprint.context["goauthentik.io/enterprise/licensed"], False)
|
||||
|
||||
def test_enterprise_license_context_licensed(self):
|
||||
"""Test enterprise license context defaults to a true boolean when licensed."""
|
||||
license_key = LicenseKey("test", 253402300799, "Test license", 1000, 1000)
|
||||
|
||||
with patch("authentik.enterprise.license.LicenseKey.get_total", return_value=license_key):
|
||||
importer = Importer.from_string("""
|
||||
version: 1
|
||||
entries:
|
||||
- identifiers:
|
||||
name: enterprise-test
|
||||
slug: enterprise-test
|
||||
model: authentik_flows.flow
|
||||
conditions:
|
||||
- !Context goauthentik.io/enterprise/licensed
|
||||
attrs:
|
||||
designation: stage_configuration
|
||||
title: foo
|
||||
""")
|
||||
|
||||
self.assertIs(importer.blueprint.context["goauthentik.io/enterprise/licensed"], True)
|
||||
|
||||
@@ -36,7 +36,10 @@ class TestBlueprintsV1RBAC(TransactionTestCase):
|
||||
self.assertTrue(importer.apply())
|
||||
role = Role.objects.filter(name=uid).first()
|
||||
self.assertIsNotNone(role)
|
||||
self.assertEqual(get_perms(role), {"authentik_blueprints.view_blueprintinstance"})
|
||||
self.assertEqual(
|
||||
list(role.group.permissions.all().values_list("codename", flat=True)),
|
||||
["view_blueprintinstance"],
|
||||
)
|
||||
|
||||
def test_object_permission(self):
|
||||
"""Test permissions"""
|
||||
@@ -50,5 +53,5 @@ class TestBlueprintsV1RBAC(TransactionTestCase):
|
||||
user = User.objects.filter(username=uid).first()
|
||||
role = Role.objects.filter(name=uid).first()
|
||||
self.assertIsNotNone(flow)
|
||||
self.assertEqual(get_perms(user, flow), {"authentik_flows.view_flow"})
|
||||
self.assertEqual(get_perms(role, flow), {"authentik_flows.view_flow"})
|
||||
self.assertEqual(get_perms(user, flow), ["view_flow"])
|
||||
self.assertEqual(get_perms(role.group, flow), ["view_flow"])
|
||||
|
||||
@@ -149,7 +149,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
|
||||
instance.status,
|
||||
BlueprintInstanceStatus.UNKNOWN,
|
||||
)
|
||||
apply_blueprint.send(instance.pk).get_result(block=True)
|
||||
apply_blueprint(instance.pk)
|
||||
instance.refresh_from_db()
|
||||
self.assertEqual(instance.last_applied_hash, "")
|
||||
self.assertEqual(
|
||||
|
||||
@@ -9,7 +9,7 @@ from functools import reduce
|
||||
from json import JSONDecodeError, loads
|
||||
from operator import ixor
|
||||
from os import getenv
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, Union
|
||||
from uuid import UUID
|
||||
|
||||
from deepmerge import always_merger
|
||||
@@ -43,6 +43,8 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
||||
continue
|
||||
if _field.read_only:
|
||||
data.pop(field_name, None)
|
||||
if _field.get_initial() == data.get(field_name, None):
|
||||
data.pop(field_name, None)
|
||||
if field_name.endswith("_set"):
|
||||
data.pop(field_name, None)
|
||||
return data
|
||||
@@ -68,17 +70,19 @@ class BlueprintEntryDesiredState(Enum):
|
||||
class BlueprintEntryPermission:
|
||||
"""Describe object-level permissions"""
|
||||
|
||||
permission: str | YAMLTag
|
||||
user: int | YAMLTag | None = field(default=None)
|
||||
role: str | YAMLTag | None = field(default=None)
|
||||
permission: Union[str, "YAMLTag"]
|
||||
user: Union[int, "YAMLTag", None] = field(default=None)
|
||||
role: Union[str, "YAMLTag", None] = field(default=None)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlueprintEntry:
|
||||
"""Single entry of a blueprint"""
|
||||
|
||||
model: str | YAMLTag
|
||||
state: BlueprintEntryDesiredState | YAMLTag = field(default=BlueprintEntryDesiredState.PRESENT)
|
||||
model: Union[str, "YAMLTag"]
|
||||
state: Union[BlueprintEntryDesiredState, "YAMLTag"] = field(
|
||||
default=BlueprintEntryDesiredState.PRESENT
|
||||
)
|
||||
conditions: list[Any] = field(default_factory=list)
|
||||
identifiers: dict[str, Any] = field(default_factory=dict)
|
||||
attrs: dict[str, Any] | None = field(default_factory=dict)
|
||||
@@ -92,7 +96,7 @@ class BlueprintEntry:
|
||||
self.__tag_contexts: list[YAMLTagContext] = []
|
||||
|
||||
@staticmethod
|
||||
def from_model(model: SerializerModel, *extra_identifier_names: str) -> BlueprintEntry:
|
||||
def from_model(model: SerializerModel, *extra_identifier_names: str) -> "BlueprintEntry":
|
||||
"""Convert a SerializerModel instance to a blueprint Entry"""
|
||||
identifiers = {
|
||||
"pk": model.pk,
|
||||
@@ -110,8 +114,8 @@ class BlueprintEntry:
|
||||
def get_tag_context(
|
||||
self,
|
||||
depth: int = 0,
|
||||
context_tag_type: type[YAMLTagContext] | tuple[YAMLTagContext, ...] | None = None,
|
||||
) -> YAMLTagContext:
|
||||
context_tag_type: type["YAMLTagContext"] | tuple["YAMLTagContext", ...] | None = None,
|
||||
) -> "YAMLTagContext":
|
||||
"""Get a YAMLTagContext object located at a certain depth in the tag tree"""
|
||||
if depth < 0:
|
||||
raise ValueError("depth must be a positive number or zero")
|
||||
@@ -126,7 +130,7 @@ class BlueprintEntry:
|
||||
except IndexError as exc:
|
||||
raise ValueError(f"invalid depth: {depth}. Max depth: {len(contexts) - 1}") from exc
|
||||
|
||||
def tag_resolver(self, value: Any, blueprint: Blueprint) -> Any:
|
||||
def tag_resolver(self, value: Any, blueprint: "Blueprint") -> Any:
|
||||
"""Check if we have any special tags that need handling"""
|
||||
val = copy(value)
|
||||
|
||||
@@ -148,23 +152,23 @@ class BlueprintEntry:
|
||||
|
||||
return val
|
||||
|
||||
def get_attrs(self, blueprint: Blueprint) -> dict[str, Any]:
|
||||
def get_attrs(self, blueprint: "Blueprint") -> dict[str, Any]:
|
||||
"""Get attributes of this entry, with all yaml tags resolved"""
|
||||
return self.tag_resolver(self.attrs, blueprint)
|
||||
|
||||
def get_identifiers(self, blueprint: Blueprint) -> dict[str, Any]:
|
||||
def get_identifiers(self, blueprint: "Blueprint") -> dict[str, Any]:
|
||||
"""Get attributes of this entry, with all yaml tags resolved"""
|
||||
return self.tag_resolver(self.identifiers, blueprint)
|
||||
|
||||
def get_state(self, blueprint: Blueprint) -> BlueprintEntryDesiredState:
|
||||
def get_state(self, blueprint: "Blueprint") -> BlueprintEntryDesiredState:
|
||||
"""Get the blueprint state, with yaml tags resolved if present"""
|
||||
return BlueprintEntryDesiredState(self.tag_resolver(self.state, blueprint))
|
||||
|
||||
def get_model(self, blueprint: Blueprint) -> str:
|
||||
def get_model(self, blueprint: "Blueprint") -> str:
|
||||
"""Get the blueprint model, with yaml tags resolved if present"""
|
||||
return str(self.tag_resolver(self.model, blueprint))
|
||||
|
||||
def get_permissions(self, blueprint: Blueprint) -> Generator[BlueprintEntryPermission]:
|
||||
def get_permissions(self, blueprint: "Blueprint") -> Generator[BlueprintEntryPermission]:
|
||||
"""Get permissions of this entry, with all yaml tags resolved"""
|
||||
for perm in self.permissions:
|
||||
yield BlueprintEntryPermission(
|
||||
@@ -173,7 +177,7 @@ class BlueprintEntry:
|
||||
role=self.tag_resolver(perm.role, blueprint),
|
||||
)
|
||||
|
||||
def check_all_conditions_match(self, blueprint: Blueprint) -> bool:
|
||||
def check_all_conditions_match(self, blueprint: "Blueprint") -> bool:
|
||||
"""Check all conditions of this entry match (evaluate to True)"""
|
||||
return all(self.tag_resolver(self.conditions, blueprint))
|
||||
|
||||
@@ -228,7 +232,7 @@ class KeyOf(YAMLTag):
|
||||
|
||||
id_from: str
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: ScalarNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: ScalarNode) -> None:
|
||||
super().__init__()
|
||||
self.id_from = node.value
|
||||
|
||||
@@ -254,7 +258,7 @@ class Env(YAMLTag):
|
||||
key: str
|
||||
default: Any | None
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: ScalarNode | SequenceNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: ScalarNode | SequenceNode) -> None:
|
||||
super().__init__()
|
||||
self.default = None
|
||||
if isinstance(node, ScalarNode):
|
||||
@@ -273,7 +277,7 @@ class File(YAMLTag):
|
||||
path: str
|
||||
default: Any | None
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: ScalarNode | SequenceNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: ScalarNode | SequenceNode) -> None:
|
||||
super().__init__()
|
||||
self.default = None
|
||||
if isinstance(node, ScalarNode):
|
||||
@@ -301,7 +305,7 @@ class Context(YAMLTag):
|
||||
key: str
|
||||
default: Any | None
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: ScalarNode | SequenceNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: ScalarNode | SequenceNode) -> None:
|
||||
super().__init__()
|
||||
self.default = None
|
||||
if isinstance(node, ScalarNode):
|
||||
@@ -324,7 +328,7 @@ class ParseJSON(YAMLTag):
|
||||
|
||||
raw: str
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: ScalarNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: ScalarNode) -> None:
|
||||
super().__init__()
|
||||
self.raw = node.value
|
||||
|
||||
@@ -341,7 +345,7 @@ class Format(YAMLTag):
|
||||
format_string: str
|
||||
args: list[Any]
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: SequenceNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
||||
super().__init__()
|
||||
self.format_string = loader.construct_object(node.value[0])
|
||||
self.args = []
|
||||
@@ -368,7 +372,7 @@ class Find(YAMLTag):
|
||||
model_name: str | YAMLTag
|
||||
conditions: list[list]
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: SequenceNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
||||
super().__init__()
|
||||
self.model_name = loader.construct_object(node.value[0])
|
||||
self.conditions = []
|
||||
@@ -440,7 +444,7 @@ class Condition(YAMLTag):
|
||||
"XNOR": lambda args: not (reduce(ixor, args) if len(args) > 1 else args[0]),
|
||||
}
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: SequenceNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
||||
super().__init__()
|
||||
self.mode = loader.construct_object(node.value[0])
|
||||
self.args = []
|
||||
@@ -474,7 +478,7 @@ class If(YAMLTag):
|
||||
when_true: Any
|
||||
when_false: Any
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: SequenceNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
||||
super().__init__()
|
||||
self.condition = loader.construct_object(node.value[0])
|
||||
if len(node.value) == 1:
|
||||
@@ -514,7 +518,7 @@ class Enumerate(YAMLTag, YAMLTagContext):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: SequenceNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
||||
super().__init__()
|
||||
self.iterable = loader.construct_object(node.value[0])
|
||||
self.output_body = loader.construct_object(node.value[1])
|
||||
@@ -580,7 +584,7 @@ class EnumeratedItem(YAMLTag):
|
||||
|
||||
_SUPPORTED_CONTEXT_TAGS = (Enumerate,)
|
||||
|
||||
def __init__(self, _loader: BlueprintLoader, node: ScalarNode) -> None:
|
||||
def __init__(self, _loader: "BlueprintLoader", node: ScalarNode) -> None:
|
||||
super().__init__()
|
||||
self.depth = int(node.value)
|
||||
|
||||
@@ -636,7 +640,7 @@ class AtIndex(YAMLTag):
|
||||
attribute: int | str | YAMLTag
|
||||
default: Any | UNSET
|
||||
|
||||
def __init__(self, loader: BlueprintLoader, node: SequenceNode) -> None:
|
||||
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
||||
super().__init__()
|
||||
self.obj = loader.construct_object(node.value[0])
|
||||
self.attribute = loader.construct_object(node.value[1])
|
||||
@@ -753,7 +757,7 @@ class EntryInvalidError(SentryIgnoredException):
|
||||
@staticmethod
|
||||
def from_entry(
|
||||
msg_or_exc: str | Exception, entry: BlueprintEntry, *args, **kwargs
|
||||
) -> EntryInvalidError:
|
||||
) -> "EntryInvalidError":
|
||||
"""Create EntryInvalidError with the context of an entry"""
|
||||
error = EntryInvalidError(msg_or_exc, *args, **kwargs)
|
||||
if isinstance(msg_or_exc, ValidationError):
|
||||
|
||||
@@ -15,7 +15,9 @@ from django.db.models import Model
|
||||
from django.db.models.query_utils import Q
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
from guardian.models import RoleObjectPermission
|
||||
from django_channels_postgres.models import GroupChannel, Message
|
||||
from guardian.models import UserObjectPermission
|
||||
from guardian.shortcuts import assign_perm
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.serializers import BaseSerializer, Serializer
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
@@ -40,16 +42,55 @@ from authentik.core.models import (
|
||||
User,
|
||||
UserSourceConnection,
|
||||
)
|
||||
from authentik.endpoints.models import Connector
|
||||
from authentik.endpoints.connectors.agent.models import (
|
||||
AgentDeviceConnection,
|
||||
AppleNonce,
|
||||
DeviceAuthenticationToken,
|
||||
)
|
||||
from authentik.endpoints.connectors.agent.models import (
|
||||
DeviceToken as EndpointDeviceToken,
|
||||
)
|
||||
from authentik.endpoints.models import Connector, Device, DeviceConnection, DeviceFactSnapshot
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.enterprise.models import LicenseUsage
|
||||
from authentik.enterprise.providers.google_workspace.models import (
|
||||
GoogleWorkspaceProviderGroup,
|
||||
GoogleWorkspaceProviderUser,
|
||||
)
|
||||
from authentik.enterprise.providers.microsoft_entra.models import (
|
||||
MicrosoftEntraProviderGroup,
|
||||
MicrosoftEntraProviderUser,
|
||||
)
|
||||
from authentik.enterprise.providers.ssf.models import StreamEvent
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
||||
EndpointDevice,
|
||||
EndpointDeviceConnection,
|
||||
)
|
||||
from authentik.events.logs import LogEvent, capture_logs
|
||||
from authentik.events.utils import cleanse_dict
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.lib.models import InternallyManagedMixin, SerializerModel
|
||||
from authentik.flows.models import FlowToken, Stage
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.reflection import get_apps
|
||||
from authentik.outposts.models import OutpostServiceConnection
|
||||
from authentik.policies.models import Policy, PolicyBindingModel
|
||||
from authentik.policies.reputation.models import Reputation
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
AuthorizationCode,
|
||||
DeviceToken,
|
||||
RefreshToken,
|
||||
)
|
||||
from authentik.providers.proxy.models import ProxySession
|
||||
from authentik.providers.rac.models import ConnectionToken
|
||||
from authentik.providers.saml.models import SAMLSession
|
||||
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
|
||||
from authentik.rbac.models import Role
|
||||
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
|
||||
from authentik.stages.consent.models import UserConsent
|
||||
from authentik.tasks.models import Task, TaskLog
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
# Context set when the serializer is created in a blueprint context
|
||||
# Update website/docs/customize/blueprints/v1/models.md when used
|
||||
@@ -69,7 +110,7 @@ def excluded_models() -> list[type[Model]]:
|
||||
DjangoGroup,
|
||||
ContentType,
|
||||
Permission,
|
||||
RoleObjectPermission,
|
||||
UserObjectPermission,
|
||||
# Base classes
|
||||
Provider,
|
||||
Source,
|
||||
@@ -84,16 +125,49 @@ def excluded_models() -> list[type[Model]]:
|
||||
# Classes that have other dependencies
|
||||
Session,
|
||||
AuthenticatedSession,
|
||||
# Classes which are only internally managed
|
||||
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
|
||||
FlowToken,
|
||||
LicenseUsage,
|
||||
SCIMProviderGroup,
|
||||
SCIMProviderUser,
|
||||
Tenant,
|
||||
Task,
|
||||
TaskLog,
|
||||
ConnectionToken,
|
||||
AuthorizationCode,
|
||||
AccessToken,
|
||||
RefreshToken,
|
||||
ProxySession,
|
||||
Reputation,
|
||||
WebAuthnDeviceType,
|
||||
SCIMSourceUser,
|
||||
SCIMSourceGroup,
|
||||
GoogleWorkspaceProviderUser,
|
||||
GoogleWorkspaceProviderGroup,
|
||||
MicrosoftEntraProviderUser,
|
||||
MicrosoftEntraProviderGroup,
|
||||
EndpointDevice,
|
||||
EndpointDeviceConnection,
|
||||
EndpointDeviceToken,
|
||||
Device,
|
||||
DeviceConnection,
|
||||
DeviceAuthenticationToken,
|
||||
AppleNonce,
|
||||
AgentDeviceConnection,
|
||||
DeviceFactSnapshot,
|
||||
DeviceToken,
|
||||
StreamEvent,
|
||||
UserConsent,
|
||||
SAMLSession,
|
||||
Message,
|
||||
GroupChannel,
|
||||
)
|
||||
|
||||
|
||||
def is_model_allowed(model: type[Model]) -> bool:
|
||||
"""Check if model is allowed"""
|
||||
return (
|
||||
model not in excluded_models()
|
||||
and issubclass(model, SerializerModel | BaseMetaModel)
|
||||
and not issubclass(model, InternallyManagedMixin)
|
||||
)
|
||||
return model not in excluded_models() and issubclass(model, SerializerModel | BaseMetaModel)
|
||||
|
||||
|
||||
class DoRollback(SentryIgnoredException):
|
||||
@@ -139,20 +213,13 @@ class Importer:
|
||||
|
||||
def default_context(self):
|
||||
"""Default context"""
|
||||
context = {
|
||||
return {
|
||||
"goauthentik.io/enterprise/licensed": LicenseKey.get_total().status().is_valid,
|
||||
"goauthentik.io/rbac/models": rbac_models(),
|
||||
"goauthentik.io/enterprise/licensed": False,
|
||||
}
|
||||
try:
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
|
||||
context["goauthentik.io/enterprise/licensed"] = LicenseKey.get_total().status().is_valid
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
return context
|
||||
|
||||
@staticmethod
|
||||
def from_string(yaml_input: str, context: dict | None = None) -> Importer:
|
||||
def from_string(yaml_input: str, context: dict | None = None) -> "Importer":
|
||||
"""Parse YAML string and create blueprint importer from it"""
|
||||
import_dict = load(yaml_input, BlueprintLoader)
|
||||
try:
|
||||
@@ -270,7 +337,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,
|
||||
@@ -288,7 +355,7 @@ class Importer:
|
||||
)
|
||||
else:
|
||||
self.logger.debug(
|
||||
"Initialized new serializer instance",
|
||||
"Initialised new serializer instance",
|
||||
model=model,
|
||||
**cleanse_dict(updated_identifiers),
|
||||
)
|
||||
@@ -327,12 +394,10 @@ class Importer:
|
||||
"""Apply object-level permissions for an entry"""
|
||||
for perm in entry.get_permissions(self._import):
|
||||
if perm.user is not None:
|
||||
User.objects.get(pk=perm.user).assign_perms_to_managed_role(
|
||||
perm.permission, instance
|
||||
)
|
||||
assign_perm(perm.permission, User.objects.get(pk=perm.user), instance)
|
||||
if perm.role is not None:
|
||||
role = Role.objects.get(pk=perm.role)
|
||||
role.assign_perms(perm.permission, obj=instance)
|
||||
role.assign_permission(perm.permission, obj=instance)
|
||||
|
||||
def apply(self) -> bool:
|
||||
"""Apply (create/update) models yaml, in database transaction"""
|
||||
|
||||
@@ -23,7 +23,7 @@ class ApplyBlueprintMetaSerializer(PassiveSerializer):
|
||||
|
||||
# We cannot override `instance` as that will confuse rest_framework
|
||||
# and make it attempt to update the instance
|
||||
blueprint_instance: BlueprintInstance
|
||||
blueprint_instance: "BlueprintInstance"
|
||||
|
||||
def validate(self, attrs):
|
||||
from authentik.blueprints.models import BlueprintInstance
|
||||
@@ -37,21 +37,14 @@ class ApplyBlueprintMetaSerializer(PassiveSerializer):
|
||||
return super().validate(attrs)
|
||||
|
||||
def create(self, validated_data: dict) -> MetaResult:
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.blueprints.v1.tasks import apply_blueprint
|
||||
|
||||
if not self.blueprint_instance:
|
||||
LOGGER.info("Blueprint does not exist, but not required")
|
||||
return MetaResult()
|
||||
LOGGER.debug("Applying blueprint from meta model", blueprint=self.blueprint_instance)
|
||||
|
||||
# Apply blueprint directly using Importer to avoid task context requirements
|
||||
# and prevent deadlocks when called from within another blueprint task
|
||||
blueprint_content = self.blueprint_instance.retrieve()
|
||||
importer = Importer.from_string(blueprint_content, self.blueprint_instance.context)
|
||||
valid, logs = importer.validate()
|
||||
[log.log() for log in logs]
|
||||
if valid:
|
||||
importer.apply()
|
||||
apply_blueprint(self.blueprint_instance.pk)
|
||||
return MetaResult()
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.db import DatabaseError, InternalError, ProgrammingError
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_dramatiq_postgres.middleware import CurrentTaskNotFound
|
||||
from dramatiq.actor import actor
|
||||
from dramatiq.middleware import Middleware
|
||||
from structlog.stdlib import get_logger
|
||||
@@ -39,6 +40,7 @@ from authentik.events.utils import sanitize_dict
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.tasks.apps import PRIORITY_HIGH
|
||||
from authentik.tasks.middleware import CurrentTask
|
||||
from authentik.tasks.models import Task
|
||||
from authentik.tasks.schedules.models import Schedule
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
@@ -189,7 +191,10 @@ def check_blueprint_v1_file(blueprint: BlueprintFile):
|
||||
|
||||
@actor(description=_("Apply single blueprint."))
|
||||
def apply_blueprint(instance_pk: UUID):
|
||||
self = CurrentTask.get_task()
|
||||
try:
|
||||
self = CurrentTask.get_task()
|
||||
except CurrentTaskNotFound:
|
||||
self = Task()
|
||||
self.set_uid(str(instance_pk))
|
||||
instance: BlueprintInstance | None = None
|
||||
try:
|
||||
|
||||
@@ -6,12 +6,7 @@ from django.db import models
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import (
|
||||
CharField,
|
||||
ChoiceField,
|
||||
ListField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.fields import CharField, ChoiceField, ListField, SerializerMethodField
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
@@ -21,7 +16,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer, ThemedUrlsSerializer
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.rbac.filters import SecretKeyFilter
|
||||
from authentik.tenants.api.settings import FlagJSONField
|
||||
from authentik.tenants.flags import Flag
|
||||
@@ -64,7 +59,6 @@ class BrandSerializer(ModelSerializer):
|
||||
"flow_unenrollment",
|
||||
"flow_user_settings",
|
||||
"flow_device_code",
|
||||
"flow_lockdown",
|
||||
"default_application",
|
||||
"web_certificate",
|
||||
"client_certificates",
|
||||
@@ -96,9 +90,7 @@ class CurrentBrandSerializer(PassiveSerializer):
|
||||
matched_domain = CharField(source="domain")
|
||||
branding_title = CharField()
|
||||
branding_logo = CharField(source="branding_logo_url")
|
||||
branding_logo_themed_urls = ThemedUrlsSerializer(read_only=True, allow_null=True)
|
||||
branding_favicon = CharField(source="branding_favicon_url")
|
||||
branding_favicon_themed_urls = ThemedUrlsSerializer(read_only=True, allow_null=True)
|
||||
branding_custom_css = CharField()
|
||||
ui_footer_links = ListField(
|
||||
child=FooterLinkSerializer(),
|
||||
@@ -118,7 +110,6 @@ class CurrentBrandSerializer(PassiveSerializer):
|
||||
flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False)
|
||||
flow_user_settings = CharField(source="flow_user_settings.slug", required=False)
|
||||
flow_device_code = CharField(source="flow_device_code.slug", required=False)
|
||||
flow_lockdown = CharField(source="flow_lockdown.slug", required=False)
|
||||
|
||||
default_locale = CharField(read_only=True)
|
||||
flags = SerializerMethodField()
|
||||
@@ -126,8 +117,10 @@ class CurrentBrandSerializer(PassiveSerializer):
|
||||
@extend_schema_field(field=FlagJSONField)
|
||||
def get_flags(self, _):
|
||||
values = {}
|
||||
for flag in Flag.available(visibility="public"):
|
||||
values[flag().key] = flag.get()
|
||||
for flag in Flag.available():
|
||||
_flag = flag()
|
||||
if _flag.visibility == "public":
|
||||
values[_flag.key] = _flag.get()
|
||||
return values
|
||||
|
||||
|
||||
@@ -156,7 +149,6 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
|
||||
"flow_unenrollment",
|
||||
"flow_user_settings",
|
||||
"flow_device_code",
|
||||
"flow_lockdown",
|
||||
"web_certificate",
|
||||
"client_certificates",
|
||||
]
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-14 02:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_brands", "0011_alter_brand_branding_default_flow_background_and_more"),
|
||||
("authentik_flows", "0031_alter_flow_layout"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="brand",
|
||||
name="flow_lockdown",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="brand_lockdown",
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -58,9 +58,6 @@ class Brand(SerializerModel):
|
||||
flow_device_code = models.ForeignKey(
|
||||
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code"
|
||||
)
|
||||
flow_lockdown = models.ForeignKey(
|
||||
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_lockdown"
|
||||
)
|
||||
|
||||
default_application = models.ForeignKey(
|
||||
"authentik_core.Application",
|
||||
@@ -92,35 +89,13 @@ class Brand(SerializerModel):
|
||||
"""Get branding_logo URL"""
|
||||
return get_file_manager(FileUsage.MEDIA).file_url(self.branding_logo)
|
||||
|
||||
def branding_logo_themed_urls(self) -> dict[str, str] | None:
|
||||
"""Get themed URLs for branding_logo if it contains %(theme)s"""
|
||||
return get_file_manager(FileUsage.MEDIA).themed_urls(self.branding_logo)
|
||||
|
||||
def branding_favicon_url(self) -> str:
|
||||
"""Get branding_favicon URL"""
|
||||
return get_file_manager(FileUsage.MEDIA).file_url(self.branding_favicon)
|
||||
|
||||
def branding_favicon_themed_urls(self) -> dict[str, str] | None:
|
||||
"""Get themed URLs for branding_favicon if it contains %(theme)s"""
|
||||
return get_file_manager(FileUsage.MEDIA).themed_urls(self.branding_favicon)
|
||||
|
||||
def branding_default_flow_background_url(self, request=None, use_cache: bool = True) -> str:
|
||||
def branding_default_flow_background_url(self) -> str:
|
||||
"""Get branding_default_flow_background URL"""
|
||||
return get_file_manager(FileUsage.MEDIA).file_url(
|
||||
self.branding_default_flow_background,
|
||||
request,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
|
||||
def branding_default_flow_background_themed_urls(
|
||||
self, request=None, use_cache: bool = True
|
||||
) -> dict[str, str] | None:
|
||||
"""Get themed URLs for branding_default_flow_background if it contains %(theme)s"""
|
||||
return get_file_manager(FileUsage.MEDIA).themed_urls(
|
||||
self.branding_default_flow_background,
|
||||
request,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
return get_file_manager(FileUsage.MEDIA).file_url(self.branding_default_flow_background)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user