mirror of
https://github.com/goauthentik/authentik
synced 2026-05-06 07:02:51 +02:00
Compare commits
2 Commits
core/separ
...
devcontain
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d8531ac0f | ||
|
|
c3db636151 |
@@ -1,35 +0,0 @@
|
||||
[licenses]
|
||||
allow = ["Apache-2.0", "MIT", "MPL-2.0", "Unicode-3.0"]
|
||||
|
||||
[licenses.private]
|
||||
ignore = true
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
wildcards = "deny"
|
||||
[bans.workspace-dependencies]
|
||||
duplicates = "deny"
|
||||
include-path-dependencies = true
|
||||
unused = "deny"
|
||||
|
||||
# No non-FIPS compliant dependencies
|
||||
[[bans.deny]]
|
||||
name = "native-tls"
|
||||
[[bans.deny]]
|
||||
name = "openssl"
|
||||
[[bans.deny]]
|
||||
name = "openssl-sys"
|
||||
[[bans.deny]]
|
||||
name = "ring"
|
||||
[[bans.features]]
|
||||
allow = [
|
||||
"alloc",
|
||||
"aws-lc-sys",
|
||||
"default",
|
||||
"fips",
|
||||
"prebuilt-nasm",
|
||||
"ring-io",
|
||||
"ring-sig-verify",
|
||||
]
|
||||
name = "aws-lc-rs"
|
||||
exact = true
|
||||
@@ -1,16 +0,0 @@
|
||||
comment_width = 100
|
||||
format_code_in_doc_comments = true
|
||||
format_strings = true
|
||||
group_imports = "StdExternalCrate"
|
||||
hex_literal_case = "Lower"
|
||||
imports_granularity = "Crate"
|
||||
max_width = 100
|
||||
newline_style = "Unix"
|
||||
normalize_comments = true
|
||||
normalize_doc_attributes = true
|
||||
reorder_impl_items = true
|
||||
style_edition = "2024"
|
||||
use_field_init_shorthand = true
|
||||
use_try_shorthand = true
|
||||
where_single_line = true
|
||||
wrap_comments = true
|
||||
63
.devcontainer/Dockerfile
Normal file
63
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,63 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Start from the same FIPS Python base as production (python-base stage)
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff
|
||||
|
||||
USER root
|
||||
|
||||
# Setup environment matching production python-base stage
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_NATIVE_TLS=1 \
|
||||
UV_PYTHON_DOWNLOADS=0
|
||||
|
||||
WORKDIR /ak-root
|
||||
|
||||
# Copy uv package manager
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.7@sha256:ba4857bf2a068e9bc0e64eed8563b065908a4cd6bfb66b531a9c424c8e25e142 /uv /uvx /bin/
|
||||
|
||||
# Install build dependencies
|
||||
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 && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Build essentials
|
||||
build-essential pkg-config libffi-dev git binutils \
|
||||
# cryptography
|
||||
curl \
|
||||
# libxml
|
||||
libxslt-dev zlib1g-dev \
|
||||
# postgresql
|
||||
libpq-dev \
|
||||
# python-kadmin-rs and kerberos testing
|
||||
clang libkrb5-dev sccache krb5-kdc krb5-admin-server \
|
||||
# xmlsec
|
||||
libltdl-dev \
|
||||
# runit (for chpst command used by lifecycle/ak)
|
||||
runit \
|
||||
# sudo (required by devcontainer features)
|
||||
sudo && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Environment for building native Python packages
|
||||
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec" \
|
||||
RUSTUP_PERMIT_COPY_RENAME="true"
|
||||
|
||||
# Create authentik user with proper home directory (required for devcontainer features)
|
||||
RUN adduser --disabled-password --gecos "" --uid 1000 --home /home/authentik authentik && \
|
||||
mkdir -p /certs /media /ak-root && \
|
||||
chown -R authentik:authentik /certs /media /ak-root /home/authentik && \
|
||||
echo "authentik ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/authentik
|
||||
|
||||
# FIPS configuration for Go development
|
||||
# Don't set GOFIPS/GOFIPS140 globally to avoid breaking Go tools like docker-compose
|
||||
# These will be set when building/running authentik Go code (see lifecycle/ak and Makefile)
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
# Set TMPDIR for PID files and temp data
|
||||
# Use /tmp instead of /dev/shm for development because go run needs to execute binaries
|
||||
ENV TMPDIR=/tmp
|
||||
|
||||
USER authentik
|
||||
68
.devcontainer/devcontainer.json
Normal file
68
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "authentik",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/ak-root",
|
||||
"containerUser": "authentik",
|
||||
"remoteUser": "authentik",
|
||||
"shutdownAction": "stopCompose",
|
||||
"containerEnv": {
|
||||
"LOCAL_PROJECT_DIR": "/ak-root"
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/go:1": {
|
||||
"version": "1.24"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "24"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"version": "latest",
|
||||
"moby": false
|
||||
}
|
||||
},
|
||||
"mounts": [],
|
||||
"forwardPorts": [9000, 9443],
|
||||
"portsAttributes": {
|
||||
"8000": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"3963": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"35151": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"9901": {
|
||||
"onAutoForward": "ignore"
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "bash .devcontainer/setup.sh",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"bashmish.es6-string-css",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"golang.go",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.isort",
|
||||
"ms-python.pylint",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"redhat.vscode-yaml",
|
||||
"Tobermory.es6-string-html",
|
||||
"charliermarsh.ruff"
|
||||
],
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/ak-root/.venv/bin/python",
|
||||
"python.terminal.activateEnvironment": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
.devcontainer/docker-compose.yml
Normal file
50
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,50 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
user: authentik
|
||||
privileged: true
|
||||
volumes:
|
||||
- ../:/ak-root
|
||||
entrypoint: []
|
||||
command: sleep infinity
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
env_file: .env
|
||||
environment:
|
||||
PATH: "/ak-root/.venv/bin:${PATH}"
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9443:9443"
|
||||
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:16
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d authentik -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
env_file: .env
|
||||
command: ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"]
|
||||
|
||||
s3:
|
||||
image: docker.io/zenko/cloudserver
|
||||
env_file: .env
|
||||
environment:
|
||||
REMOTE_MANAGEMENT_DISABLE: "1"
|
||||
ports:
|
||||
- "8020:8000"
|
||||
volumes:
|
||||
- s3-data:/usr/src/app/localData
|
||||
- s3-metadata:/usr/src/app/localMetadata
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
s3-data:
|
||||
s3-metadata:
|
||||
37
.devcontainer/setup.sh
Executable file
37
.devcontainer/setup.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "======================================"
|
||||
echo "Running authentik devcontainer setup"
|
||||
echo "======================================"
|
||||
|
||||
echo ""
|
||||
echo "Step 1/5: Installing dependencies"
|
||||
make install
|
||||
|
||||
echo ""
|
||||
echo "Step 2/5: Generating development config"
|
||||
make gen-dev-config
|
||||
|
||||
echo ""
|
||||
echo "Step 3/5: Running database migrations"
|
||||
make migrate
|
||||
|
||||
echo ""
|
||||
echo "Step 4/5: Generating API clients"
|
||||
make gen
|
||||
|
||||
echo ""
|
||||
echo "Step 5/5: Building web assets"
|
||||
make web
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Setup complete!"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "You can now run:"
|
||||
echo " - 'make run-server' to start the backend server"
|
||||
echo " - 'make run-worker' to start the worker (must be ran once after initial setup)"
|
||||
echo " - 'make web-watch' for live web development"
|
||||
echo ""
|
||||
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
.github/actions/setup/action.yml
vendored
54
.github/actions/setup/action.yml
vendored
@@ -4,7 +4,7 @@ description: "Setup authentik testing environment"
|
||||
inputs:
|
||||
dependencies:
|
||||
description: "List of dependencies to setup"
|
||||
default: "system,python,rust,node,go,runtime"
|
||||
default: "system,python,node,go,runtime"
|
||||
postgresql_version:
|
||||
description: "Optional postgresql image tag"
|
||||
default: "16"
|
||||
@@ -12,82 +12,52 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install apt deps & cleanup
|
||||
- name: Install apt deps
|
||||
if: ${{ contains(inputs.dependencies, 'system') || contains(inputs.dependencies, 'python') }}
|
||||
shell: bash
|
||||
run: |
|
||||
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 krb5-multidev libkrb5-dev heimdal-multidev libclang-dev krb5-kdc krb5-user krb5-admin-server
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v5
|
||||
uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # 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: "pyproject.toml"
|
||||
- name: Install Python deps
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
shell: bash
|
||||
run: uv sync --all-extras --dev --frozen
|
||||
- name: Setup rust (stable)
|
||||
if: ${{ contains(inputs.dependencies, 'rust') && !contains(inputs.dependencies, 'rust-nightly') }}
|
||||
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
|
||||
with:
|
||||
rustflags: ""
|
||||
- name: Setup rust (nightly)
|
||||
if: ${{ contains(inputs.dependencies, 'rust-nightly') }}
|
||||
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2
|
||||
with:
|
||||
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
||||
- name: Setup node (web)
|
||||
- name: Setup node
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v4
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v4
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Setup node (root)
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v4
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: package-lock.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Install Node deps
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
shell: bash
|
||||
run: npm ci
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Setup go
|
||||
if: ${{ contains(inputs.dependencies, 'go') }}
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v5
|
||||
with:
|
||||
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
|
||||
run: |
|
||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||
docker compose -f .github/actions/setup/compose.yml up -d
|
||||
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}
|
||||
|
||||
@@ -11,6 +11,11 @@ services:
|
||||
ports:
|
||||
- 5432:5432
|
||||
restart: always
|
||||
redis:
|
||||
image: docker.io/library/redis:7
|
||||
ports:
|
||||
- 6379:6379
|
||||
restart: always
|
||||
s3:
|
||||
container_name: s3
|
||||
image: docker.io/zenko/cloudserver
|
||||
@@ -22,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@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
with:
|
||||
files: ${{ inputs.files }}
|
||||
flags: ${{ inputs.flags }}
|
||||
use_oidc: true
|
||||
- uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
- uses: codecov/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
|
||||
|
||||
1
.github/codespell-dictionary.txt
vendored
Normal file
1
.github/codespell-dictionary.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
authentic->authentik
|
||||
32
.github/codespell-words.txt
vendored
Normal file
32
.github/codespell-words.txt
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
akadmin
|
||||
asgi
|
||||
assertIn
|
||||
authentik
|
||||
authn
|
||||
crate
|
||||
docstrings
|
||||
entra
|
||||
goauthentik
|
||||
gunicorn
|
||||
hass
|
||||
jwe
|
||||
jwks
|
||||
keypair
|
||||
keypairs
|
||||
kubernetes
|
||||
oidc
|
||||
ontext
|
||||
openid
|
||||
passwordless
|
||||
plex
|
||||
saml
|
||||
scim
|
||||
singed
|
||||
slo
|
||||
sso
|
||||
totp
|
||||
traefik
|
||||
# https://github.com/codespell-project/codespell/issues/1224
|
||||
upToDate
|
||||
warmup
|
||||
webauthn
|
||||
17
.github/dependabot.yml
vendored
17
.github/dependabot.yml
vendored
@@ -38,21 +38,6 @@ updates:
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rust
|
||||
|
||||
- package-ecosystem: rust-toolchain
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "core:"
|
||||
labels:
|
||||
- dependencies
|
||||
|
||||
#endregion
|
||||
|
||||
#region Web
|
||||
|
||||
- package-ecosystem: npm
|
||||
@@ -249,7 +234,7 @@ updates:
|
||||
|
||||
- package-ecosystem: docker
|
||||
directories:
|
||||
- /lifecycle/container
|
||||
- /
|
||||
- /website
|
||||
schedule:
|
||||
interval: daily
|
||||
|
||||
@@ -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,35 +56,35 @@ jobs:
|
||||
release: ${{ inputs.release }}
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- name: make empty clients
|
||||
if: ${{ inputs.release }}
|
||||
run: |
|
||||
mkdir -p ./gen-ts-api
|
||||
mkdir -p ./gen-go-api
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Generate API Clients
|
||||
run: |
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: generate ts client
|
||||
run: make gen-client-ts
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.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 }}
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
platforms: linux/${{ inputs.image_arch }}
|
||||
cache-from: type=registry,ref=${{ steps.ev.outputs.attestImageNames }}:buildcache-${{ inputs.image_arch }}
|
||||
cache-to: ${{ steps.ev.outputs.cacheTo }}
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@8aac06098a12365ccdf99372dcfb453ccce8a0b0 # 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 }}
|
||||
|
||||
10
.github/workflows/api-ts-publish.yml
vendored
10
.github/workflows/api-ts-publish.yml
vendored
@@ -18,14 +18,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
run: |
|
||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v7
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
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@53b83947a5a98c8d113130e565377fae1a50d02f # 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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4
|
||||
- uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/website/api/.docusaurus
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: npm run build -w api
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v4
|
||||
- uses: actions/upload-artifact@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@53b83947a5a98c8d113130e565377fae1a50d02f # 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@53b83947a5a98c8d113130e565377fae1a50d02f # 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@53b83947a5a98c8d113130e565377fae1a50d02f # 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@53b83947a5a98c8d113130e565377fae1a50d02f # 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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.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:
|
||||
|
||||
18
.github/workflows/ci-main-daily.yml
vendored
18
.github/workflows/ci-main-daily.yml
vendored
@@ -6,10 +6,6 @@ on:
|
||||
schedule:
|
||||
# Every night at 3am
|
||||
- cron: "0 3 * * *"
|
||||
pull_request:
|
||||
paths:
|
||||
# Needs to refer to itself
|
||||
- .github/workflows/ci-main-daily.yml
|
||||
|
||||
jobs:
|
||||
test-container:
|
||||
@@ -19,14 +15,14 @@ jobs:
|
||||
matrix:
|
||||
version:
|
||||
- docs
|
||||
- version-2025-12
|
||||
- version-2025-10
|
||||
- version-2025-4
|
||||
- version-2025-2
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- run: |
|
||||
current="$(pwd)"
|
||||
dir="/tmp/authentik/${{ matrix.version }}"
|
||||
mkdir -p "${dir}/lifecycle/container"
|
||||
cd "${dir}"
|
||||
wget "https://${{ matrix.version }}.goauthentik.io/docker-compose.yml" -O "${dir}/lifecycle/container/compose.yml"
|
||||
"${current}/scripts/test_docker.sh"
|
||||
mkdir -p $dir
|
||||
cd $dir
|
||||
wget https://${{ matrix.version }}.goauthentik.io/docker-compose.yml
|
||||
${current}/scripts/test_docker.sh
|
||||
|
||||
140
.github/workflows/ci-main.yml
vendored
140
.github/workflows/ci-main.yml
vendored
@@ -28,50 +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-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: generate schema
|
||||
run: make migrate gen-build
|
||||
- name: ensure schema is up-to-date
|
||||
run: git diff --exit-code -- schema.yml blueprints/schema.json
|
||||
run: uv run make ci-${{ matrix.job }}
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run migrations
|
||||
@@ -97,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
|
||||
@@ -110,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}
|
||||
@@ -162,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:
|
||||
@@ -182,11 +156,11 @@ 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
|
||||
@@ -213,25 +187,21 @@ jobs:
|
||||
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
||||
- name: ldap
|
||||
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
||||
- name: ws-fed
|
||||
glob: tests/e2e/test_provider_ws_fed*
|
||||
- name: radius
|
||||
glob: tests/e2e/test_provider_radius*
|
||||
- name: scim
|
||||
glob: tests/e2e/test_source_scim*
|
||||
- name: flows
|
||||
glob: tests/e2e/test_flows*
|
||||
- name: endpoints
|
||||
glob: tests/e2e/test_endpoints_*
|
||||
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)
|
||||
run: |
|
||||
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
|
||||
docker compose -f tests/e2e/docker-compose.yml up -d --quiet-pull
|
||||
- id: cache-web
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4
|
||||
uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v4
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
@@ -251,82 +221,10 @@ jobs:
|
||||
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: basic
|
||||
glob: tests/openid_conformance/test_basic.py
|
||||
- name: implicit
|
||||
glob: tests/openid_conformance/test_implicit.py
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup e2e env (chrome, etc)
|
||||
run: |
|
||||
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
|
||||
- name: Setup conformance suite
|
||||
run: |
|
||||
docker compose -f tests/openid_conformance/compose.yml up -d --quiet-pull
|
||||
- id: cache-web
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/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
|
||||
run: |
|
||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
uv run coverage xml
|
||||
- uses: ./.github/actions/test-results
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
flags: conformance
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: conformance-certification-${{ matrix.job.name }}
|
||||
path: tests/openid_conformance/exports/
|
||||
test-rust:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: rust
|
||||
- name: run tests
|
||||
run: |
|
||||
cargo llvm-cov --no-report nextest --workspace
|
||||
cargo llvm-cov report --codecov --output-path target/llvm-cov-target/rust.json
|
||||
- uses: ./.github/actions/test-results
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
files: target/llvm-cov-target/rust.json
|
||||
flags: rust
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: test-rust
|
||||
path: target/llvm-cov-target/rust.json
|
||||
ci-core-mark:
|
||||
if: always()
|
||||
needs:
|
||||
- lint
|
||||
- test-gen-build
|
||||
- test-migrations
|
||||
- test-migrations-from-stable
|
||||
- test-unittest
|
||||
@@ -362,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
|
||||
|
||||
28
.github/workflows/ci-outpost.yml
vendored
28
.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@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Prepare and generate API
|
||||
@@ -42,8 +42,8 @@ jobs:
|
||||
test-unittest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Setup authentik env
|
||||
@@ -86,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
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
|
||||
- name: Login to Container Registry
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -111,10 +111,10 @@ jobs:
|
||||
run: make gen-client-go
|
||||
- name: Build Docker Image
|
||||
id: push
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: lifecycle/container/${{ matrix.type }}.Dockerfile
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
context: .
|
||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
|
||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
with:
|
||||
@@ -145,13 +145,13 @@ 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@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
|
||||
12
.github/workflows/ci-web.yml
vendored
12
.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@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: ${{ matrix.project }}/package.json
|
||||
cache: "npm"
|
||||
@@ -48,8 +48,8 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
@@ -76,8 +76,8 @@ jobs:
|
||||
- ci-web-mark
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
|
||||
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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
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@03c976c29803442fc4040a9de5509669e7759b81 # 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@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # 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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
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@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # 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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # 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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # 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@53b83947a5a98c8d113130e565377fae1a50d02f # 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@22103cc46bda19c2b464ffe86db46df6922fd323 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
|
||||
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # 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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # 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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # 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@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # 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: |
|
||||
|
||||
80
.github/workflows/release-publish.yml
vendored
80
.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: website/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
|
||||
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
||||
id: attest
|
||||
if: true
|
||||
with:
|
||||
@@ -83,19 +83,14 @@ jobs:
|
||||
- radius
|
||||
- rac
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # 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,33 +98,33 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/${{ matrix.type }},authentik/${{ matrix.type }}
|
||||
- name: Generate API Clients
|
||||
- name: make empty clients
|
||||
run: |
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
mkdir -p ./gen-ts-api
|
||||
mkdir -p ./gen-go-api
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.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 }}
|
||||
@@ -151,26 +146,19 @@ jobs:
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # 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: Generate API Clients
|
||||
run: |
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
npm run build-proxy
|
||||
- name: Build outpost
|
||||
run: |
|
||||
@@ -180,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 }}
|
||||
@@ -198,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@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.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 }}
|
||||
@@ -214,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
|
||||
@@ -230,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
|
||||
@@ -244,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@dab6548b3c03c4717878099e43782cf5be654289 # 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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # 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@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
tag_name: "version/${{ inputs.version }}"
|
||||
@@ -115,17 +108,17 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
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@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # 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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # 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@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # 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@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||
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,15 +21,15 @@ jobs:
|
||||
steps:
|
||||
- id: generate_token
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
|
||||
- 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
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
make web-check-compile
|
||||
- name: Create Pull Request
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v7
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: extract-compile-backend-translation
|
||||
|
||||
24
.gitignore
vendored
24
.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
|
||||
|
||||
@@ -232,5 +211,4 @@ source_docs/
|
||||
/vendor/
|
||||
|
||||
### Docker ###
|
||||
tests/openid_conformance/exports/*.zip
|
||||
compose.override.yml
|
||||
docker-compose.override.yml
|
||||
|
||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -14,10 +14,6 @@
|
||||
"[xml]": {
|
||||
"editor.minimap.markSectionHeaderRegex": "<!--\\s*#\\bregion\\s*(?<separator>-?)\\s*(?<label>.*)\\s*-->"
|
||||
},
|
||||
"files.associations": {
|
||||
// The built-in "ignore" language gives us enough syntax highlighting to make these files readable.
|
||||
"**/dictionaries/*.txt": "ignore"
|
||||
},
|
||||
"todo-tree.tree.showCountsInTree": true,
|
||||
"todo-tree.tree.showBadges": true,
|
||||
"yaml.customTags": [
|
||||
@@ -53,9 +49,13 @@
|
||||
"ignoreCase": false
|
||||
}
|
||||
],
|
||||
"go.testFlags": ["-count=1"],
|
||||
"go.testFlags": [
|
||||
"-count=1"
|
||||
],
|
||||
"go.testEnvVars": {
|
||||
"WORKSPACE_DIR": "${workspaceFolder}"
|
||||
},
|
||||
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"]
|
||||
"github-actions.workflows.pinned.workflows": [
|
||||
".github/workflows/ci-main.yml"
|
||||
]
|
||||
}
|
||||
|
||||
12
CODEOWNERS
12
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,17 +11,15 @@ scripts/ @goauthentik/backend
|
||||
tests/ @goauthentik/backend
|
||||
pyproject.toml @goauthentik/backend
|
||||
uv.lock @goauthentik/backend
|
||||
Cargo.toml @goauthentik/backend
|
||||
Cargo.lock @goauthentik/backend
|
||||
go.mod @goauthentik/backend
|
||||
go.sum @goauthentik/backend
|
||||
.cargo/ @goauthentik/backend
|
||||
rust-toolchain.toml @goauthentik/backend
|
||||
# Infrastructure
|
||||
.github/ @goauthentik/infrastructure
|
||||
lifecycle/aws/ @goauthentik/infrastructure
|
||||
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
|
||||
@@ -39,12 +36,11 @@ 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
|
||||
|
||||
271
Cargo.lock
generated
271
Cargo.lock
generated
@@ -1,271 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docsmg"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"colored",
|
||||
"dotenvy",
|
||||
"eyre",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
||||
dependencies = [
|
||||
"indenter",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
133
Cargo.toml
133
Cargo.toml
@@ -1,133 +0,0 @@
|
||||
[workspace]
|
||||
members = ["website/scripts/docsmg"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
homepage = "https://goauthentik.io"
|
||||
repository = "https://github.com/goauthentik/authentik.git"
|
||||
license-file = "LICENSE"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = { version = "4.5.59", features = ["derive", "env"] }
|
||||
colored = "3.1.1"
|
||||
dotenvy = "0.15.7"
|
||||
eyre = "0.6.12"
|
||||
regex = "1.12.3"
|
||||
|
||||
[profile.dev.package.backtrace]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
debug = 2
|
||||
|
||||
[workspace.lints.rust]
|
||||
ambiguous_negative_literals = "warn"
|
||||
closure_returning_async_block = "warn"
|
||||
macro_use_extern_crate = "deny"
|
||||
# must_not_suspend = "deny", unstable see https://github.com/rust-lang/rust/issues/83310
|
||||
non_ascii_idents = "deny"
|
||||
redundant_imports = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unit_bindings = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unsafe_code = "deny"
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_macro_rules = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
[workspace.lints.rustdoc]
|
||||
unescaped_backticks = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
### enable all lints
|
||||
cargo = { priority = -1, level = "warn" }
|
||||
complexity = { priority = -1, level = "warn" }
|
||||
correctness = { priority = -1, level = "warn" }
|
||||
nursery = { priority = -1, level = "warn" }
|
||||
pedantic = { priority = -1, level = "warn" }
|
||||
perf = { priority = -1, level = "warn" }
|
||||
# Those are too restrictive and disabled by default, however we enable some below
|
||||
# restriction = { priority = -1, level = "warn" }
|
||||
style = { priority = -1, level = "warn" }
|
||||
suspicious = { priority = -1, level = "warn" }
|
||||
### and disable the ones we don't want
|
||||
### pedantic group
|
||||
redundant_closure_for_method_calls = "allow"
|
||||
too_many_lines = "allow"
|
||||
### nursery
|
||||
redundant_pub_crate = "allow"
|
||||
option_if_let_else = "allow"
|
||||
### restriction group
|
||||
allow_attributes = "warn"
|
||||
allow_attributes_without_reason = "warn"
|
||||
as_conversions = "warn"
|
||||
as_pointer_underscore = "warn"
|
||||
as_underscore = "warn"
|
||||
assertions_on_result_states = "warn"
|
||||
clone_on_ref_ptr = "warn"
|
||||
create_dir = "warn"
|
||||
dbg_macro = "warn"
|
||||
default_numeric_fallback = "warn"
|
||||
disallowed_script_idents = "warn"
|
||||
doc_paragraphs_missing_punctuation = "warn"
|
||||
empty_drop = "warn"
|
||||
empty_enum_variants_with_brackets = "warn"
|
||||
empty_structs_with_brackets = "warn"
|
||||
error_impl_error = "warn"
|
||||
exit = "warn"
|
||||
filetype_is_file = "warn"
|
||||
float_cmp_const = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
get_unwrap = "warn"
|
||||
if_then_some_else_none = "warn"
|
||||
impl_trait_in_params = "warn"
|
||||
infinite_loop = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
map_with_unused_argument_over_ranges = "warn"
|
||||
mem_forget = "warn"
|
||||
missing_asserts_for_indexing = "warn"
|
||||
missing_trait_methods = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mutex_atomic = "warn"
|
||||
mutex_integer = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
non_zero_suggestions = "warn"
|
||||
panic_in_result_fn = "warn"
|
||||
pathbuf_init_then_push = "warn"
|
||||
print_stdout = "warn"
|
||||
rc_buffer = "warn"
|
||||
redundant_test_prefix = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
ref_patterns = "warn"
|
||||
renamed_function_params = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
return_and_then = "warn"
|
||||
same_name_method = "warn"
|
||||
semicolon_inside_block = "warn"
|
||||
str_to_string = "warn"
|
||||
string_add = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
tests_outside_test_module = "warn"
|
||||
todo = "warn"
|
||||
try_err = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unimplemented = "warn"
|
||||
unnecessary_safety_comment = "warn"
|
||||
unnecessary_safety_doc = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
unneeded_field_pattern = "warn"
|
||||
unseparated_literal_suffix = "warn"
|
||||
unused_result_ok = "warn"
|
||||
unused_trait_names = "warn"
|
||||
unwrap_in_result = "warn"
|
||||
unwrap_used = "warn"
|
||||
verbose_file_reads = "warn"
|
||||
@@ -26,7 +26,7 @@ RUN npm run build && \
|
||||
npm run build:sfe
|
||||
|
||||
# Stage 2: Build go proxy
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.26.1-trixie@sha256:96b28783b99bcd265fbfe0b36a3ac6462416ce6bf1feac85d4c4ff533cbaa473 AS go-builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS go-builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -44,7 +44,6 @@ RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/v
|
||||
|
||||
RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
|
||||
--mount=type=bind,target=/go/src/goauthentik.io/go.sum,src=./go.sum \
|
||||
--mount=type=bind,target=/go/src/goauthentik.io/gen-go-api,src=./gen-go-api \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go mod download
|
||||
|
||||
@@ -58,7 +57,6 @@ COPY ./go.mod /go/src/goauthentik.io/go.mod
|
||||
COPY ./go.sum /go/src/goauthentik.io/go.sum
|
||||
|
||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
--mount=type=bind,target=/go/src/goauthentik.io/gen-go-api,src=./gen-go-api \
|
||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
||||
@@ -78,9 +76,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/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 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.10.12@sha256:72ab0aeb448090480ccabb99fb5f52b0dc3c71923bffb5e2e26517a1c27b7fec AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.9.17@sha256:5cb6b54d2bc3fe2eb9a8483db958a0b9eebf9edff68adedb369df8e7b98711a2 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.14.3-slim-trixie-fips@sha256:bf45eb77a010d76fe6abd7ae137d1b0c44b6227cd984945042135fdf05ebf8d9 AS python-base
|
||||
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" \
|
||||
@@ -104,7 +102,6 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloa
|
||||
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 \
|
||||
@@ -117,27 +114,21 @@ 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 && \
|
||||
export RUST_TOOLCHAIN="$(awk -F'\"' '/^[[:space:]]*channel[[:space:]]*=/{print $2; exit}' rust-toolchain.toml)" && \
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain "${RUST_TOOLCHAIN}" && \
|
||||
rustup default "${RUST_TOOLCHAIN}" && \
|
||||
rustc --version && \
|
||||
cargo --version
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
|
||||
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="1"
|
||||
RUSTUP_PERMIT_COPY_RENAME="true"
|
||||
|
||||
RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
|
||||
--mount=type=bind,target=uv.lock,src=uv.lock \
|
||||
--mount=type=bind,target=packages,src=packages \
|
||||
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
--mount=type=cache,id=uv-python-deps-$TARGETARCH$TARGETVARIANT,target=/root/.cache/uv \
|
||||
RUSTUP_TOOLCHAIN="$(awk -F'\"' '/^[[:space:]]*channel[[:space:]]*=/{print $2; exit}' rust-toolchain.toml)" \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-install-project --no-dev
|
||||
|
||||
# Stage 7: Run
|
||||
@@ -165,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 && \
|
||||
214
Makefile
214
Makefile
@@ -5,57 +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
|
||||
@@ -70,54 +45,51 @@ help: ## Show this help
|
||||
sort
|
||||
@echo ""
|
||||
|
||||
go-test: ## Run the golang tests
|
||||
go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
rust-test: ## Run the Rust tests
|
||||
$(CARGO) nextest run --workspace
|
||||
go-test:
|
||||
GOFIPS140=latest CGO_ENABLED=1 go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
test: ## Run the server tests and produce a coverage report (locally)
|
||||
$(UV) run coverage run manage.py test --keepdb $(or $(filter-out $@,$(MAKECMDGOALS)),authentik)
|
||||
$(UV) run coverage html
|
||||
$(UV) run coverage report
|
||||
$(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: ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||
$(UV) run black $(PY_SOURCES)
|
||||
$(UV) run ruff check --fix $(PY_SOURCES)
|
||||
$(CARGO) +nightly fmt --all -- --config-path .cargo/rustfmt.toml
|
||||
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||
uv run black $(PY_SOURCES)
|
||||
uv run ruff check --fix $(PY_SOURCES)
|
||||
|
||||
lint-spellcheck: ## Reports spelling errors.
|
||||
npm run lint:spellcheck
|
||||
lint-codespell: ## Reports spelling errors.
|
||||
uv run codespell -w
|
||||
|
||||
lint: ci-lint-bandit ci-lint-mypy ci-lint-cargo-deny ci-lint-cargo-machete ## Lint the python and golang sources
|
||||
lint: ## 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-server: ## Run the main authentik server process
|
||||
$(UV) run ak server
|
||||
uv run ak server
|
||||
|
||||
run-worker: ## Run the main authentik worker process
|
||||
$(UV) run ak worker
|
||||
uv run ak worker
|
||||
|
||||
core-i18n-extract:
|
||||
$(UV) run ak makemessages \
|
||||
uv run ak makemessages \
|
||||
--add-location file \
|
||||
--no-obsolete \
|
||||
--ignore web \
|
||||
@@ -130,17 +102,11 @@ core-i18n-extract:
|
||||
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.
|
||||
@@ -153,11 +119,11 @@ 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 -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
|
||||
|
||||
#########################
|
||||
@@ -168,35 +134,29 @@ 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/api/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
|
||||
@@ -212,7 +172,7 @@ gen-clean-go: ## Remove generated API client for 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/compose.yml run --rm --user "${UID}:${GID}" gen \
|
||||
docker compose -f scripts/api/docker-compose.yml run --rm --user "${UID}:${GID}" gen \
|
||||
generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
@@ -228,19 +188,28 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
|
||||
|
||||
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: gen-clean-go ## Build and install the authentik API for Golang
|
||||
gen-client-go: ## Build and install the authentik API for Golang
|
||||
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 version=${NPM_VERSION}
|
||||
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
|
||||
|
||||
@@ -291,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:
|
||||
@@ -324,7 +293,7 @@ docs-api-clean: ## Clean generated API documentation
|
||||
|
||||
docker: ## Build a docker image of the current source tree
|
||||
mkdir -p ${GEN_API_TS}
|
||||
DOCKER_BUILDKIT=1 docker build . -f lifecycle/container/Dockerfile --progress plain --tag ${DOCKER_IMAGE}
|
||||
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
|
||||
|
||||
test-docker:
|
||||
BUILD=true ${PWD}/scripts/test_docker.sh
|
||||
@@ -336,41 +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 .cargo/deny.toml
|
||||
|
||||
ci-lint-cargo-machete: ci--meta-debug
|
||||
$(CARGO) machete
|
||||
|
||||
ci-lint-rustfmt: ci--meta-debug
|
||||
$(CARGO) +nightly fmt --all --check -- --config-path .cargo/rustfmt.toml
|
||||
|
||||
ci-lint-clippy: ci--meta-debug
|
||||
$(CARGO) clippy --workspace -- -D warnings
|
||||
ci-pending-migrations: ci--meta-debug
|
||||
uv run ak makemigrations --check
|
||||
|
||||
ci-test: ci--meta-debug
|
||||
$(UV) run coverage run manage.py test --keepdb authentik
|
||||
$(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
|
||||
|
||||
@@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | ---------- |
|
||||
| 2025.12.x | ✅ |
|
||||
| 2026.2.x | ✅ |
|
||||
| 2025.8.x | ✅ |
|
||||
| 2025.10.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
|
||||
VERSION = "2026.5.0-rc1"
|
||||
VERSION = "2026.2.0-rc1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
||||
@@ -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,4 +1,9 @@
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
class AuthentikFilesConfig(ManagedAppConfig):
|
||||
@@ -6,3 +11,20 @@ class AuthentikFilesConfig(ManagedAppConfig):
|
||||
label = "authentik_admin_files"
|
||||
verbose_name = "authentik Files"
|
||||
default = True
|
||||
|
||||
@ManagedAppConfig.reconcile_global
|
||||
def check_for_media_mount(self):
|
||||
if settings.TEST:
|
||||
return
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
if (
|
||||
CONFIG.get("storage.media.backend", CONFIG.get("storage.backend", "file")) == "file"
|
||||
and Path("/media").exists()
|
||||
):
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message="/media has been moved to /data/media. "
|
||||
"Check the release notes for migration steps.",
|
||||
).save()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import mimetypes
|
||||
from collections.abc import Callable, Generator, Iterator
|
||||
from typing import cast
|
||||
|
||||
@@ -11,32 +10,6 @@ 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:
|
||||
"""
|
||||
@@ -94,7 +67,7 @@ class Backend:
|
||||
|
||||
Args:
|
||||
file_path: Relative file path
|
||||
request: Optional Django HttpRequest for fully qualified URL building
|
||||
request: Optional Django HttpRequest for fully qualifed URL building
|
||||
use_cache: whether to retrieve the URL from cache
|
||||
|
||||
Returns:
|
||||
@@ -102,29 +75,6 @@ class Backend:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def themed_urls(
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
) -> 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=True)
|
||||
for theme in get_valid_themes()
|
||||
}
|
||||
|
||||
|
||||
class ManageableBackend(Backend):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -46,25 +46,3 @@ class PassthroughBackend(Backend):
|
||||
) -> str:
|
||||
"""Return the URL as-is for passthrough files."""
|
||||
return name
|
||||
|
||||
def themed_urls(
|
||||
self,
|
||||
name: str,
|
||||
request: HttpRequest | None = None,
|
||||
) -> 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()}
|
||||
|
||||
@@ -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
|
||||
@@ -185,22 +173,7 @@ class S3Backend(ManageableBackend):
|
||||
if custom_domain:
|
||||
parsed = urlsplit(url)
|
||||
scheme = "https" if use_https else "http"
|
||||
path = parsed.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}"
|
||||
|
||||
url = f"{scheme}://{custom_domain}{path}?{parsed.query}"
|
||||
url = f"{scheme}://{custom_domain}{parsed.path}?{parsed.query}"
|
||||
|
||||
return url
|
||||
|
||||
@@ -216,7 +189,6 @@ class S3Backend(ManageableBackend):
|
||||
Key=f"{self.base_path}/{name}",
|
||||
Body=content,
|
||||
ACL="private",
|
||||
ContentType=get_content_type(name),
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
@@ -232,7 +204,6 @@ class S3Backend(ManageableBackend):
|
||||
Key=f"{self.base_path}/{name}",
|
||||
ExtraArgs={
|
||||
"ACL": "private",
|
||||
"ContentType": get_content_type(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,14 +1,10 @@
|
||||
from unittest import skipUnless
|
||||
|
||||
from botocore.exceptions import UnsupportedSignatureVersionError
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.admin.files.tests.utils import FileTestS3BackendMixin, s3_test_server_available
|
||||
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"""
|
||||
|
||||
@@ -82,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"""
|
||||
@@ -132,106 +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}",
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
@@ -88,28 +88,6 @@ class FileManager:
|
||||
LOGGER.warning(f"Could not find file backend for file: {name}")
|
||||
return ""
|
||||
|
||||
def themed_urls(
|
||||
self,
|
||||
name: str | None,
|
||||
request: HttpRequest | Request | None = None,
|
||||
) -> dict[str, str] | None:
|
||||
"""
|
||||
Get URLs for each theme variant when filename contains %(theme)s.
|
||||
|
||||
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)
|
||||
|
||||
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,17 +1,10 @@
|
||||
"""Test file service layer"""
|
||||
|
||||
from unittest import skipUnless
|
||||
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
|
||||
|
||||
@@ -88,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)
|
||||
@@ -105,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())
|
||||
|
||||
@@ -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)
|
||||
@@ -15,9 +15,7 @@ class Pagination(pagination.PageNumberPagination):
|
||||
|
||||
def get_page_size(self, request):
|
||||
if self.page_size_query_param in request.query_params:
|
||||
page_size = super().get_page_size(request)
|
||||
if page_size is not None:
|
||||
return min(super().get_page_size(request), request.tenant.pagination_max_page_size)
|
||||
return 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):
|
||||
|
||||
@@ -71,7 +71,7 @@ def postprocess_schema_responses(
|
||||
def postprocess_schema_query_params(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Optimize pagination parameters, instead of redeclaring parameters for each endpoint
|
||||
"""Optimise pagination parameters, instead of redeclaring parameters for each endpoint
|
||||
declare them globally and refer to them"""
|
||||
LOGGER.debug("Deduplicating query parameters")
|
||||
for path in result["paths"].values():
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,7 +1,9 @@
|
||||
"""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
|
||||
@@ -38,12 +40,13 @@ class PrimaryKeyRelatedFieldConverter:
|
||||
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 +93,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 +124,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)
|
||||
|
||||
@@ -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,8 @@ 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 RoleObjectPermission, UserObjectPermission
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.serializers import BaseSerializer, Serializer
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
@@ -40,16 +41,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
|
||||
@@ -70,6 +110,7 @@ def excluded_models() -> list[type[Model]]:
|
||||
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,22 +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:
|
||||
@@ -272,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,
|
||||
@@ -290,7 +355,7 @@ class Importer:
|
||||
)
|
||||
else:
|
||||
self.logger.debug(
|
||||
"Initialized new serializer instance",
|
||||
"Initialised new serializer instance",
|
||||
model=model,
|
||||
**cleanse_dict(updated_identifiers),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -95,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(),
|
||||
@@ -124,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
|
||||
|
||||
|
||||
|
||||
@@ -89,26 +89,14 @@ 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) -> str:
|
||||
"""Get branding_default_flow_background URL"""
|
||||
return get_file_manager(FileUsage.MEDIA).file_url(self.branding_default_flow_background)
|
||||
|
||||
def branding_default_flow_background_themed_urls(self) -> dict[str, str] | None:
|
||||
"""Get themed URLs for branding_default_flow_background if it contains %(theme)s"""
|
||||
return get_file_manager(FileUsage.MEDIA).themed_urls(self.branding_default_flow_background)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.brands.api import BrandSerializer
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.brands.api import Themes
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_brand
|
||||
@@ -21,8 +22,10 @@ class TestBrands(APITestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.default_flags = {}
|
||||
for flag in Flag.available(visibility="public"):
|
||||
self.default_flags[flag().key] = flag.get()
|
||||
for flag in Flag.available():
|
||||
_flag = flag()
|
||||
if _flag.visibility == "public":
|
||||
self.default_flags[_flag.key] = _flag.get()
|
||||
Brand.objects.all().delete()
|
||||
|
||||
def test_current_brand(self):
|
||||
@@ -32,14 +35,12 @@ class TestBrands(APITestCase):
|
||||
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_logo_themed_urls": None,
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_favicon_themed_urls": None,
|
||||
"branding_title": "authentik",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": brand.domain,
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": "automatic",
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
@@ -54,14 +55,12 @@ class TestBrands(APITestCase):
|
||||
).content.decode(),
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_logo_themed_urls": None,
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_favicon_themed_urls": None,
|
||||
"branding_title": "custom",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "bar.baz",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": "automatic",
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
@@ -73,14 +72,12 @@ class TestBrands(APITestCase):
|
||||
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_logo_themed_urls": None,
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_favicon_themed_urls": None,
|
||||
"branding_title": "authentik",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "fallback",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": "automatic",
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
@@ -97,14 +94,12 @@ class TestBrands(APITestCase):
|
||||
response,
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_logo_themed_urls": None,
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_favicon_themed_urls": None,
|
||||
"branding_title": "authentik",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "authentik-default",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": "automatic",
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
@@ -122,14 +117,12 @@ class TestBrands(APITestCase):
|
||||
response,
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_logo_themed_urls": None,
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_favicon_themed_urls": None,
|
||||
"branding_title": "authentik",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "authentik-default",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": "automatic",
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
@@ -140,14 +133,12 @@ class TestBrands(APITestCase):
|
||||
).content.decode(),
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_logo_themed_urls": None,
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_favicon_themed_urls": None,
|
||||
"branding_title": "custom",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "bar.baz",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": "automatic",
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
@@ -163,14 +154,12 @@ class TestBrands(APITestCase):
|
||||
).content.decode(),
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_logo_themed_urls": None,
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_favicon_themed_urls": None,
|
||||
"branding_title": "custom-strong",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "foo.bar.baz",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": "automatic",
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
@@ -186,14 +175,12 @@ class TestBrands(APITestCase):
|
||||
).content.decode(),
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_logo_themed_urls": None,
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_favicon_themed_urls": None,
|
||||
"branding_title": "custom-weak",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "bar.baz",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": "automatic",
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
@@ -269,14 +256,12 @@ class TestBrands(APITestCase):
|
||||
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
|
||||
{
|
||||
"branding_logo": "https://goauthentik.io/img/icon.png",
|
||||
"branding_logo_themed_urls": None,
|
||||
"branding_favicon": "https://goauthentik.io/img/icon.png",
|
||||
"branding_favicon_themed_urls": None,
|
||||
"branding_title": "authentik",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": brand.domain,
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": "automatic",
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from typing import Any
|
||||
|
||||
from django.db.models import Case, F, IntegerField, Q, Value, When
|
||||
from django.db.models.functions import Concat, Length
|
||||
from django.db.models.functions import Length
|
||||
from django.http.request import HttpRequest
|
||||
from django.utils.html import _json_script_escapes
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -26,8 +26,7 @@ def get_brand_for_request(request: HttpRequest) -> Brand:
|
||||
domain_length=Length("domain"),
|
||||
match_priority=Case(
|
||||
When(
|
||||
condition=Q(host_domain__iexact=F("domain"))
|
||||
| Q(host_domain__iendswith=Concat(Value("."), F("domain"))),
|
||||
condition=Q(host_domain__iendswith=F("domain")),
|
||||
then=F("domain_length"),
|
||||
),
|
||||
default=Value(-1),
|
||||
|
||||
@@ -24,8 +24,7 @@ from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import ModelSerializer, ThemedUrlsSerializer
|
||||
from authentik.core.apps import AppAccessWithoutBindings
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.events.logs import LogEventSerializer, capture_logs
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||
@@ -54,9 +53,6 @@ class ApplicationSerializer(ModelSerializer):
|
||||
)
|
||||
|
||||
meta_icon_url = ReadOnlyField(source="get_meta_icon")
|
||||
meta_icon_themed_urls = ThemedUrlsSerializer(
|
||||
source="get_meta_icon_themed_urls", read_only=True, allow_null=True
|
||||
)
|
||||
|
||||
def get_launch_url(self, app: Application) -> str | None:
|
||||
"""Allow formatting of launch URL"""
|
||||
@@ -67,7 +63,7 @@ class ApplicationSerializer(ModelSerializer):
|
||||
user = self.context["request"].user
|
||||
|
||||
# Cache serialized user data to avoid N+1 when formatting launch URLs
|
||||
# for multiple applications. UserSerializer accesses user.groups which
|
||||
# for multiple applications. UserSerializer accesses user.ak_groups which
|
||||
# would otherwise trigger a query for each application.
|
||||
if user is not None:
|
||||
if "_cached_user_data" not in self.context:
|
||||
@@ -106,7 +102,6 @@ class ApplicationSerializer(ModelSerializer):
|
||||
"meta_launch_url",
|
||||
"meta_icon",
|
||||
"meta_icon_url",
|
||||
"meta_icon_themed_urls",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
"policy_engine_mode",
|
||||
@@ -155,16 +150,15 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
return queryset
|
||||
|
||||
def _get_allowed_applications(
|
||||
self, paginated_apps: Iterator[Application], user: User | None = None
|
||||
self, pagined_apps: Iterator[Application], user: User | None = None
|
||||
) -> list[Application]:
|
||||
applications = []
|
||||
request = self.request._request
|
||||
if user:
|
||||
request = copy(request)
|
||||
request.user = user
|
||||
for application in paginated_apps:
|
||||
for application in pagined_apps:
|
||||
engine = PolicyEngine(application, request.user, request)
|
||||
engine.empty_result = AppAccessWithoutBindings.get()
|
||||
engine.build()
|
||||
if engine.passing:
|
||||
applications.append(application)
|
||||
@@ -186,10 +180,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
|
||||
def _filter_applications_with_launch_url(
|
||||
self, paginated_apps: QuerySet[Application]
|
||||
self, applications: QuerySet[Application]
|
||||
) -> list[Application]:
|
||||
applications = []
|
||||
for app in paginated_apps:
|
||||
for app in applications:
|
||||
if app.get_launch_url():
|
||||
applications.append(app)
|
||||
return applications
|
||||
@@ -222,7 +216,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
if not for_user:
|
||||
raise ValidationError({"for_user": "User not found"})
|
||||
engine = PolicyEngine(application, for_user, request)
|
||||
engine.empty_result = AppAccessWithoutBindings.get()
|
||||
engine.use_cache = False
|
||||
with capture_logs() as logs:
|
||||
engine.build()
|
||||
@@ -242,6 +235,11 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="superuser_full_list",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.BOOL,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="for_user",
|
||||
location=OpenApiParameter.QUERY,
|
||||
@@ -252,17 +250,18 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.BOOL,
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: ApplicationSerializer(many=True),
|
||||
},
|
||||
operation_id="core_applications_accessible_list",
|
||||
]
|
||||
)
|
||||
@action(methods=["GET"], detail=False, url_path="@accessible")
|
||||
def accessible(self, request: Request) -> Response:
|
||||
"""Get applications accessible for user"""
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Custom list method that checks Policy based access instead of guardian"""
|
||||
should_cache = request.query_params.get("search", "") == ""
|
||||
|
||||
superuser_full_list = (
|
||||
str(request.query_params.get("superuser_full_list", "false")).lower() == "true"
|
||||
)
|
||||
if superuser_full_list and request.user.is_superuser:
|
||||
return super().list(request)
|
||||
|
||||
only_with_launch_url = str(
|
||||
request.query_params.get("only_with_launch_url", "false")
|
||||
).lower()
|
||||
|
||||
@@ -2,31 +2,18 @@
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema,
|
||||
inline_serializer,
|
||||
)
|
||||
from rest_framework import mixins, serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework import mixins
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import (
|
||||
CharField,
|
||||
DateTimeField,
|
||||
IPAddressField,
|
||||
ListField,
|
||||
)
|
||||
from rest_framework.serializers import CharField, DateTimeField, IPAddressField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from ua_parser import user_agent_parser
|
||||
|
||||
from authentik.api.validation import validate
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.core.models import AuthenticatedSession
|
||||
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR, ASNDict
|
||||
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR, GeoIPDict
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
|
||||
class UserAgentDeviceDict(TypedDict):
|
||||
@@ -65,14 +52,6 @@ class UserAgentDict(TypedDict):
|
||||
string: str
|
||||
|
||||
|
||||
class BulkDeleteSessionSerializer(PassiveSerializer):
|
||||
"""Serializer for bulk deleting authenticated sessions by user"""
|
||||
|
||||
user_pks = ListField(
|
||||
child=serializers.IntegerField(), help_text="List of user IDs to revoke all sessions for"
|
||||
)
|
||||
|
||||
|
||||
class AuthenticatedSessionSerializer(ModelSerializer):
|
||||
"""AuthenticatedSession Serializer"""
|
||||
|
||||
@@ -136,22 +115,3 @@ class AuthenticatedSessionViewSet(
|
||||
filterset_fields = ["user__username", "session__last_ip", "session__last_user_agent"]
|
||||
ordering = ["user__username"]
|
||||
owner_field = "user"
|
||||
|
||||
@permission_required("authentik_core.delete_authenticatedsession")
|
||||
@extend_schema(
|
||||
parameters=[BulkDeleteSessionSerializer],
|
||||
responses={
|
||||
200: inline_serializer(
|
||||
"BulkDeleteSessionResponse",
|
||||
{"deleted": serializers.IntegerField()},
|
||||
),
|
||||
},
|
||||
)
|
||||
@validate(BulkDeleteSessionSerializer, location="query")
|
||||
@action(detail=False, methods=["DELETE"], pagination_class=None, filter_backends=[])
|
||||
def bulk_delete(self, request: Request, *, query: BulkDeleteSessionSerializer) -> Response:
|
||||
"""Bulk revoke all sessions for multiple users"""
|
||||
user_pks = query.validated_data.get("user_pks", [])
|
||||
deleted_count, _ = AuthenticatedSession.objects.filter(user_id__in=user_pks).delete()
|
||||
|
||||
return Response({"deleted": deleted_count}, status=200)
|
||||
|
||||
@@ -16,15 +16,11 @@ from rest_framework.viewsets import ViewSet
|
||||
from authentik.api.validation import validate
|
||||
from authentik.core.api.users import ParamUserSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
|
||||
from authentik.stages.authenticator import device_classes, devices_for_user
|
||||
from authentik.stages.authenticator.models import Device
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
|
||||
try:
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
|
||||
except ModuleNotFoundError:
|
||||
EndpointDevice = None
|
||||
|
||||
|
||||
class DeviceSerializer(MetaNameSerializer):
|
||||
"""Serializer for authenticator devices"""
|
||||
@@ -47,7 +43,7 @@ class DeviceSerializer(MetaNameSerializer):
|
||||
"""Get extra description"""
|
||||
if isinstance(instance, WebAuthnDevice):
|
||||
return instance.device_type.description if instance.device_type else None
|
||||
if EndpointDevice and isinstance(instance, EndpointDevice):
|
||||
if isinstance(instance, EndpointDevice):
|
||||
return instance.data.get("deviceSignals", {}).get("deviceModel")
|
||||
return None
|
||||
|
||||
@@ -55,7 +51,7 @@ class DeviceSerializer(MetaNameSerializer):
|
||||
"""Get external Device ID"""
|
||||
if isinstance(instance, WebAuthnDevice):
|
||||
return instance.device_type.aaguid if instance.device_type else None
|
||||
if EndpointDevice and isinstance(instance, EndpointDevice):
|
||||
if isinstance(instance, EndpointDevice):
|
||||
return instance.data.get("deviceSignals", {}).get("deviceModel")
|
||||
return None
|
||||
|
||||
|
||||
@@ -33,16 +33,6 @@ from authentik.endpoints.connectors.agent.auth import AgentAuth
|
||||
from authentik.rbac.api.roles import RoleSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
PARTIAL_USER_SERIALIZER_MODEL_FIELDS = [
|
||||
"pk",
|
||||
"username",
|
||||
"name",
|
||||
"is_active",
|
||||
"last_login",
|
||||
"email",
|
||||
"attributes",
|
||||
]
|
||||
|
||||
|
||||
class PartialUserSerializer(ModelSerializer):
|
||||
"""Partial User Serializer, does not include child relations."""
|
||||
@@ -52,7 +42,16 @@ class PartialUserSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = PARTIAL_USER_SERIALIZER_MODEL_FIELDS + ["uid"]
|
||||
fields = [
|
||||
"pk",
|
||||
"username",
|
||||
"name",
|
||||
"is_active",
|
||||
"last_login",
|
||||
"email",
|
||||
"attributes",
|
||||
"uid",
|
||||
]
|
||||
|
||||
|
||||
class RelatedGroupSerializer(ModelSerializer):
|
||||
@@ -85,7 +84,6 @@ class GroupSerializer(ModelSerializer):
|
||||
source="roles",
|
||||
required=False,
|
||||
)
|
||||
inherited_roles_obj = SerializerMethodField(allow_null=True)
|
||||
num_pk = IntegerField(read_only=True)
|
||||
|
||||
@property
|
||||
@@ -109,13 +107,6 @@ class GroupSerializer(ModelSerializer):
|
||||
return True
|
||||
return str(request.query_params.get("include_parents", "false")).lower() == "true"
|
||||
|
||||
@property
|
||||
def _should_include_inherited_roles(self) -> bool:
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return True
|
||||
return str(request.query_params.get("include_inherited_roles", "false")).lower() == "true"
|
||||
|
||||
@extend_schema_field(PartialUserSerializer(many=True))
|
||||
def get_users_obj(self, instance: Group) -> list[PartialUserSerializer] | None:
|
||||
if not self._should_include_users:
|
||||
@@ -134,15 +125,6 @@ class GroupSerializer(ModelSerializer):
|
||||
return None
|
||||
return RelatedGroupSerializer(instance.parents, many=True).data
|
||||
|
||||
@extend_schema_field(RoleSerializer(many=True))
|
||||
def get_inherited_roles_obj(self, instance: Group) -> list | None:
|
||||
"""Return only inherited roles from ancestor groups (excludes direct roles)"""
|
||||
if not self._should_include_inherited_roles:
|
||||
return None
|
||||
direct_role_pks = instance.roles.values_list("pk", flat=True)
|
||||
inherited_roles = instance.all_roles().exclude(pk__in=direct_role_pks)
|
||||
return RoleSerializer(inherited_roles, many=True).data
|
||||
|
||||
def validate_is_superuser(self, superuser: bool):
|
||||
"""Ensure that the user creating this group has permissions to set the superuser flag"""
|
||||
request: Request = self.context.get("request", None)
|
||||
@@ -184,7 +166,6 @@ class GroupSerializer(ModelSerializer):
|
||||
"attributes",
|
||||
"roles",
|
||||
"roles_obj",
|
||||
"inherited_roles_obj",
|
||||
"children",
|
||||
"children_obj",
|
||||
]
|
||||
@@ -274,21 +255,14 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
return [
|
||||
StrField(Group, "name"),
|
||||
BoolField(Group, "is_superuser", nullable=True),
|
||||
JSONSearchField(Group, "attributes"),
|
||||
JSONSearchField(Group, "attributes", suggest_nested=False),
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
base_qs = Group.objects.all().prefetch_related("roles")
|
||||
|
||||
if self.serializer_class(context={"request": self.request})._should_include_users:
|
||||
# Only fetch fields needed by PartialUserSerializer to reduce DB load and instantiation
|
||||
# time
|
||||
base_qs = base_qs.prefetch_related(
|
||||
Prefetch(
|
||||
"users",
|
||||
queryset=User.objects.all().only(*PARTIAL_USER_SERIALIZER_MODEL_FIELDS),
|
||||
)
|
||||
)
|
||||
base_qs = base_qs.prefetch_related("users")
|
||||
else:
|
||||
base_qs = base_qs.prefetch_related(
|
||||
Prefetch("users", queryset=User.objects.all().only("id"))
|
||||
@@ -307,7 +281,6 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
OpenApiParameter("include_users", bool, default=True),
|
||||
OpenApiParameter("include_children", bool, default=False),
|
||||
OpenApiParameter("include_parents", bool, default=False),
|
||||
OpenApiParameter("include_inherited_roles", bool, default=False),
|
||||
]
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
@@ -318,7 +291,6 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
OpenApiParameter("include_users", bool, default=True),
|
||||
OpenApiParameter("include_children", bool, default=False),
|
||||
OpenApiParameter("include_parents", bool, default=False),
|
||||
OpenApiParameter("include_inherited_roles", bool, default=False),
|
||||
]
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
@@ -10,6 +10,7 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
from authentik.lib.models import DeprecatedMixin
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
@@ -60,25 +61,19 @@ class TypesMixin:
|
||||
continue
|
||||
instance = subclass()
|
||||
try:
|
||||
type_signature = {
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": instance.component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
"icon_url": getattr(instance, "icon_url", None),
|
||||
"requires_enterprise": False,
|
||||
"deprecated": isinstance(instance, DeprecatedMixin),
|
||||
}
|
||||
try:
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
|
||||
type_signature["requires_enterprise"] = isinstance(
|
||||
subclass._meta.app_config, EnterpriseConfig
|
||||
)
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
data.append(type_signature)
|
||||
data.append(
|
||||
{
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": instance.component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
"icon_url": getattr(instance, "icon_url", None),
|
||||
"requires_enterprise": isinstance(
|
||||
subclass._meta.app_config, EnterpriseConfig
|
||||
),
|
||||
"deprecated": isinstance(instance, DeprecatedMixin),
|
||||
}
|
||||
)
|
||||
except NotImplementedError:
|
||||
continue
|
||||
if additional:
|
||||
|
||||
@@ -18,14 +18,10 @@ from authentik.core.models import Provider
|
||||
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Provider Serializer"""
|
||||
|
||||
assigned_application_slug = ReadOnlyField(source="application.slug", allow_null=True)
|
||||
assigned_application_name = ReadOnlyField(source="application.name", allow_null=True)
|
||||
assigned_backchannel_application_slug = ReadOnlyField(
|
||||
source="backchannel_application.slug", allow_null=True
|
||||
)
|
||||
assigned_backchannel_application_name = ReadOnlyField(
|
||||
source="backchannel_application.name", allow_null=True
|
||||
)
|
||||
assigned_application_slug = ReadOnlyField(source="application.slug")
|
||||
assigned_application_name = ReadOnlyField(source="application.name")
|
||||
assigned_backchannel_application_slug = ReadOnlyField(source="backchannel_application.slug")
|
||||
assigned_backchannel_application_name = ReadOnlyField(source="backchannel_application.name")
|
||||
|
||||
component = SerializerMethodField()
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.object_types import TypesMixin
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer, ThemedUrlsSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
|
||||
from authentik.core.models import GroupSourceConnection, Source, UserSourceConnection
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
@@ -28,7 +28,6 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
managed = ReadOnlyField()
|
||||
component = SerializerMethodField()
|
||||
icon_url = ReadOnlyField()
|
||||
icon_themed_urls = ThemedUrlsSerializer(read_only=True, allow_null=True)
|
||||
|
||||
def get_component(self, obj: Source) -> str:
|
||||
"""Get object component so that we know how to edit the object"""
|
||||
@@ -58,7 +57,6 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"user_path_template",
|
||||
"icon",
|
||||
"icon_url",
|
||||
"icon_themed_urls",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any
|
||||
|
||||
from django.utils.timezone import now
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
@@ -75,8 +76,7 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
expires = attrs.get("expires")
|
||||
if expires is not None and expires > max_token_lifetime_dt:
|
||||
if "expires" in attrs and attrs.get("expires") > max_token_lifetime_dt:
|
||||
raise ValidationError(
|
||||
{
|
||||
"expires": (
|
||||
@@ -145,6 +145,12 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
owner_field = "user"
|
||||
rbac_allow_create_without_perm = True
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user if self.request else get_anonymous_user()
|
||||
if user.is_superuser:
|
||||
return super().get_queryset()
|
||||
return super().get_queryset().filter(user=user.pk)
|
||||
|
||||
def perform_create(self, serializer: TokenSerializer):
|
||||
if not self.request.user.is_superuser:
|
||||
instance = serializer.save(
|
||||
|
||||
@@ -30,6 +30,7 @@ from drf_spectacular.utils import (
|
||||
extend_schema_field,
|
||||
inline_serializer,
|
||||
)
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@@ -41,7 +42,6 @@ from rest_framework.fields import (
|
||||
IntegerField,
|
||||
ListField,
|
||||
SerializerMethodField,
|
||||
UUIDField,
|
||||
)
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
@@ -72,14 +72,12 @@ from authentik.core.middleware import (
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
USER_PATH_SERVICE_ACCOUNT,
|
||||
USERNAME_MAX_LENGTH,
|
||||
Group,
|
||||
Session,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
UserTypes,
|
||||
default_token_duration,
|
||||
)
|
||||
from authentik.endpoints.connectors.agent.auth import AgentAuth
|
||||
from authentik.events.models import Event, EventAction
|
||||
@@ -89,7 +87,6 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||
from authentik.lib.avatars import get_avatar
|
||||
from authentik.lib.utils.reflection import ConditionalInheritance
|
||||
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
||||
from authentik.rbac.api.roles import RoleSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.rbac.models import Role, get_permission_choices
|
||||
@@ -132,6 +129,7 @@ class UserSerializer(ModelSerializer):
|
||||
groups = PrimaryKeyRelatedField(
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
source="ak_groups",
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
default=list,
|
||||
)
|
||||
@@ -145,7 +143,7 @@ class UserSerializer(ModelSerializer):
|
||||
roles_obj = SerializerMethodField(allow_null=True)
|
||||
uid = CharField(read_only=True)
|
||||
username = CharField(
|
||||
max_length=USERNAME_MAX_LENGTH,
|
||||
max_length=150,
|
||||
validators=[UniqueValidator(queryset=User.objects.all().order_by("username"))],
|
||||
)
|
||||
|
||||
@@ -167,7 +165,7 @@ class UserSerializer(ModelSerializer):
|
||||
def get_groups_obj(self, instance: User) -> list[PartialGroupSerializer] | None:
|
||||
if not self._should_include_groups:
|
||||
return None
|
||||
return PartialGroupSerializer(instance.groups, many=True).data
|
||||
return PartialGroupSerializer(instance.ak_groups, many=True).data
|
||||
|
||||
@extend_schema_field(RoleSerializer(many=True))
|
||||
def get_roles_obj(self, instance: User) -> list[RoleSerializer] | None:
|
||||
@@ -241,14 +239,14 @@ class UserSerializer(ModelSerializer):
|
||||
and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
and user_type != UserTypes.INTERNAL_SERVICE_ACCOUNT.value
|
||||
):
|
||||
raise ValidationError(_("Can't change internal service account to other user type."))
|
||||
raise ValidationError("Can't change internal service account to other user type.")
|
||||
if not self.instance and user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT.value:
|
||||
raise ValidationError(_("Setting a user to internal service account is not allowed."))
|
||||
raise ValidationError("Setting a user to internal service account is not allowed.")
|
||||
return user_type
|
||||
|
||||
def validate(self, attrs: dict) -> dict:
|
||||
if self.instance and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
|
||||
raise ValidationError(_("Can't modify internal service account users"))
|
||||
raise ValidationError("Can't modify internal service account users")
|
||||
return super().validate(attrs)
|
||||
|
||||
class Meta:
|
||||
@@ -400,18 +398,6 @@ class UserServiceAccountSerializer(PassiveSerializer):
|
||||
)
|
||||
|
||||
|
||||
class UserRecoveryLinkSerializer(PassiveSerializer):
|
||||
"""Payload to create a recovery link"""
|
||||
|
||||
token_duration = CharField(required=False)
|
||||
|
||||
|
||||
class UserRecoveryEmailSerializer(UserRecoveryLinkSerializer):
|
||||
"""Payload to create and email a recovery link"""
|
||||
|
||||
email_stage = UUIDField()
|
||||
|
||||
|
||||
class UsersFilter(FilterSet):
|
||||
"""Filter for users"""
|
||||
|
||||
@@ -430,12 +416,7 @@ class UsersFilter(FilterSet):
|
||||
last_updated = IsoDateTimeFilter(field_name="last_updated")
|
||||
last_updated__gt = IsoDateTimeFilter(field_name="last_updated", lookup_expr="gt")
|
||||
|
||||
last_login__lt = IsoDateTimeFilter(field_name="last_login", lookup_expr="lt")
|
||||
last_login = IsoDateTimeFilter(field_name="last_login")
|
||||
last_login__gt = IsoDateTimeFilter(field_name="last_login", lookup_expr="gt")
|
||||
last_login__isnull = BooleanFilter(field_name="last_login", lookup_expr="isnull")
|
||||
|
||||
is_superuser = BooleanFilter(field_name="groups", method="filter_is_superuser")
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
||||
uuid = UUIDFilter(field_name="uuid")
|
||||
|
||||
path = CharFilter(field_name="path")
|
||||
@@ -444,12 +425,12 @@ class UsersFilter(FilterSet):
|
||||
type = MultipleChoiceFilter(choices=UserTypes.choices, field_name="type")
|
||||
|
||||
groups_by_name = ModelMultipleChoiceFilter(
|
||||
field_name="groups__name",
|
||||
field_name="ak_groups__name",
|
||||
to_field_name="name",
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
)
|
||||
groups_by_pk = ModelMultipleChoiceFilter(
|
||||
field_name="groups",
|
||||
field_name="ak_groups",
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
)
|
||||
|
||||
@@ -465,22 +446,22 @@ class UsersFilter(FilterSet):
|
||||
|
||||
def filter_is_superuser(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(groups__is_superuser=True).distinct()
|
||||
return queryset.exclude(groups__is_superuser=True).distinct()
|
||||
return queryset.filter(ak_groups__is_superuser=True).distinct()
|
||||
return queryset.exclude(ak_groups__is_superuser=True).distinct()
|
||||
|
||||
def filter_attributes(self, queryset, name, value):
|
||||
"""Filter attributes by query args"""
|
||||
try:
|
||||
value = loads(value)
|
||||
except ValueError:
|
||||
raise ValidationError(_("filter: failed to parse JSON")) from None
|
||||
raise ValidationError(detail="filter: failed to parse JSON") from None
|
||||
if not isinstance(value, dict):
|
||||
raise ValidationError(_("filter: value must be key:value mapping"))
|
||||
raise ValidationError(detail="filter: value must be key:value mapping")
|
||||
qs = {}
|
||||
for key, _value in value.items():
|
||||
qs[f"attributes__{key}"] = _value
|
||||
try:
|
||||
__ = len(queryset.filter(**qs))
|
||||
_ = len(queryset.filter(**qs))
|
||||
return queryset.filter(**qs)
|
||||
except ValueError:
|
||||
return queryset
|
||||
@@ -492,7 +473,6 @@ class UsersFilter(FilterSet):
|
||||
"email",
|
||||
"date_joined",
|
||||
"last_updated",
|
||||
"last_login",
|
||||
"name",
|
||||
"is_active",
|
||||
"is_superuser",
|
||||
@@ -513,7 +493,7 @@ class UserViewSet(
|
||||
"""User Viewset"""
|
||||
|
||||
queryset = User.objects.none()
|
||||
ordering = ["username", "date_joined", "last_updated", "last_login"]
|
||||
ordering = ["username", "date_joined", "last_updated"]
|
||||
serializer_class = UserSerializer
|
||||
filterset_class = UsersFilter
|
||||
search_fields = ["email", "name", "uuid", "username"]
|
||||
@@ -538,13 +518,13 @@ class UserViewSet(
|
||||
StrField(User, "path"),
|
||||
BoolField(User, "is_active", nullable=True),
|
||||
ChoiceSearchField(User, "type"),
|
||||
JSONSearchField(User, "attributes"),
|
||||
JSONSearchField(User, "attributes", suggest_nested=False),
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
base_qs = User.objects.all().exclude_anonymous()
|
||||
if self.serializer_class(context={"request": self.request})._should_include_groups:
|
||||
base_qs = base_qs.prefetch_related("groups")
|
||||
base_qs = base_qs.prefetch_related("ak_groups")
|
||||
if self.serializer_class(context={"request": self.request})._should_include_roles:
|
||||
base_qs = base_qs.prefetch_related("roles")
|
||||
return base_qs
|
||||
@@ -558,16 +538,14 @@ class UserViewSet(
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def _create_recovery_link(
|
||||
self, token_duration: str | None, for_email=False
|
||||
) -> tuple[str, Token]:
|
||||
def _create_recovery_link(self, for_email=False) -> tuple[str, Token]:
|
||||
"""Create a recovery link (when the current brand has a recovery flow set),
|
||||
that can either be shown to an admin or sent to the user directly"""
|
||||
brand: Brand = self.request.brand
|
||||
brand: Brand = self.request._request.brand
|
||||
# Check that there is a recovery flow, if not return an error
|
||||
flow = brand.flow_recovery
|
||||
if not flow:
|
||||
raise ValidationError({"non_field_errors": _("No recovery flow set.")})
|
||||
raise ValidationError({"non_field_errors": "No recovery flow set."})
|
||||
user: User = self.get_object()
|
||||
planner = FlowPlanner(flow)
|
||||
planner.allow_empty_flows = True
|
||||
@@ -581,15 +559,11 @@ class UserViewSet(
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
raise ValidationError(
|
||||
{"non_field_errors": _("Recovery flow not applicable to user")}
|
||||
{"non_field_errors": "Recovery flow not applicable to user"}
|
||||
) from None
|
||||
_plan = FlowToken.pickle(plan)
|
||||
if for_email:
|
||||
_plan = pickle_flow_token_for_email(plan)
|
||||
expires = default_token_duration()
|
||||
if token_duration:
|
||||
timedelta_string_validator(token_duration)
|
||||
expires = now() + timedelta_from_string(token_duration)
|
||||
token, __ = FlowToken.objects.update_or_create(
|
||||
identifier=f"{user.uid}-password-reset",
|
||||
defaults={
|
||||
@@ -597,7 +571,6 @@ class UserViewSet(
|
||||
"flow": flow,
|
||||
"_plan": _plan,
|
||||
"revoke_on_execution": not for_email,
|
||||
"expires": expires,
|
||||
},
|
||||
)
|
||||
querystring = urlencode({QS_KEY_TOKEN: token.key})
|
||||
@@ -745,60 +718,60 @@ class UserViewSet(
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
request=UserRecoveryLinkSerializer,
|
||||
responses={
|
||||
"200": LinkSerializer(many=False),
|
||||
},
|
||||
request=None,
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
@validate(UserRecoveryLinkSerializer)
|
||||
def recovery(self, request: Request, pk: int, body: UserRecoveryLinkSerializer) -> Response:
|
||||
def recovery(self, request: Request, pk: int) -> Response:
|
||||
"""Create a temporary link that a user can use to recover their account"""
|
||||
link, _ = self._create_recovery_link(
|
||||
token_duration=body.validated_data.get("token_duration")
|
||||
)
|
||||
link, _ = self._create_recovery_link()
|
||||
return Response({"link": link})
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
request=UserRecoveryEmailSerializer,
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="email_stage",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
responses={
|
||||
"204": OpenApiResponse(description="Successfully sent recover email"),
|
||||
},
|
||||
request=None,
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
@validate(UserRecoveryEmailSerializer)
|
||||
def recovery_email(
|
||||
self, request: Request, pk: int, body: UserRecoveryEmailSerializer
|
||||
) -> Response:
|
||||
def recovery_email(self, request: Request, pk: int) -> Response:
|
||||
"""Send an email with a temporary link that a user can use to recover their account"""
|
||||
email_error_message = _("User does not have an email address set.")
|
||||
stage_error_message = _("Email stage not found.")
|
||||
user: User = self.get_object()
|
||||
if not user.email:
|
||||
for_user: User = self.get_object()
|
||||
if for_user.email == "":
|
||||
LOGGER.debug("User doesn't have an email address")
|
||||
raise ValidationError({"non_field_errors": email_error_message})
|
||||
if not (stage := EmailStage.objects.filter(pk=body.validated_data["email_stage"]).first()):
|
||||
LOGGER.debug("Email stage does not exist")
|
||||
raise ValidationError({"non_field_errors": stage_error_message})
|
||||
if not request.user.has_perm("authentik_stages_email.view_emailstage", stage):
|
||||
LOGGER.debug("User has no view access to email stage")
|
||||
raise ValidationError({"non_field_errors": stage_error_message})
|
||||
link, token = self._create_recovery_link(
|
||||
token_duration=body.validated_data.get("token_duration"), for_email=True
|
||||
)
|
||||
raise ValidationError({"non_field_errors": "User does not have an email address set."})
|
||||
link, token = self._create_recovery_link(for_email=True)
|
||||
# Lookup the email stage to assure the current user can access it
|
||||
stages = get_objects_for_user(
|
||||
request.user, "authentik_stages_email.view_emailstage"
|
||||
).filter(pk=request.query_params.get("email_stage"))
|
||||
if not stages.exists():
|
||||
LOGGER.debug("Email stage does not exist/user has no permissions")
|
||||
raise ValidationError({"non_field_errors": "Email stage does not exist."})
|
||||
email_stage: EmailStage = stages.first()
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(stage.subject),
|
||||
to=[(user.name, user.email)],
|
||||
template_name=stage.template,
|
||||
language=user.locale(request),
|
||||
subject=_(email_stage.subject),
|
||||
to=[(for_user.name, for_user.email)],
|
||||
template_name=email_stage.template,
|
||||
language=for_user.locale(request),
|
||||
template_context={
|
||||
"url": link,
|
||||
"user": user,
|
||||
"user": for_user,
|
||||
"expires": token.expires,
|
||||
},
|
||||
)
|
||||
send_mails(stage, message)
|
||||
send_mails(email_stage, message)
|
||||
return Response(status=204)
|
||||
|
||||
@permission_required("authentik_core.impersonate")
|
||||
|
||||
@@ -127,10 +127,3 @@ class LinkSerializer(PassiveSerializer):
|
||||
"""Returns a single link"""
|
||||
|
||||
link = CharField()
|
||||
|
||||
|
||||
class ThemedUrlsSerializer(PassiveSerializer):
|
||||
"""Themed URLs - maps theme names to URLs for light and dark themes"""
|
||||
|
||||
light = CharField(required=False, allow_null=True)
|
||||
dark = CharField(required=False, allow_null=True)
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
"""authentik core app config"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.tasks.schedules.common import ScheduleSpec
|
||||
from authentik.tenants.flags import Flag
|
||||
|
||||
|
||||
class AppAccessWithoutBindings(Flag[bool], key="core_default_app_access"):
|
||||
|
||||
default = True
|
||||
visibility = "none"
|
||||
description = _(
|
||||
"Configure if applications without any policy/group/user bindings "
|
||||
"should be accessible to any user."
|
||||
)
|
||||
|
||||
|
||||
class AuthentikCoreConfig(ManagedAppConfig):
|
||||
|
||||
@@ -8,7 +8,7 @@ from uuid import uuid4
|
||||
from django.contrib.auth import logout
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.translation import override
|
||||
@@ -47,7 +47,7 @@ async def aget_user(request):
|
||||
|
||||
|
||||
class AuthenticationMiddleware(MiddlewareMixin):
|
||||
def process_request(self, request: HttpRequest) -> HttpResponseBadRequest | None:
|
||||
def process_request(self, request):
|
||||
if not hasattr(request, "session"):
|
||||
raise ImproperlyConfigured(
|
||||
"The Django authentication middleware requires session "
|
||||
@@ -62,8 +62,7 @@ class AuthenticationMiddleware(MiddlewareMixin):
|
||||
user = request.user
|
||||
if user and user.is_authenticated and not user.is_active:
|
||||
logout(request)
|
||||
return HttpResponseBadRequest()
|
||||
return None
|
||||
raise AssertionError()
|
||||
|
||||
|
||||
class ImpersonateMiddleware:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user