mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 07:32:23 +02:00
Compare commits
8 Commits
root/move-
...
playwright
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c0f13299b | ||
|
|
f4bc6ea895 | ||
|
|
336f2d309e | ||
|
|
b2b32e60e3 | ||
|
|
0a5e117663 | ||
|
|
a24948259c | ||
|
|
5b0fa6d071 | ||
|
|
246165ae70 |
36
.bumpversion.cfg
Normal file
36
.bumpversion.cfg
Normal file
@@ -0,0 +1,36 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.6.4
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
serialize =
|
||||
{major}.{minor}.{patch}-{rc_t}{rc_n}
|
||||
{major}.{minor}.{patch}
|
||||
message = release: {new_version}
|
||||
tag_name = version/{new_version}
|
||||
|
||||
[bumpversion:part:rc_t]
|
||||
values =
|
||||
rc
|
||||
final
|
||||
optional_value = final
|
||||
|
||||
[bumpversion:file:pyproject.toml]
|
||||
|
||||
[bumpversion:file:uv.lock]
|
||||
|
||||
[bumpversion:file:package.json]
|
||||
|
||||
[bumpversion:file:package-lock.json]
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
[bumpversion:file:schema.yml]
|
||||
|
||||
[bumpversion:file:blueprints/schema.json]
|
||||
|
||||
[bumpversion:file:authentik/__init__.py]
|
||||
|
||||
[bumpversion:file:internal/constants/constants.go]
|
||||
|
||||
[bumpversion:file:lifecycle/aws/template.yaml]
|
||||
@@ -1,6 +1,5 @@
|
||||
htmlcov
|
||||
*.env.yml
|
||||
node_modules
|
||||
**/node_modules
|
||||
dist/**
|
||||
build/**
|
||||
|
||||
@@ -54,10 +54,6 @@ outputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: "python"
|
||||
- name: Generate config
|
||||
id: ev
|
||||
shell: bash
|
||||
@@ -68,4 +64,4 @@ runs:
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
uv run python3 ${{ github.action_path }}/push_vars.py
|
||||
python3 ${{ github.action_path }}/push_vars.py
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""Helper script to get the actual branch name, docker safe"""
|
||||
|
||||
import configparser
|
||||
import os
|
||||
from json import dumps
|
||||
from time import time
|
||||
|
||||
from authentik import authentik_version
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(".bumpversion.cfg")
|
||||
|
||||
# Decide if we should push the image or not
|
||||
should_push = True
|
||||
@@ -29,7 +31,7 @@ is_release = "dev" not in image_names[0]
|
||||
sha = os.environ["GITHUB_SHA"] if not is_pull_request else os.getenv("PR_HEAD_SHA")
|
||||
|
||||
# 2042.1.0 or 2042.1.0-rc1
|
||||
version = authentik_version()
|
||||
version = parser.get("bumpversion", "current_version")
|
||||
# 2042.1
|
||||
version_family = ".".join(version.split("-", 1)[0].split(".")[:-1])
|
||||
prerelease = "-" in version
|
||||
|
||||
@@ -4,7 +4,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
GITHUB_OUTPUT=/dev/stdout \
|
||||
GITHUB_REF=ref \
|
||||
GITHUB_SHA=sha \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,authentik/server \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
||||
GITHUB_REPOSITORY=goauthentik/authentik \
|
||||
python $SCRIPT_DIR/push_vars.py
|
||||
|
||||
@@ -12,7 +12,7 @@ GITHUB_OUTPUT=/dev/stdout \
|
||||
GITHUB_OUTPUT=/dev/stdout \
|
||||
GITHUB_REF=ref \
|
||||
GITHUB_SHA=sha \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,authentik/server \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
||||
GITHUB_REPOSITORY=goauthentik/authentik \
|
||||
DOCKER_USERNAME=foo \
|
||||
python $SCRIPT_DIR/push_vars.py
|
||||
|
||||
13
.github/actions/setup/action.yml
vendored
13
.github/actions/setup/action.yml
vendored
@@ -2,9 +2,6 @@ name: "Setup authentik testing environment"
|
||||
description: "Setup authentik testing environment"
|
||||
|
||||
inputs:
|
||||
dependencies:
|
||||
description: "List of dependencies to setup"
|
||||
default: "system,python,node,go,runtime"
|
||||
postgresql_version:
|
||||
description: "Optional postgresql image tag"
|
||||
default: "16"
|
||||
@@ -13,52 +10,42 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- 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 libkrb5-dev krb5-kdc krb5-user krb5-admin-server
|
||||
- name: Install uv
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Setup python
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
uses: actions/setup-python@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 node
|
||||
if: ${{ contains(inputs.dependencies, 'node') }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Setup go
|
||||
if: ${{ contains(inputs.dependencies, 'go') }}
|
||||
uses: actions/setup-go@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/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/docker-compose.yml up -d
|
||||
cd web && npm ci
|
||||
- name: Generate config
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
shell: uv run python {0}
|
||||
run: |
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
# Re-usable workflow for a single-architecture build
|
||||
name: Reusable - Single-arch Container build
|
||||
name: Single-arch Container build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -42,14 +41,14 @@ jobs:
|
||||
# Needed for checkout
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-qemu-action@v3.6.0
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ${{ inputs.image_name }}
|
||||
image-arch: ${{ inputs.image_arch }}
|
||||
@@ -58,8 +57,8 @@ jobs:
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.registry_ghcr }}
|
||||
uses: docker/login-action@v3
|
||||
|
||||
15
.github/workflows/_reusable-docker-build.yaml
vendored
15
.github/workflows/_reusable-docker-build.yaml
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
# Re-usable workflow for a multi-architecture build
|
||||
name: Reusable - Multi-arch container build
|
||||
name: Multi-arch container build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -49,12 +48,12 @@ jobs:
|
||||
tags: ${{ steps.ev.outputs.imageTagsJSON }}
|
||||
shouldPush: ${{ steps.ev.outputs.shouldPush }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ${{ inputs.image_name }}
|
||||
merge-server:
|
||||
@@ -69,20 +68,20 @@ jobs:
|
||||
matrix:
|
||||
tag: ${{ fromJson(needs.get-tags.outputs.tags) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ${{ inputs.image_name }}
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.registry_ghcr }}
|
||||
uses: docker/login-action@v3
|
||||
|
||||
7
.github/workflows/api-py-publish.yml
vendored
7
.github/workflows/api-py-publish.yml
vendored
@@ -1,13 +1,10 @@
|
||||
---
|
||||
name: API - Publish Python client
|
||||
|
||||
name: authentik-api-py-publish
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "schema.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
@@ -20,7 +17,7 @@ jobs:
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Install poetry & deps
|
||||
|
||||
7
.github/workflows/api-ts-publish.yml
vendored
7
.github/workflows/api-ts-publish.yml
vendored
@@ -1,13 +1,10 @@
|
||||
---
|
||||
name: API - Publish Typescript client
|
||||
|
||||
name: authentik-api-ts-publish
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "schema.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
@@ -18,7 +15,7 @@ jobs:
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
11
.github/workflows/ci-api-docs.yml
vendored
11
.github/workflows/ci-api-docs.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: CI - API Docs
|
||||
name: authentik-ci-api-docs
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -21,7 +20,7 @@ jobs:
|
||||
command:
|
||||
- prettier-check
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Dependencies
|
||||
working-directory: website/
|
||||
run: npm ci
|
||||
@@ -32,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
@@ -66,8 +65,8 @@ jobs:
|
||||
- lint
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: api-docs
|
||||
path: website/api/build
|
||||
|
||||
5
.github/workflows/ci-aws-cfn.yml
vendored
5
.github/workflows/ci-aws-cfn.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: CI - AWS cfn
|
||||
name: authentik-ci-aws-cfn
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -21,7 +20,7 @@ jobs:
|
||||
check-changes-applied:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
13
.github/workflows/ci-docs.yml
vendored
13
.github/workflows/ci-docs.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: CI - Docs
|
||||
name: authentik-ci-docs
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -21,7 +20,7 @@ jobs:
|
||||
command:
|
||||
- prettier-check
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
working-directory: website/
|
||||
run: npm ci
|
||||
@@ -32,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
@@ -48,7 +47,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
@@ -70,7 +69,7 @@ jobs:
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
@@ -81,7 +80,7 @@ jobs:
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/dev-docs
|
||||
- name: Login to Container Registry
|
||||
|
||||
4
.github/workflows/ci-main-daily.yml
vendored
4
.github/workflows/ci-main-daily.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: CI - Main daily
|
||||
name: authentik-ci-main-daily
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- version-2025-4
|
||||
- version-2025-2
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
current="$(pwd)"
|
||||
dir="/tmp/authentik/${{ matrix.version }}"
|
||||
|
||||
36
.github/workflows/ci-main.yml
vendored
36
.github/workflows/ci-main.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: CI - Main
|
||||
name: authentik-ci-main
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -17,12 +17,6 @@ env:
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
|
||||
permissions:
|
||||
# Needed for checkout
|
||||
contents: read
|
||||
# Needed for codecov OIDC token
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
strategy:
|
||||
@@ -36,7 +30,7 @@ jobs:
|
||||
- ruff
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run job
|
||||
@@ -44,7 +38,7 @@ jobs:
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run migrations
|
||||
@@ -71,7 +65,7 @@ jobs:
|
||||
- 17-alpine
|
||||
run_id: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: checkout stable
|
||||
@@ -126,7 +120,7 @@ jobs:
|
||||
- 17-alpine
|
||||
run_id: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -142,18 +136,18 @@ jobs:
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
flags: unit
|
||||
use_oidc: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
flags: unit
|
||||
file: unittest.xml
|
||||
use_oidc: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Create k8s Kind Cluster
|
||||
@@ -166,13 +160,13 @@ jobs:
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
flags: integration
|
||||
use_oidc: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
flags: integration
|
||||
file: unittest.xml
|
||||
use_oidc: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
test-e2e:
|
||||
name: test-e2e (${{ matrix.job.name }})
|
||||
runs-on: ubuntu-latest
|
||||
@@ -198,7 +192,7 @@ jobs:
|
||||
- name: flows
|
||||
glob: tests/e2e/test_flows*
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup e2e env (chrome, etc)
|
||||
@@ -225,13 +219,13 @@ jobs:
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
flags: e2e
|
||||
use_oidc: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
flags: e2e
|
||||
file: unittest.xml
|
||||
use_oidc: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
ci-core-mark:
|
||||
if: always()
|
||||
needs:
|
||||
@@ -271,14 +265,14 @@ jobs:
|
||||
pull-requests: write
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/dev-server
|
||||
- name: Comment on PR
|
||||
|
||||
12
.github/workflows/ci-outpost.yml
vendored
12
.github/workflows/ci-outpost.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: CI - Outpost
|
||||
name: authentik-ci-outpost
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
lint-golint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
test-unittest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
|
||||
- name: Login to Container Registry
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-go@v5
|
||||
|
||||
9
.github/workflows/ci-web.yml
vendored
9
.github/workflows/ci-web.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: CI - Web
|
||||
name: authentik-ci-web
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -31,7 +30,7 @@ jobs:
|
||||
- command: lit-analyse
|
||||
project: web
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ${{ matrix.project }}/package.json
|
||||
@@ -48,7 +47,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
@@ -76,7 +75,7 @@ jobs:
|
||||
- ci-web-mark
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: QA - CodeQL
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -24,7 +23,7 @@ jobs:
|
||||
language: ["go", "javascript", "python"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Initialize CodeQL
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
name: Gen - Webauthn MDS
|
||||
|
||||
name: authentik-gen-update-webauthn-mds
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
@@ -21,7 +19,7 @@ jobs:
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Setup authentik env
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
# See https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
||||
name: GH - Cleanup actions cache after PR is closed
|
||||
|
||||
name: Cleanup cache after PR is closed
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
@@ -16,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: GH - GHCR retention
|
||||
name: ghcr-retention
|
||||
|
||||
on:
|
||||
# schedule:
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Gen - Compress images
|
||||
name: authentik-compress-images
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Compress images
|
||||
7
.github/workflows/packages-npm-publish.yml
vendored
7
.github/workflows/packages-npm-publish.yml
vendored
@@ -1,6 +1,4 @@
|
||||
---
|
||||
name: Packages - Publish NPM packages
|
||||
|
||||
name: authentik-packages-npm-publish
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -11,7 +9,6 @@ on:
|
||||
- packages/tsconfig/**
|
||||
- packages/esbuild-plugin-live-reload/**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
@@ -26,7 +23,7 @@ jobs:
|
||||
- packages/tsconfig
|
||||
- packages/esbuild-plugin-live-reload
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: CI - Source code docs
|
||||
name: authentik-publish-source-docs
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -17,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: generate docs
|
||||
83
.github/workflows/release-branch-off.yml
vendored
83
.github/workflows/release-branch-off.yml
vendored
@@ -1,83 +0,0 @@
|
||||
---
|
||||
name: Release - Branch-off
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
next_version:
|
||||
description: Next major version (for example, if releasing 2042.2, this is 2042.4)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
POSTGRES_DB: authentik
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
|
||||
jobs:
|
||||
check-inputs:
|
||||
name: Check inputs validity
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "${{ inputs.next_version }}" | grep -E "^[0-9]{4}\.[0-9]{1,2}$"
|
||||
branch-off:
|
||||
name: Branch-off
|
||||
needs:
|
||||
- check-inputs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: python
|
||||
- name: Create version branch
|
||||
run: |
|
||||
current_major_version="$(uv version --short | grep -oE "^[0-9]{4}\.[0-9]{1,2}")"
|
||||
git checkout -b "version-${current_major_version}"
|
||||
git push origin "version-${current_major_version}"
|
||||
bump-version-pr:
|
||||
name: Open version bump PR
|
||||
needs:
|
||||
- branch-off
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run migrations
|
||||
run: make migrate
|
||||
- name: Bump version
|
||||
run: "make bump version=${{ inputs.next_version }}.0-rc1"
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: release-bump-${{ inputs.next_version }}
|
||||
commit-message: "root: bump version to ${{ inputs.next_version }}.0-rc1"
|
||||
title: "root: bump version to ${{ inputs.next_version }}.0-rc1"
|
||||
body: "root: bump version to ${{ inputs.next_version }}.0-rc1"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
# ID from https://api.github.com/users/authentik-automation[bot]
|
||||
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
|
||||
5
.github/workflows/release-next-branch.yml
vendored
5
.github/workflows/release-next-branch.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: Release - Update next branch
|
||||
name: authentik-on-release-next-branch
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -16,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: internal-production
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
- run: |
|
||||
|
||||
31
.github/workflows/release-publish.yml
vendored
31
.github/workflows/release-publish.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Release - On publish
|
||||
name: authentik-on-release
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -10,28 +10,26 @@ jobs:
|
||||
uses: ./.github/workflows/_reusable-docker-build.yaml
|
||||
secrets: inherit
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed to upload container images to ghcr.io
|
||||
packages: write
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
attestations: write
|
||||
with:
|
||||
image_name: ghcr.io/goauthentik/server,authentik/server
|
||||
image_name: ghcr.io/goauthentik/server,beryju/authentik
|
||||
release: true
|
||||
registry_dockerhub: true
|
||||
registry_ghcr: true
|
||||
build-docs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed to upload container images to ghcr.io
|
||||
packages: write
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.6.0
|
||||
- name: Set up Docker Buildx
|
||||
@@ -40,7 +38,7 @@ jobs:
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/docs
|
||||
- name: Login to GitHub Container Registry
|
||||
@@ -68,7 +66,6 @@ jobs:
|
||||
build-outpost:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed to upload container images to ghcr.io
|
||||
packages: write
|
||||
# Needed for attestation
|
||||
@@ -83,7 +80,7 @@ jobs:
|
||||
- radius
|
||||
- rac
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
@@ -95,9 +92,9 @@ jobs:
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/${{ matrix.type }},authentik/${{ matrix.type }}
|
||||
image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }}
|
||||
- name: make empty clients
|
||||
run: |
|
||||
mkdir -p ./gen-ts-api
|
||||
@@ -105,8 +102,8 @@ jobs:
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -146,7 +143,7 @@ jobs:
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
@@ -186,7 +183,7 @@ jobs:
|
||||
AWS_REGION: eu-central-1
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: "arn:aws:iam::016170277896:role/github_goauthentik_authentik"
|
||||
@@ -202,7 +199,7 @@ jobs:
|
||||
- build-outpost-binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
|
||||
@@ -218,12 +215,12 @@ jobs:
|
||||
- build-outpost-binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/server
|
||||
- name: Get static files from docker image
|
||||
|
||||
206
.github/workflows/release-tag.yml
vendored
206
.github/workflows/release-tag.yml
vendored
@@ -1,195 +1,39 @@
|
||||
---
|
||||
name: Release - Tag new version
|
||||
name: authentik-on-tag
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version
|
||||
required: true
|
||||
type: string
|
||||
release_reason:
|
||||
description: Release reason
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- bugfix
|
||||
- feature
|
||||
- security
|
||||
- other
|
||||
- prerelease
|
||||
|
||||
env:
|
||||
POSTGRES_DB: authentik
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
push:
|
||||
tags:
|
||||
- "version/*"
|
||||
|
||||
jobs:
|
||||
check-inputs:
|
||||
name: Check inputs validity
|
||||
build:
|
||||
name: Create Release from Tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: check
|
||||
- uses: actions/checkout@v4
|
||||
- name: Pre-release test
|
||||
run: |
|
||||
echo "${{ inputs.version }}" | grep -E '^[0-9]{4}\.(0?[1-9]|1[0-2])\.[0-9]+(-rc[0-9]+)?$'
|
||||
echo "major_version=${{ inputs.version }}" | grep -oE "^major_version=[0-9]{4}\.[0-9]{1,2}" >> "$GITHUB_OUTPUT"
|
||||
- id: changelog-url
|
||||
run: |
|
||||
if [ "${{ inputs.release_reason }}" = "feature" ] || [ "${{ inputs.release_reason }}" = "prerelease" ]; then
|
||||
changelog_url="https://docs.goauthentik.io/docs/releases/${{ steps.check.outputs.major_version }}"
|
||||
else
|
||||
changelog_url="https://docs.goauthentik.io/docs/releases/${{ steps.check.outputs.major_version }}#fixed-in-$(echo -n ${{ inputs.version }} | sed 's/\.//g')"
|
||||
fi
|
||||
echo "changelog_url=${changelog_url}" >> "$GITHUB_OUTPUT"
|
||||
outputs:
|
||||
major_version: "${{ steps.check.outputs.major_version }}"
|
||||
changelog_url: "${{ steps.changelog-url.outputs.changelog_url }}"
|
||||
test:
|
||||
name: Pre-release test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: make test-docker
|
||||
bump-authentik:
|
||||
name: Bump authentik version
|
||||
needs:
|
||||
- check-inputs
|
||||
- test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@v2
|
||||
make test-docker
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
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"
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
|
||||
- uses: actions/checkout@v5
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
ref: "version-${{ needs.check-inputs.outputs.major_version }}"
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run migrations
|
||||
run: make migrate
|
||||
- name: Bump version
|
||||
run: "make bump version=${{ inputs.version }}"
|
||||
- name: Commit and push
|
||||
run: |
|
||||
# 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 commit -a -m "release: ${{ inputs.version }}" --allow-empty
|
||||
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
|
||||
git push --follow-tags
|
||||
image-name: ghcr.io/goauthentik/server
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
id: create_release
|
||||
uses: actions/create-release@v1.1.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
tag_name: "version/${{ inputs.version }}"
|
||||
name: Release ${{ inputs.version }}
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ steps.ev.outputs.version }}
|
||||
draft: true
|
||||
prerelease: ${{ inputs.release_reason == 'prerelease' }}
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
See ${{ needs.check-inputs.outputs.changelog_url }}
|
||||
bump-helm:
|
||||
name: Bump Helm version
|
||||
if: ${{ inputs.release_reason != 'prerelease' }}
|
||||
needs:
|
||||
- bump-authentik
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
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@v5
|
||||
with:
|
||||
repository: "${{ github.repository_owner }}/helm"
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
- name: Bump version
|
||||
run: |
|
||||
sed -i 's/^version: .*/version: ${{ inputs.version }}/' charts/authentik/Chart.yaml
|
||||
sed -i 's/^appVersion: .*/appVersion: ${{ inputs.version }}/' charts/authentik/Chart.yaml
|
||||
sed -i 's/upgrade to authentik .*/upgrade to authentik ${{ inputs.version }}/' charts/authentik/Chart.yaml
|
||||
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@v7
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
branch: bump-${{ inputs.version }}
|
||||
commit-message: "charts/authentik: bump to ${{ inputs.version }}"
|
||||
title: "charts/authentik: bump to ${{ inputs.version }}"
|
||||
body: "charts/authentik: bump to ${{ inputs.version }}"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
author: "${{ steps.app-token.outputs.app-slug }}[bot] <${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>"
|
||||
bump-version:
|
||||
name: Bump version repository
|
||||
if: ${{ inputs.release_reason != 'prerelease' }}
|
||||
needs:
|
||||
- check-inputs
|
||||
- bump-authentik
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
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@v5
|
||||
with:
|
||||
repository: "${{ github.repository_owner }}/version"
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
- name: Bump version
|
||||
if: "${{ inputs.release_reason == 'feature' }}"
|
||||
run: |
|
||||
changelog_url="https://docs.goauthentik.io/docs/releases/${{ needs.check-inputs.outputs.major_version }}"
|
||||
jq \
|
||||
--arg version "${{ inputs.version }}" \
|
||||
--arg changelog "See ${changelog_url}" \
|
||||
--arg changelog_url "${changelog_url}" \
|
||||
'.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')"
|
||||
jq \
|
||||
--arg version "${{ inputs.version }}" \
|
||||
--arg changelog "See ${changelog_url}" \
|
||||
--arg changelog_url "${changelog_url}" \
|
||||
'.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@v7
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
branch: bump-${{ inputs.version }}
|
||||
commit-message: "version: bump to ${{ inputs.version }}"
|
||||
title: "version: bump to ${{ inputs.version }}"
|
||||
body: "version: bump to ${{ inputs.version }}"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
author: "${{ steps.app-token.outputs.app-slug }}[bot] <${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>"
|
||||
prerelease: ${{ steps.ev.outputs.prerelease == 'true' }}
|
||||
|
||||
5
.github/workflows/repo-mirror-cleanup.yml
vendored
5
.github/workflows/repo-mirror-cleanup.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: Repo - Cleanup internal mirror
|
||||
name: "authentik-repo-mirror-cleanup"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -9,7 +8,7 @@ jobs:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- if: ${{ env.MIRROR_KEY != '' }}
|
||||
|
||||
5
.github/workflows/repo-mirror.yml
vendored
5
.github/workflows/repo-mirror.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: Repo - Mirror to internal
|
||||
name: "authentik-repo-mirror"
|
||||
|
||||
on: [push, delete]
|
||||
|
||||
@@ -8,7 +7,7 @@ jobs:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- if: ${{ env.MIRROR_KEY != '' }}
|
||||
|
||||
3
.github/workflows/repo-stale.yml
vendored
3
.github/workflows/repo-stale.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: Repo - Mark and close stale issues
|
||||
name: "authentik-repo-stale"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
name: QA - Semgrep
|
||||
|
||||
name: authentik-semgrep
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
pull_request: {}
|
||||
@@ -9,11 +7,10 @@ on:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/qa-semgrep.yml
|
||||
- .github/workflows/semgrep.yml
|
||||
schedule:
|
||||
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
|
||||
- cron: '12 15 * * *'
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
name: semgrep/ci
|
||||
@@ -26,5 +23,5 @@ jobs:
|
||||
image: semgrep/semgrep
|
||||
if: (github.actor != 'dependabot[bot]')
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- run: semgrep ci
|
||||
3
.github/workflows/translation-advice.yml
vendored
3
.github/workflows/translation-advice.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: Translation - Post advice
|
||||
name: authentik-translation-advice
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
name: Translation - Extract and compile
|
||||
|
||||
name: authentik-translate-extract-compile
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # every day at midnight
|
||||
@@ -26,11 +25,11 @@ jobs:
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
5
.github/workflows/translation-rename.yml
vendored
5
.github/workflows/translation-rename.yml
vendored
@@ -1,7 +1,6 @@
|
||||
---
|
||||
# Rename transifex pull requests to have a correct naming
|
||||
# Also enables auto squash-merge
|
||||
name: Translation - Auto-rename Transifex PRs
|
||||
name: authentik-translation-transifex-rename
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -16,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
|
||||
54
Dockerfile
54
Dockerfile
@@ -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.25-bookworm AS go-builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -76,12 +76,12 @@ 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.8.11 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.4 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.6-slim-bookworm-fips AS python-base
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/ak-root/lifecycle:/ak-root/venv/bin:$PATH" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_NATIVE_TLS=1 \
|
||||
@@ -134,16 +134,13 @@ ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
|
||||
org.opencontainers.image.description="goauthentik.io Main server image, see https://goauthentik.io for more info." \
|
||||
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
|
||||
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
|
||||
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
|
||||
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
|
||||
org.opencontainers.image.title="authentik server image" \
|
||||
org.opencontainers.image.url="https://goauthentik.io" \
|
||||
org.opencontainers.image.vendor="Authentik Security Inc." \
|
||||
org.opencontainers.image.version=${VERSION}
|
||||
LABEL org.opencontainers.image.url=https://goauthentik.io
|
||||
LABEL org.opencontainers.image.description="goauthentik.io Main server image, see https://goauthentik.io for more info."
|
||||
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
|
||||
LABEL org.opencontainers.image.version=${VERSION}
|
||||
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# We cannot cache this layer otherwise we'll end up with a bigger image
|
||||
RUN apt-get update && \
|
||||
@@ -155,26 +152,27 @@ RUN apt-get update && \
|
||||
pip3 install --no-cache-dir --upgrade pip && \
|
||||
apt-get clean && \
|
||||
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /ak-root authentik && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
||||
mkdir -p /certs /media /blueprints && \
|
||||
mkdir -p /ak-root/authentik/.ssh && \
|
||||
chown authentik:authentik /certs /media /ak-root/authentik/.ssh /ak-root
|
||||
mkdir -p /authentik/.ssh && \
|
||||
mkdir -p /ak-root && \
|
||||
chown authentik:authentik /certs /media /authentik/.ssh /ak-root
|
||||
|
||||
COPY ./authentik/ /ak-root/authentik
|
||||
COPY ./pyproject.toml /ak-root/
|
||||
COPY ./uv.lock /ak-root/
|
||||
COPY ./schemas /ak-root/schemas
|
||||
COPY ./locale /ak-root/locale
|
||||
COPY ./tests /ak-root/tests
|
||||
COPY ./manage.py /ak-root/
|
||||
COPY ./authentik/ /authentik
|
||||
COPY ./pyproject.toml /
|
||||
COPY ./uv.lock /
|
||||
COPY ./schemas /schemas
|
||||
COPY ./locale /locale
|
||||
COPY ./tests /tests
|
||||
COPY ./manage.py /
|
||||
COPY ./blueprints /blueprints
|
||||
COPY ./lifecycle/ /ak-root/lifecycle
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
|
||||
COPY --from=go-builder /go/authentik /bin/authentik
|
||||
COPY ./packages/ /ak-root/packages
|
||||
COPY --from=python-deps /ak-root/.venv /ak-root/.venv
|
||||
COPY --from=node-builder /work/web/dist/ /ak-root/web/dist/
|
||||
COPY --from=node-builder /work/web/authentik/ /ak-root/web/authentik/
|
||||
COPY --from=node-builder /work/web/dist/ /web/dist/
|
||||
COPY --from=node-builder /work/web/authentik/ /web/authentik/
|
||||
COPY --from=geoip /usr/share/GeoIP /geoip
|
||||
|
||||
USER 1000
|
||||
@@ -186,6 +184,4 @@ ENV TMPDIR=/dev/shm/ \
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
|
||||
|
||||
WORKDIR /ak-root
|
||||
|
||||
ENTRYPOINT [ "dumb-init", "--", "ak" ]
|
||||
|
||||
21
Makefile
21
Makefile
@@ -16,7 +16,6 @@ GEN_API_GO = gen-go-api
|
||||
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)
|
||||
redis_db := $(shell uv run python -m authentik.lib.config redis.db 2>/dev/null)
|
||||
|
||||
all: lint-fix lint test gen web ## Lint, build, and test everything
|
||||
|
||||
@@ -58,7 +57,7 @@ migrate: ## Run the Authentik Django server's migrations
|
||||
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 run aws-cfn
|
||||
|
||||
run-server: ## Run the main authentik server process
|
||||
uv run ak server
|
||||
@@ -80,10 +79,10 @@ core-i18n-extract:
|
||||
install: node-install docs-install core-install ## Install all requires dependencies for `node`, `docs` and `core`
|
||||
|
||||
dev-drop-db:
|
||||
dropdb -U ${pg_user} -h ${pg_host} ${pg_name} || true
|
||||
dropdb -U ${pg_user} -h ${pg_host} ${pg_name}
|
||||
# Also remove the test-db if it exists
|
||||
dropdb -U ${pg_user} -h ${pg_host} test_${pg_name} || true
|
||||
redis-cli -n ${redis_db} flushall
|
||||
redis-cli -n 0 flushall
|
||||
|
||||
dev-create-db:
|
||||
createdb -U ${pg_user} -h ${pg_host} ${pg_name}
|
||||
@@ -94,17 +93,6 @@ update-test-mmdb: ## Update test GeoIP and ASN Databases
|
||||
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb
|
||||
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb
|
||||
|
||||
bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
|
||||
ifndef version
|
||||
$(error Usage: make bump version=20xx.xx.xx )
|
||||
endif
|
||||
sed -i 's/^version = ".*"/version = "$(version)"/' pyproject.toml
|
||||
sed -i 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
|
||||
$(MAKE) gen-build gen-compose aws-cfn
|
||||
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
|
||||
|
||||
#########################
|
||||
## API Schema
|
||||
#########################
|
||||
@@ -119,9 +107,6 @@ gen-build: ## Extract the schema from the database
|
||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||
uv run ak spectacular --file schema.yml
|
||||
|
||||
gen-compose:
|
||||
uv run scripts/generate_docker_compose.py
|
||||
|
||||
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
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
|
||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
|
||||
[](https://codecov.io/gh/goauthentik/authentik)
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://www.transifex.com/authentik/authentik/)
|
||||
|
||||
## What is authentik?
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
"""authentik root module"""
|
||||
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
|
||||
VERSION = "2025.10.0-rc1"
|
||||
__version__ = "2025.6.4"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
@lru_cache
|
||||
def authentik_version() -> str:
|
||||
return VERSION
|
||||
|
||||
|
||||
@lru_cache
|
||||
def authentik_build_hash(fallback: str | None = None) -> str:
|
||||
def get_build_hash(fallback: str | None = None) -> str:
|
||||
"""Get build hash"""
|
||||
build_hash = environ.get(ENV_GIT_HASH_KEY, fallback if fallback else "")
|
||||
return fallback if build_hash == "" and fallback else build_hash
|
||||
|
||||
|
||||
@lru_cache
|
||||
def authentik_full_version() -> str:
|
||||
def get_full_version() -> str:
|
||||
"""Get full version, with build hash appended"""
|
||||
version = authentik_version()
|
||||
if (build_hash := authentik_build_hash()) != "":
|
||||
version = __version__
|
||||
if (build_hash := get_build_hash()) != "":
|
||||
return f"{version}+{build_hash}"
|
||||
return version
|
||||
|
||||
@@ -16,7 +16,7 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik import get_full_version
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.lib.config import CONFIG
|
||||
@@ -78,7 +78,7 @@ class SystemInfoSerializer(PassiveSerializer):
|
||||
"""Get versions"""
|
||||
return {
|
||||
"architecture": platform.machine(),
|
||||
"authentik_version": authentik_full_version(),
|
||||
"authentik_version": get_full_version(),
|
||||
"environment": get_env(),
|
||||
"openssl_fips_enabled": (
|
||||
backend._fips_enabled if LicenseKey.get_total().status().is_valid else None
|
||||
|
||||
@@ -10,7 +10,7 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik import authentik_build_hash, authentik_version
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.admin.tasks import VERSION_CACHE_KEY, VERSION_NULL, update_latest_version
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.outposts.models import Outpost
|
||||
@@ -29,20 +29,20 @@ class VersionSerializer(PassiveSerializer):
|
||||
|
||||
def get_build_hash(self, _) -> str:
|
||||
"""Get build hash, if version is not latest or released"""
|
||||
return authentik_build_hash()
|
||||
return get_build_hash()
|
||||
|
||||
def get_version_current(self, _) -> str:
|
||||
"""Get current version"""
|
||||
return authentik_version()
|
||||
return __version__
|
||||
|
||||
def get_version_latest(self, _) -> str:
|
||||
"""Get latest version from cache"""
|
||||
if get_current_tenant().schema_name == get_public_schema_name():
|
||||
return authentik_version()
|
||||
return __version__
|
||||
version_in_cache = cache.get(VERSION_CACHE_KEY)
|
||||
if not version_in_cache: # pragma: no cover
|
||||
update_latest_version.send()
|
||||
return authentik_version()
|
||||
return __version__
|
||||
return version_in_cache
|
||||
|
||||
def get_version_latest_valid(self, _) -> bool:
|
||||
|
||||
@@ -8,7 +8,7 @@ from packaging.version import parse
|
||||
from requests import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import authentik_build_hash, authentik_version
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.admin.apps import PROM_INFO
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
@@ -19,16 +19,16 @@ LOGGER = get_logger()
|
||||
VERSION_NULL = "0.0.0"
|
||||
VERSION_CACHE_KEY = "authentik_latest_version"
|
||||
VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours
|
||||
LOCAL_VERSION = parse(authentik_version())
|
||||
LOCAL_VERSION = parse(__version__)
|
||||
|
||||
|
||||
def _set_prom_info():
|
||||
"""Set prometheus info for version"""
|
||||
PROM_INFO.info(
|
||||
{
|
||||
"version": authentik_version(),
|
||||
"version": __version__,
|
||||
"latest": cache.get(VERSION_CACHE_KEY, ""),
|
||||
"build_hash": authentik_build_hash(),
|
||||
"build_hash": get_build_hash(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from json import loads
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik import authentik_version
|
||||
from authentik import __version__
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.lib.generators import generate_id
|
||||
@@ -27,7 +27,7 @@ class TestAdminAPI(TestCase):
|
||||
response = self.client.get(reverse("authentik_api:admin_version"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content)
|
||||
self.assertEqual(body["version_current"], authentik_version())
|
||||
self.assertEqual(body["version_current"], __version__)
|
||||
|
||||
def test_apps(self):
|
||||
"""Test apps API"""
|
||||
|
||||
@@ -8,6 +8,8 @@ API Browser - {{ brand.branding_title }}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% versioned_script 'dist/standalone/api-browser/index-%v.js' %}" type="module"></script>
|
||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -11,7 +11,7 @@ from rest_framework.relations import PrimaryKeyRelatedField
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import authentik_version
|
||||
from authentik import __version__
|
||||
from authentik.blueprints.v1.common import BlueprintEntryDesiredState
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, is_model_allowed
|
||||
from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry
|
||||
@@ -48,7 +48,7 @@ class Command(BaseCommand):
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": f"authentik {authentik_version()} Blueprint schema",
|
||||
"title": f"authentik {__version__} Blueprint schema",
|
||||
"required": ["version", "entries"],
|
||||
"properties": {
|
||||
"version": {
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
from typing import Any
|
||||
|
||||
from django.db import models
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from drf_spectacular.utils import extend_schema
|
||||
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
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
@@ -18,8 +18,6 @@ from authentik.brands.models import Brand
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
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
|
||||
from authentik.tenants.utils import get_current_tenant
|
||||
|
||||
|
||||
@@ -112,16 +110,6 @@ class CurrentBrandSerializer(PassiveSerializer):
|
||||
flow_device_code = CharField(source="flow_device_code.slug", required=False)
|
||||
|
||||
default_locale = CharField(read_only=True)
|
||||
flags = SerializerMethodField()
|
||||
|
||||
@extend_schema_field(field=FlagJSONField)
|
||||
def get_flags(self, _):
|
||||
values = {}
|
||||
for flag in Flag.available():
|
||||
_flag = flag()
|
||||
if _flag.visibility == "public":
|
||||
values[_flag.key] = _flag.get()
|
||||
return values
|
||||
|
||||
|
||||
class BrandViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@@ -10,20 +10,11 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_brand
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
from authentik.providers.saml.models import SAMLProvider
|
||||
from authentik.tenants.flags import Flag
|
||||
|
||||
|
||||
class TestBrands(APITestCase):
|
||||
"""Test brands"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.default_flags = {}
|
||||
for flag in Flag.available():
|
||||
_flag = flag()
|
||||
if _flag.visibility == "public":
|
||||
self.default_flags[_flag.key] = _flag.get()
|
||||
|
||||
def test_current_brand(self):
|
||||
"""Test Current brand API"""
|
||||
brand = create_test_brand()
|
||||
@@ -38,7 +29,6 @@ class TestBrands(APITestCase):
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -59,7 +49,27 @@ class TestBrands(APITestCase):
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
def test_brand_subdomain_same_suffix(self):
|
||||
"""Test Current brand API"""
|
||||
Brand.objects.all().delete()
|
||||
Brand.objects.create(domain="bar.baz", branding_title="custom")
|
||||
Brand.objects.create(domain="foo.bar.baz", branding_title="custom")
|
||||
self.assertJSONEqual(
|
||||
self.client.get(
|
||||
reverse("authentik_api:brand-current"), HTTP_HOST="foo.bar.baz"
|
||||
).content.decode(),
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_title": "custom",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "foo.bar.baz",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -77,7 +87,6 @@ class TestBrands(APITestCase):
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -158,7 +167,6 @@ class TestBrands(APITestCase):
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ from typing import Any
|
||||
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import Value as V
|
||||
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
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik import get_full_version
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.lib.sentry import get_http_meta
|
||||
from authentik.tenants.models import Tenant
|
||||
@@ -20,9 +21,9 @@ DEFAULT_BRAND = Brand(domain="fallback")
|
||||
def get_brand_for_request(request: HttpRequest) -> Brand:
|
||||
"""Get brand object for current request"""
|
||||
db_brands = (
|
||||
Brand.objects.annotate(host_domain=V(request.get_host()))
|
||||
Brand.objects.annotate(host_domain=V(request.get_host()), match_length=Length("domain"))
|
||||
.filter(Q(host_domain__iendswith=F("domain")) | _q_default)
|
||||
.order_by("default")
|
||||
.order_by("-match_length", "default")
|
||||
)
|
||||
brands = list(db_brands.all())
|
||||
if len(brands) < 1:
|
||||
@@ -43,5 +44,5 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
|
||||
"brand_css": brand_css,
|
||||
"footer_links": tenant.footer_links,
|
||||
"html_meta": {**get_http_meta()},
|
||||
"version": authentik_full_version(),
|
||||
"version": get_full_version(),
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ from copy import copy
|
||||
from django.core.cache import cache
|
||||
from django.db.models import QuerySet
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
@@ -67,15 +66,6 @@ class ApplicationSerializer(ModelSerializer):
|
||||
user = self.context["request"].user
|
||||
return app.get_launch_url(user)
|
||||
|
||||
def validate_slug(self, slug: str) -> str:
|
||||
if slug in Application.reserved_slugs:
|
||||
raise ValidationError(
|
||||
_("The slug '{slug}' is reserved and cannot be used for applications.").format(
|
||||
slug=slug
|
||||
)
|
||||
)
|
||||
return slug
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
||||
|
||||
@@ -49,28 +49,11 @@ class GroupMemberSerializer(ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class GroupChildSerializer(ModelSerializer):
|
||||
"""Stripped down group serializer to show relevant children for groups"""
|
||||
|
||||
attributes = JSONDictField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"is_superuser",
|
||||
"attributes",
|
||||
"group_uuid",
|
||||
]
|
||||
|
||||
|
||||
class GroupSerializer(ModelSerializer):
|
||||
"""Group Serializer"""
|
||||
|
||||
attributes = JSONDictField(required=False)
|
||||
users_obj = SerializerMethodField(allow_null=True)
|
||||
children_obj = SerializerMethodField(allow_null=True)
|
||||
roles_obj = ListSerializer(
|
||||
child=RoleSerializer(),
|
||||
read_only=True,
|
||||
@@ -78,6 +61,7 @@ class GroupSerializer(ModelSerializer):
|
||||
required=False,
|
||||
)
|
||||
parent_name = CharField(source="parent.name", read_only=True, allow_null=True)
|
||||
|
||||
num_pk = IntegerField(read_only=True)
|
||||
|
||||
@property
|
||||
@@ -87,25 +71,12 @@ class GroupSerializer(ModelSerializer):
|
||||
return True
|
||||
return str(request.query_params.get("include_users", "true")).lower() == "true"
|
||||
|
||||
@property
|
||||
def _should_include_children(self) -> bool:
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return True
|
||||
return str(request.query_params.get("include_children", "false")).lower() == "true"
|
||||
|
||||
@extend_schema_field(GroupMemberSerializer(many=True))
|
||||
def get_users_obj(self, instance: Group) -> list[GroupMemberSerializer] | None:
|
||||
if not self._should_include_users:
|
||||
return None
|
||||
return GroupMemberSerializer(instance.users, many=True).data
|
||||
|
||||
@extend_schema_field(GroupChildSerializer(many=True))
|
||||
def get_children_obj(self, instance: Group) -> list[GroupChildSerializer] | None:
|
||||
if not self._should_include_children:
|
||||
return None
|
||||
return GroupChildSerializer(instance.children, many=True).data
|
||||
|
||||
def validate_parent(self, parent: Group | None):
|
||||
"""Validate group parent (if set), ensuring the parent isn't itself"""
|
||||
if not self.instance or not parent:
|
||||
@@ -155,17 +126,11 @@ class GroupSerializer(ModelSerializer):
|
||||
"attributes",
|
||||
"roles",
|
||||
"roles_obj",
|
||||
"children",
|
||||
"children_obj",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"users": {
|
||||
"default": list,
|
||||
},
|
||||
"children": {
|
||||
"required": False,
|
||||
"default": list,
|
||||
},
|
||||
# TODO: This field isn't unique on the database which is hard to backport
|
||||
# hence we just validate the uniqueness here
|
||||
"name": {"validators": [UniqueValidator(Group.objects.all())]},
|
||||
@@ -238,15 +203,11 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
Prefetch("users", queryset=User.objects.all().only("id"))
|
||||
)
|
||||
|
||||
if self.serializer_class(context={"request": self.request})._should_include_children:
|
||||
base_qs = base_qs.prefetch_related("children")
|
||||
|
||||
return base_qs
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter("include_users", bool, default=True),
|
||||
OpenApiParameter("include_children", bool, default=False),
|
||||
]
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
@@ -255,7 +216,6 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter("include_users", bool, default=True),
|
||||
OpenApiParameter("include_children", bool, default=False),
|
||||
]
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
@@ -5,7 +5,7 @@ from json import loads
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.models import AnonymousUser, Permission
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
from django.urls import reverse_lazy
|
||||
@@ -16,7 +16,6 @@ from django.utils.translation import gettext as _
|
||||
from django_filters.filters import (
|
||||
BooleanFilter,
|
||||
CharFilter,
|
||||
IsoDateTimeFilter,
|
||||
ModelMultipleChoiceFilter,
|
||||
MultipleChoiceFilter,
|
||||
UUIDFilter,
|
||||
@@ -154,8 +153,7 @@ class UserSerializer(ModelSerializer):
|
||||
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
||||
self.fields["password"] = CharField(required=False, allow_null=True)
|
||||
self.fields["permissions"] = ListField(
|
||||
required=False,
|
||||
child=ChoiceField(choices=get_permission_choices()),
|
||||
required=False, child=ChoiceField(choices=get_permission_choices())
|
||||
)
|
||||
|
||||
def create(self, validated_data: dict) -> User:
|
||||
@@ -243,7 +241,6 @@ class UserSerializer(ModelSerializer):
|
||||
"type",
|
||||
"uuid",
|
||||
"password_change_date",
|
||||
"last_updated",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"allow_blank": True},
|
||||
@@ -270,10 +267,7 @@ class UserSelfSerializer(ModelSerializer):
|
||||
ListSerializer(
|
||||
child=inline_serializer(
|
||||
"UserSelfGroups",
|
||||
{
|
||||
"name": CharField(read_only=True),
|
||||
"pk": CharField(read_only=True),
|
||||
},
|
||||
{"name": CharField(read_only=True), "pk": CharField(read_only=True)},
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -321,8 +315,7 @@ class UserSelfSerializer(ModelSerializer):
|
||||
|
||||
class SessionUserSerializer(PassiveSerializer):
|
||||
"""Response for the /user/me endpoint, returns the currently active user (as `user` property)
|
||||
and, if this user is being impersonated, the original user in the `original` property.
|
||||
"""
|
||||
and, if this user is being impersonated, the original user in the `original` property."""
|
||||
|
||||
user = UserSelfSerializer()
|
||||
original = UserSelfSerializer(required=False)
|
||||
@@ -338,14 +331,6 @@ class UsersFilter(FilterSet):
|
||||
method="filter_attributes",
|
||||
)
|
||||
|
||||
date_joined__lt = IsoDateTimeFilter(field_name="date_joined", lookup_expr="lt")
|
||||
date_joined = IsoDateTimeFilter(field_name="date_joined")
|
||||
date_joined__gt = IsoDateTimeFilter(field_name="date_joined", lookup_expr="gt")
|
||||
|
||||
last_updated__lt = IsoDateTimeFilter(field_name="last_updated", lookup_expr="lt")
|
||||
last_updated = IsoDateTimeFilter(field_name="last_updated")
|
||||
last_updated__gt = IsoDateTimeFilter(field_name="last_updated", lookup_expr="gt")
|
||||
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
||||
uuid = UUIDFilter(field_name="uuid")
|
||||
|
||||
@@ -391,8 +376,6 @@ class UsersFilter(FilterSet):
|
||||
fields = [
|
||||
"username",
|
||||
"email",
|
||||
"date_joined",
|
||||
"last_updated",
|
||||
"name",
|
||||
"is_active",
|
||||
"is_superuser",
|
||||
@@ -407,18 +390,15 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
"""User Viewset"""
|
||||
|
||||
queryset = User.objects.none()
|
||||
ordering = ["username", "date_joined", "last_updated"]
|
||||
ordering = ["username"]
|
||||
serializer_class = UserSerializer
|
||||
filterset_class = UsersFilter
|
||||
search_fields = ["email", "name", "uuid", "username"]
|
||||
search_fields = ["username", "name", "is_active", "email", "uuid", "attributes"]
|
||||
|
||||
def get_ql_fields(self):
|
||||
from djangoql.schema import BoolField, StrField
|
||||
|
||||
from authentik.enterprise.search.fields import (
|
||||
ChoiceSearchField,
|
||||
JSONSearchField,
|
||||
)
|
||||
from authentik.enterprise.search.fields import ChoiceSearchField, JSONSearchField
|
||||
|
||||
return [
|
||||
StrField(User, "username"),
|
||||
@@ -455,7 +435,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
user: User = self.get_object()
|
||||
planner = FlowPlanner(flow)
|
||||
planner.allow_empty_flows = True
|
||||
self.request._request.user = AnonymousUser()
|
||||
try:
|
||||
plan = planner.plan(
|
||||
self.request._request,
|
||||
@@ -513,12 +492,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
},
|
||||
)
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["POST"],
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
)
|
||||
@action(detail=False, methods=["POST"], pagination_class=None, filter_backends=[])
|
||||
def service_account(self, request: Request) -> Response:
|
||||
"""Create a new user account that is marked as a service account"""
|
||||
username = request.data.get("name")
|
||||
@@ -562,13 +536,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
return Response(data={"non_field_errors": [str(exc)]}, status=400)
|
||||
|
||||
@extend_schema(responses={200: SessionUserSerializer(many=False)})
|
||||
@action(
|
||||
url_path="me",
|
||||
url_name="me",
|
||||
detail=False,
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
)
|
||||
@action(url_path="me", url_name="me", detail=False, pagination_class=None, filter_backends=[])
|
||||
def user_me(self, request: Request) -> Response:
|
||||
"""Get information about current user"""
|
||||
context = {"request": request}
|
||||
@@ -620,7 +588,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
def recovery(self, request: Request, pk: int) -> Response:
|
||||
"""Create a temporary link that a user can use to recover their account"""
|
||||
"""Create a temporary link that a user can use to recover their accounts"""
|
||||
link, _ = self._create_recovery_link()
|
||||
return Response({"link": link})
|
||||
|
||||
@@ -641,7 +609,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
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"""
|
||||
"""Create a temporary link that a user can use to recover their accounts"""
|
||||
for_user: User = self.get_object()
|
||||
if for_user.email == "":
|
||||
LOGGER.debug("User doesn't have an email address")
|
||||
@@ -694,18 +662,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
if not request.user.has_perm(
|
||||
"authentik_core.impersonate", user_to_be
|
||||
) and not request.user.has_perm("authentik_core.impersonate"):
|
||||
LOGGER.debug(
|
||||
"User attempted to impersonate without permissions",
|
||||
user=request.user,
|
||||
)
|
||||
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
|
||||
return Response(status=401)
|
||||
if user_to_be.pk == self.request.user.pk:
|
||||
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
|
||||
return Response(status=401)
|
||||
if not reason and request.tenant.impersonation_require_reason:
|
||||
LOGGER.debug(
|
||||
"User attempted to impersonate without providing a reason",
|
||||
user=request.user,
|
||||
"User attempted to impersonate without providing a reason", user=request.user
|
||||
)
|
||||
return Response(status=401)
|
||||
|
||||
@@ -744,8 +708,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: inline_serializer(
|
||||
"UserPathSerializer",
|
||||
{"paths": ListField(child=CharField(), read_only=True)},
|
||||
"UserPathSerializer", {"paths": ListField(child=CharField(), read_only=True)}
|
||||
)
|
||||
},
|
||||
parameters=[
|
||||
|
||||
@@ -21,6 +21,8 @@ from rest_framework.serializers import (
|
||||
raise_errors_on_nested_writes,
|
||||
)
|
||||
|
||||
from authentik.rbac.permissions import assign_initial_permissions
|
||||
|
||||
|
||||
def is_dict(value: Any):
|
||||
"""Ensure a value is a dictionary, useful for JSONFields"""
|
||||
@@ -50,6 +52,15 @@ class ModelSerializer(BaseModelSerializer):
|
||||
serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy()
|
||||
serializer_field_mapping[models.JSONField] = JSONDictField
|
||||
|
||||
def create(self, validated_data):
|
||||
instance = super().create(validated_data)
|
||||
|
||||
request = self.context.get("request")
|
||||
if request and hasattr(request, "user") and not request.user.is_anonymous:
|
||||
assign_initial_permissions(request.user, instance)
|
||||
|
||||
return instance
|
||||
|
||||
def update(self, instance: Model, validated_data):
|
||||
raise_errors_on_nested_writes("update", self, validated_data)
|
||||
info = model_meta.get_field_info(instance)
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.core.management.base import BaseCommand
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik import get_full_version
|
||||
from authentik.core.models import User
|
||||
from authentik.events.middleware import should_log_model
|
||||
from authentik.events.models import Event, EventAction
|
||||
@@ -19,7 +19,7 @@ from authentik.events.utils import model_to_dict
|
||||
|
||||
|
||||
def get_banner_text(shell_type="shell") -> str:
|
||||
return f"""### authentik {shell_type} ({authentik_full_version()})
|
||||
return f"""### authentik {shell_type} ({get_full_version()})
|
||||
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 5.1.11 on 2025-07-15 15:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("authentik_core", "0049_alter_token_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="last_updated",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="user",
|
||||
index=models.Index(fields=["last_updated"], name="authentik_c_last_up_ed7486_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="user",
|
||||
index=models.Index(fields=["date_joined"], name="authentik_c_date_jo_58c256_idx"),
|
||||
),
|
||||
]
|
||||
@@ -274,8 +274,6 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
||||
ak_groups = models.ManyToManyField("Group", related_name="users")
|
||||
password_change_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
last_updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
class Meta:
|
||||
@@ -295,8 +293,6 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
||||
models.Index(fields=["uuid"]),
|
||||
models.Index(fields=["path"]),
|
||||
models.Index(fields=["type"]),
|
||||
models.Index(fields=["date_joined"]),
|
||||
models.Index(fields=["last_updated"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
@@ -548,9 +544,6 @@ class Application(SerializerModel, PolicyBindingModel):
|
||||
|
||||
objects = ApplicationQuerySet.as_manager()
|
||||
|
||||
# Reserved slugs that would clash with OAuth2 provider endpoints
|
||||
reserved_slugs = ["authorize", "token", "device", "userinfo", "introspect", "revoke"]
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
from authentik.core.api.applications import ApplicationSerializer
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
|
||||
{% block head_before %}
|
||||
{% endblock %}
|
||||
|
||||
{% include "base/theme.html" %}
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
|
||||
<style>{{ brand_css }}</style>
|
||||
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{% if ui_theme == "dark" %}
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="theme-color" content="#18191a">
|
||||
{% elif ui_theme == "light" %}
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
{% else %}
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
||||
{% endif %}
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
{% block head %}
|
||||
<script src="{% versioned_script 'dist/admin/AdminInterface-%v.js' %}" type="module"></script>
|
||||
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||
{% include "base/header_js.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
{% block head %}
|
||||
<script src="{% versioned_script 'dist/user/UserInterface-%v.js' %}" type="module"></script>
|
||||
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
|
||||
{% include "base/header_js.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from django import template
|
||||
from django.templatetags.static import static as static_loader
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik import get_full_version
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -11,4 +11,4 @@ register = template.Library()
|
||||
@register.simple_tag()
|
||||
def versioned_script(path: str) -> str:
|
||||
"""Wrapper around {% static %} tag that supports setting the version"""
|
||||
return static_loader(path.replace("%v", authentik_full_version()))
|
||||
return static_loader(path.replace("%v", get_full_version()))
|
||||
|
||||
@@ -257,35 +257,3 @@ class TestApplicationsAPI(APITestCase):
|
||||
self.assertEqual(
|
||||
Application.objects.with_provider().get(slug=slug).get_provider(), provider
|
||||
)
|
||||
|
||||
def test_create_application_with_reserved_slug(self):
|
||||
"""Test creating an application with a reserved slug"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:application-list"),
|
||||
{
|
||||
"name": "Test Application",
|
||||
"slug": Application.reserved_slugs[0],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("slug", response.data)
|
||||
self.assertIn("reserved", response.data["slug"][0])
|
||||
|
||||
def test_update_application_with_reserved_slug(self):
|
||||
"""Test updating an application to use a reserved slug"""
|
||||
self.client.force_login(self.user)
|
||||
app = Application.objects.create(
|
||||
name="Test Application",
|
||||
slug="valid-slug",
|
||||
)
|
||||
|
||||
response = self.client.patch(
|
||||
reverse("authentik_api:application-detail", kwargs={"slug": app.slug}),
|
||||
{
|
||||
"slug": Application.reserved_slugs[0],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("slug", response.data)
|
||||
self.assertIn("reserved", response.data["slug"][0])
|
||||
|
||||
@@ -21,7 +21,7 @@ from authentik.core.tests.utils import (
|
||||
create_test_flow,
|
||||
create_test_user,
|
||||
)
|
||||
from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.stages.email.models import EmailStage
|
||||
|
||||
@@ -103,11 +103,8 @@ class TestUsersAPI(APITestCase):
|
||||
self.assertTrue(self.admin.check_password(new_pw))
|
||||
|
||||
def test_recovery(self):
|
||||
"""Test user recovery link"""
|
||||
flow = create_test_flow(
|
||||
FlowDesignation.RECOVERY,
|
||||
authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED,
|
||||
)
|
||||
"""Test user recovery link (no recovery flow set)"""
|
||||
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||
brand: Brand = create_test_brand()
|
||||
brand.flow_recovery = flow
|
||||
brand.save()
|
||||
@@ -390,72 +387,3 @@ class TestUsersAPI(APITestCase):
|
||||
self.assertFalse(
|
||||
AuthenticatedSession.objects.filter(session__session_key=session_id).exists()
|
||||
)
|
||||
|
||||
def test_sort_by_last_updated(self):
|
||||
"""Test API sorting by last_updated"""
|
||||
User.objects.all().delete()
|
||||
admin = create_test_admin_user()
|
||||
self.client.force_login(admin)
|
||||
|
||||
user = create_test_user()
|
||||
admin.first_name = "Sample change"
|
||||
admin.last_name = "To trigger an update"
|
||||
admin.save()
|
||||
|
||||
# Ascending
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={
|
||||
"ordering": "last_updated",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 2)
|
||||
self.assertEqual(body["results"][0]["pk"], user.pk)
|
||||
|
||||
# Descending
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={
|
||||
"ordering": "-last_updated",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 2)
|
||||
self.assertEqual(body["results"][0]["pk"], admin.pk)
|
||||
|
||||
def test_sort_by_date_joined(self):
|
||||
"""Test API sorting by date_joined"""
|
||||
User.objects.all().delete()
|
||||
admin = create_test_admin_user()
|
||||
self.client.force_login(admin)
|
||||
|
||||
user = create_test_user()
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={
|
||||
"ordering": "date_joined",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 2)
|
||||
self.assertEqual(body["results"][0]["pk"], admin.pk)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={
|
||||
"ordering": "-date_joined",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 2)
|
||||
self.assertEqual(body["results"][0]["pk"], user.pk)
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import RedirectView, TemplateView
|
||||
from rest_framework.request import Request
|
||||
|
||||
from authentik import authentik_build_hash
|
||||
from authentik import get_build_hash
|
||||
from authentik.admin.tasks import LOCAL_VERSION
|
||||
from authentik.api.v3.config import ConfigView
|
||||
from authentik.brands.api import CurrentBrandSerializer
|
||||
@@ -46,13 +46,11 @@ class InterfaceView(TemplateView):
|
||||
"""Base interface view"""
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
brand = CurrentBrandSerializer(self.request.brand)
|
||||
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data)
|
||||
kwargs["ui_theme"] = brand.data["ui_theme"]
|
||||
kwargs["brand_json"] = dumps(brand.data)
|
||||
kwargs["brand_json"] = dumps(CurrentBrandSerializer(self.request.brand).data)
|
||||
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
|
||||
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
|
||||
kwargs["build"] = authentik_build_hash()
|
||||
kwargs["build"] = get_build_hash()
|
||||
kwargs["url_kwargs"] = self.kwargs
|
||||
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
|
||||
kwargs["base_url_rel"] = CONFIG.get("web.path", "/")
|
||||
|
||||
@@ -12,7 +12,7 @@ from cryptography.x509.oid import NameOID
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik import authentik_version
|
||||
from authentik import __version__
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ class CertificateBuilder:
|
||||
.issuer_name(
|
||||
x509.Name(
|
||||
[
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, f"authentik {authentik_version()}"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, f"authentik {__version__}"),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -55,7 +55,6 @@ class TestEnterpriseAudit(APITestCase):
|
||||
self.assertIsNotNone(event)
|
||||
self.assertIsNotNone(event.context["diff"])
|
||||
diff = event.context["diff"]
|
||||
diff.pop("last_updated")
|
||||
self.assertEqual(
|
||||
diff,
|
||||
{
|
||||
@@ -117,7 +116,6 @@ class TestEnterpriseAudit(APITestCase):
|
||||
self.assertIsNotNone(event)
|
||||
self.assertIsNotNone(event.context["diff"])
|
||||
diff = event.context["diff"]
|
||||
diff.pop("last_updated")
|
||||
self.assertEqual(
|
||||
diff,
|
||||
{
|
||||
|
||||
@@ -23,7 +23,6 @@ from authentik.events.models import (
|
||||
)
|
||||
from authentik.events.utils import get_user
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.stages.email.models import get_template_choices
|
||||
|
||||
|
||||
class NotificationTransportSerializer(ModelSerializer):
|
||||
@@ -31,18 +30,6 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||
|
||||
mode_verbose = SerializerMethodField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["email_template"].choices = get_template_choices()
|
||||
|
||||
def validate_email_template(self, value: str) -> str:
|
||||
"""Check validity of email template"""
|
||||
choices = get_template_choices()
|
||||
for path, _ in choices:
|
||||
if path == value:
|
||||
return value
|
||||
raise ValidationError(f"Invalid template '{value}' specified.")
|
||||
|
||||
def get_mode_verbose(self, instance: NotificationTransport) -> str:
|
||||
"""Return selected mode with a UI Label"""
|
||||
return TransportMode(instance.mode).label
|
||||
@@ -65,8 +52,6 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||
"webhook_url",
|
||||
"webhook_mapping_body",
|
||||
"webhook_mapping_headers",
|
||||
"email_subject_prefix",
|
||||
"email_template",
|
||||
"send_once",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.1.11 on 2025-08-14 13:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0011_alter_systemtask_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="notificationtransport",
|
||||
name="email_subject_prefix",
|
||||
field=models.TextField(blank=True, default="authentik Notification: "),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="notificationtransport",
|
||||
name="email_template",
|
||||
field=models.TextField(default="email/event_notification.html"),
|
||||
),
|
||||
]
|
||||
@@ -18,7 +18,7 @@ from requests import RequestException
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik import get_full_version
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.brands.utils import DEFAULT_BRAND
|
||||
from authentik.core.middleware import (
|
||||
@@ -41,7 +41,6 @@ from authentik.lib.utils.http import get_http_session
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.root.middleware import ClientIPMiddleware
|
||||
from authentik.stages.email.models import EmailTemplates
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
from authentik.tasks.models import TasksModel
|
||||
from authentik.tenants.models import Tenant
|
||||
@@ -296,15 +295,6 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
|
||||
name = models.TextField(unique=True)
|
||||
mode = models.TextField(choices=TransportMode.choices, default=TransportMode.LOCAL)
|
||||
send_once = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"Only send notification once, for example when sending a webhook into a chat channel."
|
||||
),
|
||||
)
|
||||
|
||||
email_subject_prefix = models.TextField(default="authentik Notification: ", blank=True)
|
||||
email_template = models.TextField(default=EmailTemplates.EVENT_NOTIFICATION)
|
||||
|
||||
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
|
||||
webhook_mapping_body = models.ForeignKey(
|
||||
@@ -329,6 +319,12 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
"Mapping should return a dictionary of key-value pairs"
|
||||
),
|
||||
)
|
||||
send_once = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"Only send notification once, for example when sending a webhook into a chat channel."
|
||||
),
|
||||
)
|
||||
|
||||
def send(self, notification: "Notification") -> list[str]:
|
||||
"""Send notification to user, called from async task"""
|
||||
@@ -438,7 +434,7 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
"title": notification.body,
|
||||
"color": "#fd4b2d",
|
||||
"fields": fields,
|
||||
"footer": f"authentik {authentik_full_version()}",
|
||||
"footer": f"authentik {get_full_version()}",
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -466,6 +462,7 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
notification=notification,
|
||||
)
|
||||
return None
|
||||
subject_prefix = "authentik Notification: "
|
||||
context = {
|
||||
"key_value": {
|
||||
"user_email": notification.user.email,
|
||||
@@ -493,10 +490,10 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
"from": self.name,
|
||||
}
|
||||
mail = TemplateEmailMessage(
|
||||
subject=self.email_subject_prefix + context["title"],
|
||||
subject=subject_prefix + context["title"],
|
||||
to=[(notification.user.name, notification.user.email)],
|
||||
language=notification.user.locale(),
|
||||
template_name=self.email_template,
|
||||
template_name="email/event_notification.html",
|
||||
template_context=context,
|
||||
)
|
||||
send_mail.send_with_options(args=(mail.__dict__,), rel_obj=self)
|
||||
|
||||
@@ -5,12 +5,10 @@ from unittest.mock import PropertyMock, patch
|
||||
from django.core import mail
|
||||
from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik import get_full_version
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.events.api.notification_transports import NotificationTransportSerializer
|
||||
from authentik.events.models import (
|
||||
Event,
|
||||
Notification,
|
||||
@@ -20,7 +18,6 @@ from authentik.events.models import (
|
||||
TransportMode,
|
||||
)
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.stages.email.models import get_template_choices
|
||||
|
||||
|
||||
class TestEventTransports(TestCase):
|
||||
@@ -121,7 +118,7 @@ class TestEventTransports(TestCase):
|
||||
{"short": True, "title": "Event user", "value": self.user.username},
|
||||
{"title": "foo", "value": "bar,"},
|
||||
],
|
||||
"footer": f"authentik {authentik_full_version()}",
|
||||
"footer": f"authentik {get_full_version()}",
|
||||
}
|
||||
],
|
||||
},
|
||||
@@ -141,76 +138,3 @@ class TestEventTransports(TestCase):
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik Notification: custom_foo")
|
||||
self.assertIn(self.notification.body, mail.outbox[0].alternatives[0][0])
|
||||
|
||||
def test_transport_email_custom_template(self):
|
||||
"""Test email transport with custom template"""
|
||||
transport: NotificationTransport = NotificationTransport.objects.create(
|
||||
name=generate_id(),
|
||||
mode=TransportMode.EMAIL,
|
||||
email_template="email/event_notification.html",
|
||||
)
|
||||
with patch(
|
||||
"authentik.stages.email.models.EmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
transport.send(self.notification)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertIn(self.notification.body, mail.outbox[0].alternatives[0][0])
|
||||
|
||||
def test_transport_email_custom_subject_prefix(self):
|
||||
"""Test email transport with custom subject prefix"""
|
||||
transport: NotificationTransport = NotificationTransport.objects.create(
|
||||
name=generate_id(),
|
||||
mode=TransportMode.EMAIL,
|
||||
email_subject_prefix="[CUSTOM] ",
|
||||
)
|
||||
with patch(
|
||||
"authentik.stages.email.models.EmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
transport.send(self.notification)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "[CUSTOM] custom_foo")
|
||||
|
||||
def test_transport_email_validation(self):
|
||||
"""Test email transport template validation"""
|
||||
|
||||
# Test valid template
|
||||
serializer = NotificationTransportSerializer(
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"mode": TransportMode.EMAIL,
|
||||
"email_template": "email/event_notification.html",
|
||||
}
|
||||
)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
# Test invalid template - should fail due to choices validation
|
||||
serializer = NotificationTransportSerializer(
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"mode": TransportMode.EMAIL,
|
||||
"email_template": "invalid/template.html",
|
||||
}
|
||||
)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("email_template", serializer.errors)
|
||||
|
||||
def test_templates_api_endpoint(self):
|
||||
"""Test templates API endpoint returns valid templates"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("authentik_api:emailstage-templates"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = response.json()
|
||||
self.assertIsInstance(data, list)
|
||||
|
||||
# Check that we have at least the default templates
|
||||
template_names = [item["name"] for item in data]
|
||||
self.assertIn("email/event_notification.html", template_names)
|
||||
|
||||
# Verify all templates are valid choices
|
||||
valid_choices = dict(get_template_choices())
|
||||
for template in data:
|
||||
self.assertIn(template["name"], valid_choices)
|
||||
self.assertEqual(template["description"], valid_choices[template["name"]])
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.core.management.base import BaseCommand
|
||||
from django.test import RequestFactory
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import authentik_version
|
||||
from authentik import __version__
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
@@ -99,7 +99,7 @@ class Command(BaseCommand):
|
||||
total_min: int = min(min(inner) for inner in values)
|
||||
total_avg = sum(sum(inner) for inner in values) / sum(len(inner) for inner in values)
|
||||
|
||||
print(f"Version: {authentik_version()}")
|
||||
print(f"Version: {__version__}")
|
||||
print(f"Processes: {len(values)}")
|
||||
print(f"\tMax: {total_max * 100}ms")
|
||||
print(f"\tMin: {total_min * 100}ms")
|
||||
|
||||
@@ -301,7 +301,6 @@ class SessionEndStage(ChallengeStageView):
|
||||
"flow_slug": self.request.brand.flow_invalidation.slug,
|
||||
},
|
||||
)
|
||||
|
||||
return SessionEndChallenge(data=data)
|
||||
|
||||
# This can never be reached since this challenge is created on demand and only the
|
||||
|
||||
@@ -14,7 +14,7 @@ LOG_PRE_CHAIN = [
|
||||
# is not from structlog.
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.processors.TimeStamper(fmt="iso", utc=False),
|
||||
structlog.processors.TimeStamper(),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
]
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from django.db import DatabaseError, InternalError, OperationalError, Programmin
|
||||
from django.http.response import Http404
|
||||
from django_redis.exceptions import ConnectionInterrupted
|
||||
from docker.errors import DockerException
|
||||
from dramatiq.errors import Retry
|
||||
from h11 import LocalProtocolError
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from psycopg.errors import Error
|
||||
@@ -22,7 +21,6 @@ from sentry_sdk import init as sentry_sdk_init
|
||||
from sentry_sdk.api import set_tag
|
||||
from sentry_sdk.integrations.argv import ArgvIntegration
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.dramatiq import DramatiqIntegration
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from sentry_sdk.integrations.socket import SocketIntegration
|
||||
from sentry_sdk.integrations.stdlib import StdlibIntegration
|
||||
@@ -31,7 +29,7 @@ from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME
|
||||
from structlog.stdlib import get_logger
|
||||
from websockets.exceptions import WebSocketException
|
||||
|
||||
from authentik import authentik_build_hash, authentik_version
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.http import authentik_user_agent
|
||||
from authentik.lib.utils.reflection import get_env
|
||||
@@ -70,8 +68,6 @@ ignored_classes = (
|
||||
LocalProtocolError,
|
||||
# rest_framework error
|
||||
APIException,
|
||||
# dramatiq errors
|
||||
Retry,
|
||||
# custom baseclass
|
||||
SentryIgnoredException,
|
||||
# ldap errors
|
||||
@@ -110,20 +106,19 @@ def sentry_init(**sentry_init_kwargs):
|
||||
dsn=CONFIG.get("error_reporting.sentry_dsn"),
|
||||
integrations=[
|
||||
ArgvIntegration(),
|
||||
DjangoIntegration(transaction_style="function_name", cache_spans=True),
|
||||
DramatiqIntegration(),
|
||||
RedisIntegration(),
|
||||
SocketIntegration(),
|
||||
StdlibIntegration(),
|
||||
DjangoIntegration(transaction_style="function_name", cache_spans=True),
|
||||
RedisIntegration(),
|
||||
ThreadingIntegration(propagate_hub=True),
|
||||
SocketIntegration(),
|
||||
],
|
||||
before_send=before_send,
|
||||
traces_sampler=traces_sampler,
|
||||
release=f"authentik@{authentik_version()}",
|
||||
release=f"authentik@{__version__}",
|
||||
transport=SentryTransport,
|
||||
**kwargs,
|
||||
)
|
||||
set_tag("authentik.build_hash", authentik_build_hash("tagged"))
|
||||
set_tag("authentik.build_hash", get_build_hash("tagged"))
|
||||
set_tag("authentik.env", get_env())
|
||||
set_tag("authentik.component", "backend")
|
||||
|
||||
|
||||
@@ -16,18 +16,8 @@ def register_signals(
|
||||
"""Register sync signals"""
|
||||
uid = class_to_path(provider_type)
|
||||
|
||||
def model_post_save(
|
||||
sender: type[Model],
|
||||
instance: User | Group,
|
||||
created: bool,
|
||||
update_fields: list[str] | None = None,
|
||||
**_,
|
||||
):
|
||||
def model_post_save(sender: type[Model], instance: User | Group, created: bool, **_):
|
||||
"""Post save handler"""
|
||||
# Special case for user object; don't start sync task when we've only updated `last_login`
|
||||
# This primarily happens during user login
|
||||
if sender == User and update_fields == {"last_login"}:
|
||||
return
|
||||
task_sync_direct_dispatch.send(
|
||||
class_to_path(instance.__class__),
|
||||
instance.pk,
|
||||
|
||||
@@ -5,7 +5,7 @@ from uuid import uuid4
|
||||
from requests.sessions import PreparedRequest, Session
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik import get_full_version
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
LOGGER = get_logger()
|
||||
@@ -13,7 +13,7 @@ LOGGER = get_logger()
|
||||
|
||||
def authentik_user_agent() -> str:
|
||||
"""Get a common user agent"""
|
||||
return f"authentik@{authentik_full_version()}"
|
||||
return f"authentik@{get_full_version()}"
|
||||
|
||||
|
||||
class TimeoutSession(Session):
|
||||
|
||||
@@ -13,7 +13,7 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik import authentik_build_hash
|
||||
from authentik import get_build_hash
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
|
||||
@@ -194,7 +194,7 @@ class OutpostViewSet(UsedByMixin, ModelViewSet):
|
||||
"openssl_version": state.openssl_version,
|
||||
"fips_enabled": state.fips_enabled,
|
||||
"hostname": state.hostname,
|
||||
"build_hash_should": authentik_build_hash(),
|
||||
"build_hash_should": get_build_hash(),
|
||||
}
|
||||
)
|
||||
return Response(OutpostHealthSerializer(states, many=True).data)
|
||||
|
||||
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
||||
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import authentik_build_hash, authentik_version
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.events.logs import LogEvent, capture_logs
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
@@ -99,6 +99,6 @@ class BaseController:
|
||||
image_name_template: str = CONFIG.get("outposts.container_image_base")
|
||||
return image_name_template % {
|
||||
"type": self.outpost.type,
|
||||
"version": authentik_version(),
|
||||
"build_hash": authentik_build_hash(),
|
||||
"version": __version__,
|
||||
"build_hash": get_build_hash(),
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ from paramiko.ssh_exception import SSHException
|
||||
from structlog.stdlib import get_logger
|
||||
from yaml import safe_dump
|
||||
|
||||
from authentik import authentik_version
|
||||
from authentik import __version__
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
||||
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
|
||||
@@ -185,7 +185,7 @@ class DockerController(BaseController):
|
||||
try:
|
||||
self.client.images.pull(image)
|
||||
except DockerException: # pragma: no cover
|
||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:{authentik_version()}"
|
||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}"
|
||||
self.client.images.pull(image)
|
||||
return image
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from requests import Response
|
||||
from structlog.stdlib import get_logger
|
||||
from urllib3.exceptions import HTTPError
|
||||
|
||||
from authentik import authentik_version
|
||||
from authentik import __version__
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.controllers.base import ControllerException
|
||||
from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
|
||||
@@ -29,8 +29,8 @@ T = TypeVar("T", V1Pod, V1Deployment)
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
"""Wrapper for authentik_version() to make testing easier"""
|
||||
return authentik_version()
|
||||
"""Wrapper for __version__ to make testing easier"""
|
||||
return __version__
|
||||
|
||||
|
||||
class KubernetesObjectReconciler(Generic[T]):
|
||||
|
||||
@@ -23,7 +23,7 @@ from kubernetes.client import (
|
||||
V1SecurityContext,
|
||||
)
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik import get_full_version
|
||||
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
|
||||
@@ -94,7 +94,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
meta = self.get_object_meta(name=self.name)
|
||||
image_name = self.controller.get_container_image()
|
||||
image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets
|
||||
version = authentik_full_version().replace("+", "-")
|
||||
version = get_full_version().replace("+", "-")
|
||||
return V1Deployment(
|
||||
metadata=meta,
|
||||
spec=V1DeploymentSpec(
|
||||
|
||||
@@ -19,7 +19,7 @@ from packaging.version import Version, parse
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import authentik_build_hash, authentik_version
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.blueprints.models import ManagedModel
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import (
|
||||
@@ -40,7 +40,7 @@ from authentik.outposts.controllers.k8s.utils import get_namespace
|
||||
from authentik.tasks.schedules.common import ScheduleSpec
|
||||
from authentik.tasks.schedules.models import ScheduledModel
|
||||
|
||||
OUR_VERSION = parse(authentik_version())
|
||||
OUR_VERSION = parse(__version__)
|
||||
OUTPOST_HELLO_INTERVAL = 10
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -481,7 +481,7 @@ class OutpostState:
|
||||
"""Check if outpost version matches our version"""
|
||||
if not self.version:
|
||||
return False
|
||||
if self.build_hash != authentik_build_hash():
|
||||
if self.build_hash != get_build_hash():
|
||||
return False
|
||||
return parse(self.version) != OUR_VERSION
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from channels.routing import URLRouter
|
||||
from channels.testing import WebsocketCommunicator
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from authentik import authentik_version
|
||||
from authentik import __version__
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.outposts.consumer import WebsocketMessage, WebsocketMessageInstruction
|
||||
from authentik.outposts.models import Outpost, OutpostType
|
||||
@@ -65,7 +65,7 @@ class TestOutpostWS(TransactionTestCase):
|
||||
WebsocketMessage(
|
||||
instruction=WebsocketMessageInstruction.HELLO,
|
||||
args={
|
||||
"version": authentik_version(),
|
||||
"version": __version__,
|
||||
"buildHash": "foo",
|
||||
"uuid": "123",
|
||||
},
|
||||
|
||||
@@ -7,7 +7,6 @@ For example: The 'dummy' policy is available at `authentik.policies.dummy`.
|
||||
from prometheus_client import Gauge, Histogram
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.tenants.flags import Flag
|
||||
|
||||
GAUGE_POLICIES_CACHED = Gauge(
|
||||
"authentik_policies_cached",
|
||||
@@ -33,12 +32,6 @@ HIST_POLICIES_EXECUTION_TIME = Histogram(
|
||||
)
|
||||
|
||||
|
||||
class BufferedPolicyAccessViewFlag(Flag[bool], key="policies_buffered_access_view"):
|
||||
|
||||
default = False
|
||||
visibility = "public"
|
||||
|
||||
|
||||
class AuthentikPoliciesConfig(ManagedAppConfig):
|
||||
"""authentik policies app config"""
|
||||
|
||||
@@ -46,4 +39,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
|
||||
label = "authentik_policies"
|
||||
verbose_name = "authentik Policies"
|
||||
default = True
|
||||
mountpoint = "policy/"
|
||||
|
||||
@@ -103,7 +103,7 @@ class PasswordPolicy(Policy):
|
||||
if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase:
|
||||
LOGGER.debug("password failed", check="static", reason="amount_lowercase")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_uppercase:
|
||||
if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_lowercase:
|
||||
LOGGER.debug("password failed", check="static", reason="amount_uppercase")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_symbols > 0:
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
let redirecting = false;
|
||||
|
||||
async function checkAuth() {
|
||||
if (redirecting) {
|
||||
console.debug(
|
||||
"authentik/policies/buffer: Already authenticating in another tab. This page will refresh once authentication is completed.",
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const url = "{{ check_auth_url }}";
|
||||
console.debug("authentik/policies/buffer: Checking authentication...");
|
||||
|
||||
return fetch(url, {
|
||||
method: "HEAD",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status >= 400) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.debug("authentik/policies/buffer: Continuing");
|
||||
|
||||
if ("{{ auth_req_method }}" === "post") {
|
||||
document.querySelector("form")?.submit();
|
||||
return true;
|
||||
}
|
||||
|
||||
window.location.assign("{{ continue_url|escapejs }}");
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("authentik/policies/buffer: Error checking authentication.", error);
|
||||
return false;
|
||||
})
|
||||
}
|
||||
|
||||
const offset = 20;
|
||||
|
||||
let timeoutID = -1;
|
||||
let timeout = 100;
|
||||
let attempts = 0;
|
||||
|
||||
async function main() {
|
||||
window.clearTimeout(timeoutID);
|
||||
|
||||
attempts += 1;
|
||||
|
||||
redirecting = await checkAuth();
|
||||
|
||||
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
|
||||
|
||||
timeoutID = window.setTimeout(main, timeout);
|
||||
|
||||
timeout += offset * attempts;
|
||||
|
||||
if (timeout >= 2000) {
|
||||
timeout = 2000;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("visibilitychange", async () => {
|
||||
if (document.hidden) return;
|
||||
|
||||
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
|
||||
|
||||
redirecting = await checkAuth();
|
||||
});
|
||||
|
||||
main();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
{% trans 'Waiting for authentication...' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
|
||||
{% if auth_req_method == "post" %}
|
||||
{% for key, value in auth_req_body.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<div class="pf-c-empty-state__icon">
|
||||
<span class="pf-c-spinner pf-m-xl" role="progressbar">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
|
||||
{% trans "Authenticate in this tab" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,125 +0,0 @@
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http import HttpResponse
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import Application, Provider
|
||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.policies.apps import BufferedPolicyAccessViewFlag
|
||||
from authentik.policies.views import (
|
||||
QS_BUFFER_ID,
|
||||
SESSION_KEY_BUFFER,
|
||||
BufferedPolicyAccessView,
|
||||
BufferView,
|
||||
PolicyAccessView,
|
||||
)
|
||||
from authentik.tenants.flags import patch_flag
|
||||
|
||||
|
||||
class TestPolicyViews(TestCase):
|
||||
"""Test PolicyAccessView"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_test_user()
|
||||
|
||||
def test_pav(self):
|
||||
"""Test simple policy access view"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
|
||||
class TestView(PolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/")
|
||||
req.user = self.user
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.content, b"foo")
|
||||
|
||||
@patch_flag(BufferedPolicyAccessViewFlag, True)
|
||||
def test_pav_buffer(self):
|
||||
"""Test simple policy access view"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
|
||||
class TestView(BufferedPolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/")
|
||||
req.user = AnonymousUser()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(req)
|
||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
||||
req.session.save()
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
|
||||
|
||||
@patch_flag(BufferedPolicyAccessViewFlag, True)
|
||||
def test_pav_buffer_skip(self):
|
||||
"""Test simple policy access view (skip buffer)"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
|
||||
class TestView(BufferedPolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/?skip_buffer=true")
|
||||
req.user = AnonymousUser()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(req)
|
||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
||||
req.session.save()
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication")))
|
||||
|
||||
def test_buffer(self):
|
||||
"""Test buffer view"""
|
||||
uid = generate_id()
|
||||
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
|
||||
req.user = AnonymousUser()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(req)
|
||||
ts = generate_id()
|
||||
req.session[SESSION_KEY_BUFFER % uid] = {
|
||||
"method": "get",
|
||||
"body": {},
|
||||
"url": f"/{ts}",
|
||||
}
|
||||
req.session.save()
|
||||
|
||||
res = BufferView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn(ts, res.render().content.decode())
|
||||
@@ -1,14 +1,7 @@
|
||||
"""API URLs"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from authentik.policies.api.bindings import PolicyBindingViewSet
|
||||
from authentik.policies.api.policies import PolicyViewSet
|
||||
from authentik.policies.views import BufferView
|
||||
|
||||
urlpatterns = [
|
||||
path("buffer", BufferView.as_view(), name="buffer"),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
("policies/all", PolicyViewSet),
|
||||
|
||||
@@ -1,37 +1,23 @@
|
||||
"""authentik access helper classes"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.http import HttpRequest, HttpResponse, QueryDict
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import TemplateView, View
|
||||
from django.views.generic.base import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Application, Provider, User
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views.executor import (
|
||||
SESSION_KEY_APPLICATION_PRE,
|
||||
SESSION_KEY_PLAN,
|
||||
SESSION_KEY_POST,
|
||||
)
|
||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.policies.apps import BufferedPolicyAccessViewFlag
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
QS_BUFFER_ID = "af_bf_id"
|
||||
QS_SKIP_BUFFER = "skip_buffer"
|
||||
SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s"
|
||||
|
||||
|
||||
class RequestValidationError(SentryIgnoredException):
|
||||
@@ -139,66 +125,3 @@ class PolicyAccessView(AccessMixin, View):
|
||||
for message in result.messages:
|
||||
messages.error(self.request, _(message))
|
||||
return result
|
||||
|
||||
|
||||
def url_with_qs(url: str, **kwargs):
|
||||
"""Update/set querystring of `url` with the parameters in `kwargs`. Original query string
|
||||
parameters are retained"""
|
||||
if "?" not in url:
|
||||
return url + f"?{urlencode(kwargs)}"
|
||||
url, _, qs = url.partition("?")
|
||||
qs = QueryDict(qs, mutable=True)
|
||||
qs.update(kwargs)
|
||||
return url + f"?{urlencode(qs.items())}"
|
||||
|
||||
|
||||
class BufferView(TemplateView):
|
||||
"""Buffer view"""
|
||||
|
||||
template_name = "policies/buffer.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
buf_id = self.request.GET.get(QS_BUFFER_ID)
|
||||
buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id)
|
||||
kwargs["auth_req_method"] = buffer["method"]
|
||||
kwargs["auth_req_body"] = buffer["body"]
|
||||
kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True})
|
||||
kwargs["check_auth_url"] = reverse("authentik_api:user-me")
|
||||
kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id})
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class BufferedPolicyAccessView(PolicyAccessView):
|
||||
"""PolicyAccessView which buffers access requests in case the user is not logged in"""
|
||||
|
||||
def handle_no_permission(self):
|
||||
plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN)
|
||||
if plan:
|
||||
flow = Flow.objects.filter(pk=plan.flow_pk).first()
|
||||
if not flow or flow.designation != FlowDesignation.AUTHENTICATION:
|
||||
LOGGER.debug("Not buffering request, no flow or flow not for authentication")
|
||||
return super().handle_no_permission()
|
||||
if not plan:
|
||||
LOGGER.debug("Not buffering request, no flow plan active")
|
||||
return super().handle_no_permission()
|
||||
if not BufferedPolicyAccessViewFlag().get():
|
||||
return super().handle_no_permission()
|
||||
if self.request.GET.get(QS_SKIP_BUFFER):
|
||||
LOGGER.debug("Not buffering request, explicit skip")
|
||||
return super().handle_no_permission()
|
||||
buffer_id = str(uuid4())
|
||||
LOGGER.debug("Buffering access request", bf_id=buffer_id)
|
||||
self.request.session[SESSION_KEY_BUFFER % buffer_id] = {
|
||||
"body": self.request.POST,
|
||||
"url": self.request.build_absolute_uri(self.request.get_full_path()),
|
||||
"method": self.request.method.lower(),
|
||||
}
|
||||
return redirect(
|
||||
url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id})
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
if QS_BUFFER_ID in self.request.GET:
|
||||
self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None)
|
||||
return response
|
||||
|
||||
@@ -70,7 +70,6 @@ class OAuth2ProviderSerializer(ProviderSerializer):
|
||||
"signing_key",
|
||||
"encryption_key",
|
||||
"redirect_uris",
|
||||
"backchannel_logout_uri",
|
||||
"sub_mode",
|
||||
"property_mappings",
|
||||
"issuer_mode",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""OAuth/OpenID Constants"""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"
|
||||
GRANT_TYPE_IMPLICIT = "implicit"
|
||||
GRANT_TYPE_REFRESH_TOKEN = "refresh_token" # nosec
|
||||
@@ -54,23 +51,3 @@ AMR_MFA = "mfa"
|
||||
AMR_OTP = "otp"
|
||||
AMR_WEBAUTHN = "user"
|
||||
AMR_SMART_CARD = "sc"
|
||||
|
||||
|
||||
class SubModes(models.TextChoices):
|
||||
"""Mode after which 'sub' attribute is generated, for compatibility reasons"""
|
||||
|
||||
HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID")
|
||||
USER_ID = "user_id", _("Based on user ID")
|
||||
USER_UUID = "user_uuid", _("Based on user UUID")
|
||||
USER_USERNAME = "user_username", _("Based on the username")
|
||||
USER_EMAIL = (
|
||||
"user_email",
|
||||
_("Based on the User's Email. This is recommended over the UPN method."),
|
||||
)
|
||||
USER_UPN = (
|
||||
"user_upn",
|
||||
_(
|
||||
"Based on the User's UPN, only works if user has a 'upn' attribute set. "
|
||||
"Use this method only if you have different UPN and Mail domains."
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,8 +4,10 @@ from dataclasses import asdict, dataclass, field
|
||||
from hashlib import sha256
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.core.models import default_token_duration
|
||||
from authentik.events.signals import get_login_event
|
||||
@@ -16,7 +18,6 @@ from authentik.providers.oauth2.constants import (
|
||||
AMR_PASSWORD,
|
||||
AMR_SMART_CARD,
|
||||
AMR_WEBAUTHN,
|
||||
SubModes,
|
||||
)
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
@@ -29,6 +30,26 @@ def hash_session_key(session_key: str) -> str:
|
||||
return sha256(session_key.encode("ascii")).hexdigest()
|
||||
|
||||
|
||||
class SubModes(models.TextChoices):
|
||||
"""Mode after which 'sub' attribute is generated, for compatibility reasons"""
|
||||
|
||||
HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID")
|
||||
USER_ID = "user_id", _("Based on user ID")
|
||||
USER_UUID = "user_uuid", _("Based on user UUID")
|
||||
USER_USERNAME = "user_username", _("Based on the username")
|
||||
USER_EMAIL = (
|
||||
"user_email",
|
||||
_("Based on the User's Email. This is recommended over the UPN method."),
|
||||
)
|
||||
USER_UPN = (
|
||||
"user_upn",
|
||||
_(
|
||||
"Based on the User's UPN, only works if user has a 'upn' attribute set. "
|
||||
"Use this method only if you have different UPN and Mail domains."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class IDToken:
|
||||
"""The primary extension that OpenID Connect makes to OAuth 2.0 to enable End-Users to be
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 5.1.11 on 2025-07-04 03:23
|
||||
|
||||
import authentik.lib.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_oauth2", "0028_migrate_session"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="oauth2provider",
|
||||
name="backchannel_logout_uri",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
||||
verbose_name="Back-Channel Logout URI",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="oauth2provider",
|
||||
name="_redirect_uris",
|
||||
field=models.JSONField(default=list, verbose_name="Redirect URIs"),
|
||||
),
|
||||
]
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
from dataclasses import asdict, dataclass
|
||||
from functools import cached_property
|
||||
from hashlib import sha256
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
@@ -42,14 +42,11 @@ from authentik.core.models import (
|
||||
)
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key
|
||||
from authentik.lib.models import DomainlessURLValidator, SerializerModel
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.providers.oauth2.constants import SubModes
|
||||
from authentik.providers.oauth2.id_token import IDToken, SubModes
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@@ -196,14 +193,9 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
||||
default=generate_client_secret,
|
||||
)
|
||||
_redirect_uris = models.JSONField(
|
||||
default=list,
|
||||
default=dict,
|
||||
verbose_name=_("Redirect URIs"),
|
||||
)
|
||||
backchannel_logout_uri = models.TextField(
|
||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
||||
verbose_name=_("Back-Channel Logout URI"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
include_claims_in_id_token = models.BooleanField(
|
||||
default=True,
|
||||
@@ -488,15 +480,13 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
|
||||
return f"Access Token for {self.provider_id} for user {self.user_id}"
|
||||
|
||||
@property
|
||||
def id_token(self) -> "IDToken":
|
||||
def id_token(self) -> IDToken:
|
||||
"""Load ID Token from json"""
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
|
||||
raw_token = json.loads(self._id_token)
|
||||
return from_dict(IDToken, raw_token)
|
||||
|
||||
@id_token.setter
|
||||
def id_token(self, value: "IDToken"):
|
||||
def id_token(self, value: IDToken):
|
||||
self.token = value.to_access_token(self.provider)
|
||||
self._id_token = json.dumps(asdict(value))
|
||||
|
||||
@@ -541,15 +531,13 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
|
||||
return f"Refresh Token for {self.provider_id} for user {self.user_id}"
|
||||
|
||||
@property
|
||||
def id_token(self) -> "IDToken":
|
||||
def id_token(self) -> IDToken:
|
||||
"""Load ID Token from json"""
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
|
||||
raw_token = json.loads(self._id_token)
|
||||
return from_dict(IDToken, raw_token)
|
||||
|
||||
@id_token.setter
|
||||
def id_token(self, value: "IDToken"):
|
||||
def id_token(self, value: IDToken):
|
||||
self._id_token = json.dumps(asdict(value))
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, User
|
||||
from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken
|
||||
from authentik.providers.oauth2.tasks import backchannel_logout_notification_dispatch
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||
def user_session_deleted_oauth_backchannel_logout_and_tokens_removal(
|
||||
sender, instance: AuthenticatedSession, **_
|
||||
):
|
||||
def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_):
|
||||
"""Revoke tokens upon user logout"""
|
||||
LOGGER.debug("Sending back-channel logout notifications signal!", session=instance)
|
||||
|
||||
access_tokens = AccessToken.objects.filter(
|
||||
AccessToken.objects.filter(
|
||||
user=instance.user,
|
||||
session__session__session_key=instance.session.session_key,
|
||||
)
|
||||
|
||||
backchannel_logout_notification_dispatch.send(
|
||||
revocations=[
|
||||
(token.provider_id, token.id_token.iss, token.session.user.uid)
|
||||
for token in access_tokens
|
||||
],
|
||||
)
|
||||
|
||||
access_tokens.delete()
|
||||
).delete()
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
"""OAuth2 Provider Tasks"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_dramatiq_postgres.middleware import CurrentTask
|
||||
from dramatiq.actor import actor
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
from authentik.providers.oauth2.utils import create_logout_token
|
||||
from authentik.tasks.models import Task
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@actor(description=_("Send a back-channel logout request to the registered client"))
|
||||
def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None) -> bool:
|
||||
"""Send a back-channel logout request to the registered client
|
||||
|
||||
Args:
|
||||
provider_pk: The OAuth2 provider's primary key
|
||||
iss: The issuer URL for the logout token
|
||||
sub: The subject identifier to include in the logout token
|
||||
|
||||
Returns:
|
||||
bool: True if the request was sent successfully, False otherwise
|
||||
"""
|
||||
self: Task = CurrentTask.get_task()
|
||||
LOGGER.debug("Sending back-channel logout request", provider_pk=provider_pk, sub=sub)
|
||||
|
||||
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
|
||||
if provider is None:
|
||||
return
|
||||
|
||||
# Generate the logout token
|
||||
logout_token = create_logout_token(iss, provider, None, sub)
|
||||
|
||||
# Get the back-channel logout URI from the provider's dedicated backchannel_logout_uri field
|
||||
# Back-channel logout requires explicit configuration - no fallback to redirect URIs
|
||||
|
||||
backchannel_logout_uri = provider.backchannel_logout_uri
|
||||
if not backchannel_logout_uri:
|
||||
self.info("No back-channel logout URI found for provider")
|
||||
return
|
||||
|
||||
# Send the back-channel logout request
|
||||
response = get_http_session().post(
|
||||
backchannel_logout_uri,
|
||||
data={"logout_token": logout_token},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
allow_redirects=True,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
self.info("Back-channel logout successful", sub=sub)
|
||||
return True
|
||||
|
||||
|
||||
@actor(description=_("Handle backchannel logout notifications dispatched via signal"))
|
||||
def backchannel_logout_notification_dispatch(revocations: list, **kwargs):
|
||||
"""Handle backchannel logout notifications dispatched via signal"""
|
||||
for revocation in revocations:
|
||||
provider_pk, iss, sub = revocation
|
||||
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
|
||||
send_backchannel_logout_request.send_with_options(
|
||||
args=(provider_pk, iss, sub),
|
||||
rel_obj=provider,
|
||||
)
|
||||
@@ -81,46 +81,4 @@ class TestAPI(APITestCase):
|
||||
},
|
||||
)
|
||||
self.assertJSONEqual(response.content, {"redirect_uris": ["Invalid Regex Pattern: **"]})
|
||||
|
||||
def test_backchannel_logout_uri_validation(self):
|
||||
"""Test backchannel_logout_uri API validation"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:oauth2provider-list"),
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"authorization_flow": create_test_flow().pk,
|
||||
"invalidation_flow": create_test_flow().pk,
|
||||
"redirect_uris": [
|
||||
{"matching_mode": "strict", "url": "http://goauthentik.io"},
|
||||
],
|
||||
"backchannel_logout_uri": "invalid-url",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_backchannel_logout_uri_create_and_retrieve(self):
|
||||
"""Test creating and retrieving backchannel logout URI"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:oauth2provider-list"),
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"authorization_flow": create_test_flow().pk,
|
||||
"invalidation_flow": create_test_flow().pk,
|
||||
"redirect_uris": [
|
||||
{"matching_mode": "strict", "url": "http://goauthentik.io"},
|
||||
],
|
||||
"backchannel_logout_uri": "http://goauthentik.io/logout",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
provider_data = response.json()
|
||||
self.assertEqual(provider_data["backchannel_logout_uri"], "http://goauthentik.io/logout")
|
||||
|
||||
# Test retrieving the provider
|
||||
provider_pk = provider_data["pk"]
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:oauth2provider-detail", kwargs={"pk": provider_pk})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
retrieved_data = response.json()
|
||||
self.assertEqual(retrieved_data["backchannel_logout_uri"], "http://goauthentik.io/logout")
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
"""Test OAuth2 Back-Channel Logout implementation"""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import jwt
|
||||
from django.test import RequestFactory
|
||||
from django.utils import timezone
|
||||
from dramatiq.results.errors import ResultFailure
|
||||
from requests import Response
|
||||
from requests.exceptions import HTTPError, Timeout
|
||||
|
||||
from authentik.core.models import Application, AuthenticatedSession, Session
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.id_token import hash_session_key
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
RefreshToken,
|
||||
)
|
||||
from authentik.providers.oauth2.tasks import send_backchannel_logout_request
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
from authentik.providers.oauth2.utils import create_logout_token
|
||||
|
||||
|
||||
class TestBackChannelLogout(OAuthTestCase):
|
||||
"""Test Back-Channel Logout functionality"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_test_admin_user()
|
||||
self.app = Application.objects.create(name=generate_id(), slug="test-app")
|
||||
self.provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver/callback"),
|
||||
],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
self.app.provider = self.provider
|
||||
self.app.save()
|
||||
|
||||
def _create_session(self, session_key=None):
|
||||
"""Create a session with the given key or a generated one"""
|
||||
session_key = session_key or f"session-{generate_id()}"
|
||||
session = Session.objects.create(
|
||||
session_key=session_key,
|
||||
expires=timezone.now() + timezone.timedelta(hours=1),
|
||||
last_ip="255.255.255.255",
|
||||
)
|
||||
auth_session = AuthenticatedSession.objects.create(
|
||||
session=session,
|
||||
user=self.user,
|
||||
)
|
||||
return auth_session
|
||||
|
||||
def _create_token(
|
||||
self, provider, user, session=None, token_type="access", token_id=None
|
||||
): # nosec
|
||||
"""Create a token of the specified type"""
|
||||
token_id = token_id or f"{token_type}-token-{generate_id()}"
|
||||
kwargs = {
|
||||
"provider": provider,
|
||||
"user": user,
|
||||
"session": session,
|
||||
"token": token_id,
|
||||
"_id_token": "{}",
|
||||
"auth_time": timezone.now(),
|
||||
}
|
||||
|
||||
if token_type == "access": # nosec
|
||||
return AccessToken.objects.create(**kwargs)
|
||||
else: # refresh
|
||||
return RefreshToken.objects.create(**kwargs)
|
||||
|
||||
def _create_provider(self, name=None):
|
||||
"""Create an OAuth2 provider"""
|
||||
name = name or f"provider-{generate_id()}"
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=name,
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, f"http://{name}/callback"),
|
||||
],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
return provider
|
||||
|
||||
def _create_logout_token(
|
||||
self,
|
||||
provider: OAuth2Provider | None = None,
|
||||
session_id: str | None = None,
|
||||
sub: str | None = None,
|
||||
):
|
||||
"""Create a logout token with the given parameters"""
|
||||
provider = provider or self.provider
|
||||
|
||||
# Create a token with the same issuer that the view will expect
|
||||
# Use the same request object that will be used in the test
|
||||
request = self.factory.post("/backchannel_logout")
|
||||
|
||||
return create_logout_token(
|
||||
iss=provider.get_issuer(request),
|
||||
provider=provider,
|
||||
session_key=session_id,
|
||||
sub=sub,
|
||||
)
|
||||
|
||||
def _decode_token(self, token, provider=None):
|
||||
"""Helper to decode and validate a JWT token"""
|
||||
provider = provider or self.provider
|
||||
key, alg = provider.jwt_key
|
||||
if alg != "HS256":
|
||||
key = provider.signing_key.public_key
|
||||
return jwt.decode(
|
||||
token, key, algorithms=[alg], options={"verify_exp": False, "verify_aud": False}
|
||||
)
|
||||
|
||||
def test_create_logout_token_variants(self):
|
||||
"""Test creating logout tokens with different combinations of parameters"""
|
||||
# Test case 1: With session_id only
|
||||
session_id = "test-session-123"
|
||||
token1 = self._create_logout_token(session_id=session_id)
|
||||
decoded1 = self._decode_token(token1)
|
||||
|
||||
self.assertIn("iss", decoded1)
|
||||
self.assertEqual(decoded1["aud"], self.provider.client_id)
|
||||
self.assertIn("iat", decoded1)
|
||||
self.assertIn("jti", decoded1)
|
||||
self.assertEqual(decoded1["sid"], hash_session_key(session_id))
|
||||
self.assertIn("events", decoded1)
|
||||
self.assertIn("http://schemas.openid.net/event/backchannel-logout", decoded1["events"])
|
||||
self.assertNotIn("sub", decoded1)
|
||||
|
||||
# Test case 2: With sub only
|
||||
sub = "user-123"
|
||||
token2 = self._create_logout_token(sub=sub)
|
||||
decoded2 = self._decode_token(token2)
|
||||
|
||||
self.assertEqual(decoded2["sub"], sub)
|
||||
self.assertIn("events", decoded2)
|
||||
self.assertIn("http://schemas.openid.net/event/backchannel-logout", decoded2["events"])
|
||||
self.assertNotIn("sid", decoded2)
|
||||
|
||||
# Test case 3: With both session_id and sub
|
||||
token3 = self._create_logout_token(session_id=session_id, sub=sub)
|
||||
decoded3 = self._decode_token(token3)
|
||||
|
||||
self.assertEqual(decoded3["sid"], hash_session_key(session_id))
|
||||
self.assertEqual(decoded3["sub"], sub)
|
||||
self.assertIn("events", decoded3)
|
||||
|
||||
@patch("authentik.providers.oauth2.tasks.get_http_session")
|
||||
def test_send_backchannel_logout_request_scenarios(self, mock_get_session):
|
||||
"""Test various scenarios for backchannel logout request task"""
|
||||
# Setup provider with backchannel logout URI
|
||||
self.provider.backchannel_logout_uri = "http://testserver/backchannel_logout"
|
||||
self.provider.save()
|
||||
|
||||
# Setup mock session and response
|
||||
mock_session = Mock()
|
||||
mock_get_session.return_value = mock_session
|
||||
mock_response = Mock(spec=Response)
|
||||
mock_response.status_code = 200
|
||||
mock_response.raise_for_status.return_value = None # No exception for successful request
|
||||
mock_session.post.return_value = mock_response
|
||||
|
||||
result = send_backchannel_logout_request.send(
|
||||
self.provider.pk, "http://testserver", sub="test-user-uid"
|
||||
)
|
||||
self.assertTrue(result)
|
||||
mock_session.post.assert_called_once()
|
||||
call_args = mock_session.post.call_args
|
||||
self.assertIn("logout_token", call_args[1]["data"])
|
||||
self.assertEqual(
|
||||
call_args[1]["headers"]["Content-Type"], "application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
# Scenario 2: Failed request (400 response) - should raise exception
|
||||
mock_session.post.reset_mock()
|
||||
error_response = Mock(spec=Response)
|
||||
error_response.status_code = 400
|
||||
error_response.raise_for_status.side_effect = HTTPError("HTTP 400")
|
||||
mock_session.post.return_value = error_response
|
||||
with self.assertRaises(ResultFailure):
|
||||
send_backchannel_logout_request.send(
|
||||
self.provider.pk, "http://testserver", sub="test-user-uid"
|
||||
).get_result()
|
||||
|
||||
# Scenario 3: No URI configured
|
||||
mock_session.post.reset_mock()
|
||||
self.provider.backchannel_logout_uri = ""
|
||||
self.provider.save()
|
||||
result = send_backchannel_logout_request.send(
|
||||
self.provider.pk, "http://testserver", sub="test-user-uid"
|
||||
).get_result()
|
||||
self.assertIsNone(result)
|
||||
mock_session.post.assert_not_called()
|
||||
|
||||
# Scenario 4: No sub provided - should fail
|
||||
result = send_backchannel_logout_request.send(
|
||||
self.provider.pk, "http://testserver"
|
||||
).get_result()
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Scenario 5: Non-existent provider
|
||||
result = send_backchannel_logout_request.send(
|
||||
99999, "http://testserver", sub="test-user-uid"
|
||||
).get_result()
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Scenario 6: Request timeout
|
||||
mock_session.post.side_effect = Timeout("Request timed out")
|
||||
self.provider.backchannel_logout_uri = "http://testserver/backchannel_logout"
|
||||
self.provider.save()
|
||||
with self.assertRaises(ResultFailure):
|
||||
send_backchannel_logout_request.send(
|
||||
self.provider.pk, "http://testserver", sub="test-user-uid"
|
||||
).get_result()
|
||||
@@ -11,9 +11,9 @@ from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
IDToken,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
|
||||
@@ -10,11 +10,11 @@ from django.utils import timezone
|
||||
from authentik.core.models import Application, AuthenticatedSession, Session
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
ClientTypes,
|
||||
DeviceToken,
|
||||
IDToken,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
|
||||
@@ -11,9 +11,9 @@ from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
IDToken,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user