mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 07:32:23 +02:00
Compare commits
244 Commits
web/bundle
...
version-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49b46b5338 | ||
|
|
cfcdc70542 | ||
|
|
471f0d65e0 | ||
|
|
3c0731ab6d | ||
|
|
bce6ba2afa | ||
|
|
1b490c1b5a | ||
|
|
9a038aa0fe | ||
|
|
c80112e5f8 | ||
|
|
65ae736fe2 | ||
|
|
271e7eae1c | ||
|
|
8bf66b8987 | ||
|
|
6f9369283c | ||
|
|
859ef3a722 | ||
|
|
b47ed8894c | ||
|
|
8c1f08fecb | ||
|
|
cc676793b0 | ||
|
|
76815cd5b9 | ||
|
|
57a378bbd0 | ||
|
|
f542a0d415 | ||
|
|
f69bbbe85e | ||
|
|
0845e23337 | ||
|
|
de6d2a02b2 | ||
|
|
73be5321a9 | ||
|
|
ff8ad31a6b | ||
|
|
ad2cedac98 | ||
|
|
7a0bd0d0c1 | ||
|
|
269551cf4d | ||
|
|
23a6ab915b | ||
|
|
0c77e5c33e | ||
|
|
93aeae5405 | ||
|
|
cd3ff47221 | ||
|
|
a370a54db8 | ||
|
|
1d0e45abe8 | ||
|
|
a501b627eb | ||
|
|
78a4c08fc8 | ||
|
|
8d3a289d12 | ||
|
|
99e92bf998 | ||
|
|
1df7b22d29 | ||
|
|
af484738f6 | ||
|
|
19ba3c8453 | ||
|
|
9d1b384baa | ||
|
|
8a702b148f | ||
|
|
2db42a3a04 | ||
|
|
4403baaa28 | ||
|
|
8030d4d734 | ||
|
|
4b7a78453c | ||
|
|
b9746106a0 | ||
|
|
faa47670ef | ||
|
|
add5f4e0cb | ||
|
|
7636c038d9 | ||
|
|
557f781f5c | ||
|
|
1997011328 | ||
|
|
dfdd5ebc8c | ||
|
|
42977caa6a | ||
|
|
bce46c880d | ||
|
|
9c987c5694 | ||
|
|
f33e50c28c | ||
|
|
280d6febf7 | ||
|
|
3a32918ceb | ||
|
|
723bd4bbbc | ||
|
|
7b130e1832 | ||
|
|
3aa969d64e | ||
|
|
04688c86b3 | ||
|
|
4f8dbb5efb | ||
|
|
3b382199eb | ||
|
|
db50991e9c | ||
|
|
88036ebe14 | ||
|
|
680feaefa1 | ||
|
|
8676cd3a43 | ||
|
|
53e0f6b734 | ||
|
|
eaee475662 | ||
|
|
19b672b3bc | ||
|
|
bf0a31ce86 | ||
|
|
28ff561400 | ||
|
|
ff2472a551 | ||
|
|
dac302e8be | ||
|
|
930a6f7c6f | ||
|
|
b661b0bb39 | ||
|
|
5203713ca0 | ||
|
|
94794b106e | ||
|
|
eb8c21cf04 | ||
|
|
8a501377f2 | ||
|
|
5c67a0fecd | ||
|
|
145e6a3a4f | ||
|
|
0219ed73f5 | ||
|
|
88ccb7857b | ||
|
|
e33d6becba | ||
|
|
41417affc0 | ||
|
|
1b4a6c3f6d | ||
|
|
aa98edd661 | ||
|
|
cb70331c82 | ||
|
|
31e105b190 | ||
|
|
08d2615f71 | ||
|
|
bb9e8b1c42 | ||
|
|
6b8d7376a6 | ||
|
|
4f680d8c06 | ||
|
|
3eeb741975 | ||
|
|
39cb638132 | ||
|
|
be8f5d21cb | ||
|
|
acf18836e8 | ||
|
|
d688621de4 | ||
|
|
5173d09191 | ||
|
|
45bab4d32f | ||
|
|
f383e54c72 | ||
|
|
ab6b9b27cc | ||
|
|
db3fb0bf2e | ||
|
|
5859e6a5e5 | ||
|
|
1d3271fec7 | ||
|
|
673c8ef62c | ||
|
|
4614ae320f | ||
|
|
10b103c0bf | ||
|
|
361e64a8a1 | ||
|
|
e56081b863 | ||
|
|
01a44b281b | ||
|
|
7a6631c6e8 | ||
|
|
ada973dd44 | ||
|
|
3a7e962bde | ||
|
|
ae297e2f60 | ||
|
|
2bc2b6bd41 | ||
|
|
8199371172 | ||
|
|
dac1879de5 | ||
|
|
dd7c6b29d9 | ||
|
|
41b7e05f59 | ||
|
|
c5f5714e02 | ||
|
|
d20d8322af | ||
|
|
288f5d5015 | ||
|
|
a640eb9180 | ||
|
|
7d48baab3e | ||
|
|
b1cd6d34fc | ||
|
|
f14d033cef | ||
|
|
ec75e161e2 | ||
|
|
2e8fb8f2c6 | ||
|
|
362bf22139 | ||
|
|
890da9b287 | ||
|
|
7b8dadf945 | ||
|
|
8f70dbb963 | ||
|
|
15505f5caf | ||
|
|
b2d770c0a4 | ||
|
|
f7e25fff1a | ||
|
|
688b3a9f8b | ||
|
|
8f48e18854 | ||
|
|
306f75be59 | ||
|
|
982c3cf4dc | ||
|
|
156cda6cb6 | ||
|
|
9f5125cf6b | ||
|
|
a84411363e | ||
|
|
9f3bb0210b | ||
|
|
d1271502ef | ||
|
|
d1065b2d49 | ||
|
|
aa19227e30 | ||
|
|
d7a2861bbe | ||
|
|
5aae9c9afa | ||
|
|
93cb48c928 | ||
|
|
55f7f93a24 | ||
|
|
5a608a4235 | ||
|
|
77d023758f | ||
|
|
f9edafd374 | ||
|
|
d94219eb0e | ||
|
|
0871aa2cf3 | ||
|
|
e0fe99d0b8 | ||
|
|
c1acf53585 | ||
|
|
baf4eed0d9 | ||
|
|
60e1192a7a | ||
|
|
2608e02d6e | ||
|
|
0a5928fbcb | ||
|
|
7b99b02b4a | ||
|
|
dfeadc9ebe | ||
|
|
7ff64fbd09 | ||
|
|
c7679434e1 | ||
|
|
8bc58e24e9 | ||
|
|
2aa256d532 | ||
|
|
2f2bdd032d | ||
|
|
e36529614f | ||
|
|
883367263a | ||
|
|
96a47c69e0 | ||
|
|
3ae9721698 | ||
|
|
30a3ad402b | ||
|
|
4e500030d1 | ||
|
|
885f1cc018 | ||
|
|
a4c7e7ba2e | ||
|
|
a4dbd1dc93 | ||
|
|
1bc2daae98 | ||
|
|
a38239509b | ||
|
|
34bab28985 | ||
|
|
b68adec303 | ||
|
|
cf4dd24b6f | ||
|
|
ffe767fe13 | ||
|
|
c0732ec687 | ||
|
|
cf72074820 | ||
|
|
e154fee28a | ||
|
|
85ccd7acbc | ||
|
|
3f52706e3f | ||
|
|
662ac47332 | ||
|
|
b3e69c1665 | ||
|
|
b9a34afaf4 | ||
|
|
8ca78c5a3d | ||
|
|
9ac320a5d9 | ||
|
|
7ce1ac08d4 | ||
|
|
fc68221495 | ||
|
|
37066eef6c | ||
|
|
63e116f109 | ||
|
|
eb0611950c | ||
|
|
c03fe12406 | ||
|
|
72ac793065 | ||
|
|
43ed08c636 | ||
|
|
2534261954 | ||
|
|
41cd3305f9 | ||
|
|
cccbc473e5 | ||
|
|
13bdad33cb | ||
|
|
0fd53bb8cf | ||
|
|
e56ac8494f | ||
|
|
6d1d15d6e8 | ||
|
|
638dfb4d3c | ||
|
|
080588bb41 | ||
|
|
6700ed8b9d | ||
|
|
d2da2fb966 | ||
|
|
08ddd3e8d7 | ||
|
|
67d9534181 | ||
|
|
575cf63f5b | ||
|
|
1731d1e68d | ||
|
|
ad29786512 | ||
|
|
0e5d1c2983 | ||
|
|
a1174192ce | ||
|
|
01b299c970 | ||
|
|
16e9061468 | ||
|
|
0c66fe1a2c | ||
|
|
dff2b871e1 | ||
|
|
510816983b | ||
|
|
a936e16b68 | ||
|
|
6c8c9804ce | ||
|
|
988c339576 | ||
|
|
1380d04480 | ||
|
|
f06ebd7dc3 | ||
|
|
c35dff77fb | ||
|
|
28b0c9c87b | ||
|
|
589ee66fa1 | ||
|
|
b234a17e96 | ||
|
|
7d4ce2abfc | ||
|
|
aeb5c9a24f | ||
|
|
38e0a5794d | ||
|
|
fdf0416ea9 | ||
|
|
e771bb74ee | ||
|
|
50a22e685e | ||
|
|
e789818526 |
@@ -1,36 +0,0 @@
|
||||
[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,5 +1,6 @@
|
||||
htmlcov
|
||||
*.env.yml
|
||||
node_modules
|
||||
**/node_modules
|
||||
dist/**
|
||||
build/**
|
||||
|
||||
@@ -54,6 +54,10 @@ outputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
dependencies: "python"
|
||||
- name: Generate config
|
||||
id: ev
|
||||
shell: bash
|
||||
@@ -64,4 +68,4 @@ runs:
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
python3 ${{ github.action_path }}/push_vars.py
|
||||
uv run python3 ${{ github.action_path }}/push_vars.py
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"""Helper script to get the actual branch name, docker safe"""
|
||||
|
||||
import configparser
|
||||
import os
|
||||
from json import dumps
|
||||
from time import time
|
||||
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(".bumpversion.cfg")
|
||||
from authentik import authentik_version
|
||||
|
||||
# Decide if we should push the image or not
|
||||
should_push = True
|
||||
@@ -31,7 +29,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 = parser.get("bumpversion", "current_version")
|
||||
version = authentik_version()
|
||||
# 2042.1
|
||||
version_family = ".".join(version.split("-", 1)[0].split(".")[:-1])
|
||||
prerelease = "-" in version
|
||||
|
||||
13
.github/actions/setup/action.yml
vendored
13
.github/actions/setup/action.yml
vendored
@@ -2,6 +2,9 @@ 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"
|
||||
@@ -10,42 +13,52 @@ 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
|
||||
|
||||
@@ -42,14 +42,14 @@ jobs:
|
||||
# Needed for checkout
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- 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_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ${{ inputs.image_name }}
|
||||
image-arch: ${{ inputs.image_arch }}
|
||||
@@ -58,8 +58,8 @@ jobs:
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.registry_ghcr }}
|
||||
uses: docker/login-action@v3
|
||||
@@ -67,14 +67,20 @@ jobs:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: make empty clients
|
||||
if: ${{ inputs.release }}
|
||||
- name: Setup 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
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Generate API Clients
|
||||
run: |
|
||||
mkdir -p ./gen-ts-api
|
||||
mkdir -p ./gen-go-api
|
||||
- name: generate ts client
|
||||
if: ${{ !inputs.release }}
|
||||
run: make gen-client-ts
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
id: push
|
||||
|
||||
12
.github/workflows/_reusable-docker-build.yaml
vendored
12
.github/workflows/_reusable-docker-build.yaml
vendored
@@ -49,12 +49,12 @@ jobs:
|
||||
tags: ${{ steps.ev.outputs.imageTagsJSON }}
|
||||
shouldPush: ${{ steps.ev.outputs.shouldPush }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ${{ inputs.image_name }}
|
||||
merge-server:
|
||||
@@ -69,20 +69,20 @@ jobs:
|
||||
matrix:
|
||||
tag: ${{ fromJson(needs.get-tags.outputs.tags) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_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_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.registry_ghcr }}
|
||||
uses: docker/login-action@v3
|
||||
|
||||
2
.github/workflows/api-py-publish.yml
vendored
2
.github/workflows/api-py-publish.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Install poetry & deps
|
||||
|
||||
3
.github/workflows/api-ts-publish.yml
vendored
3
.github/workflows/api-ts-publish.yml
vendored
@@ -10,7 +10,6 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
@@ -18,7 +17,7 @@ jobs:
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
6
.github/workflows/ci-api-docs.yml
vendored
6
.github/workflows/ci-api-docs.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
command:
|
||||
- prettier-check
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Dependencies
|
||||
working-directory: website/
|
||||
run: npm ci
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
- lint
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: api-docs
|
||||
|
||||
2
.github/workflows/ci-aws-cfn.yml
vendored
2
.github/workflows/ci-aws-cfn.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
check-changes-applied:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
3
.github/workflows/ci-docs-source.yml
vendored
3
.github/workflows/ci-docs-source.yml
vendored
@@ -13,11 +13,10 @@ env:
|
||||
|
||||
jobs:
|
||||
publish-source-docs:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: generate docs
|
||||
|
||||
12
.github/workflows/ci-docs.yml
vendored
12
.github/workflows/ci-docs.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
command:
|
||||
- prettier-check
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install dependencies
|
||||
working-directory: website/
|
||||
run: npm ci
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
@@ -61,7 +61,6 @@ jobs:
|
||||
working-directory: website/
|
||||
run: npm run build -w integrations
|
||||
build-container:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload container images to ghcr.io
|
||||
@@ -70,7 +69,7 @@ jobs:
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
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_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/dev-docs
|
||||
- name: Login to Container Registry
|
||||
@@ -121,4 +120,3 @@ jobs:
|
||||
- uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
allowed-skips: ${{ github.repository == 'goauthentik/authentik-internal' && 'build-container' || '[]' }}
|
||||
|
||||
3
.github/workflows/ci-main-daily.yml
vendored
3
.github/workflows/ci-main-daily.yml
vendored
@@ -9,7 +9,6 @@ on:
|
||||
|
||||
jobs:
|
||||
test-container:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -19,7 +18,7 @@ jobs:
|
||||
- version-2025-4
|
||||
- version-2025-2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- run: |
|
||||
current="$(pwd)"
|
||||
dir="/tmp/authentik/${{ matrix.version }}"
|
||||
|
||||
26
.github/workflows/ci-main.yml
vendored
26
.github/workflows/ci-main.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
- ruff
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run job
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run migrations
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- 17-alpine
|
||||
run_id: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: checkout stable
|
||||
@@ -80,7 +80,15 @@ jobs:
|
||||
cp authentik/lib/default.yml local.env.yml
|
||||
cp -R .github ..
|
||||
cp -R scripts ..
|
||||
git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
|
||||
# Previous stable tag
|
||||
prev_stable=$(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
|
||||
# Current version family based on
|
||||
current_version_family=$(python -c "from authentik import VERSION; print(VERSION)" | grep -vE -- 'rc[0-9]+$')
|
||||
if [[ -n $current_version_family ]]; then
|
||||
prev_stable=$current_version_family
|
||||
fi
|
||||
echo "::notice::Checking out ${prev_stable} as stable version..."
|
||||
git checkout $(prev_stable)
|
||||
rm -rf .github/ scripts/
|
||||
mv ../.github ../scripts .
|
||||
- name: Setup authentik env (stable)
|
||||
@@ -126,7 +134,7 @@ jobs:
|
||||
- 17-alpine
|
||||
run_id: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -153,7 +161,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Create k8s Kind Cluster
|
||||
@@ -198,7 +206,7 @@ jobs:
|
||||
- name: flows
|
||||
glob: tests/e2e/test_flows*
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup e2e env (chrome, etc)
|
||||
@@ -271,14 +279,14 @@ jobs:
|
||||
pull-requests: write
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/dev-server
|
||||
- name: Comment on PR
|
||||
|
||||
11
.github/workflows/ci-outpost.yml
vendored
11
.github/workflows/ci-outpost.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
lint-golint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
@@ -59,7 +59,6 @@ jobs:
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
build-container:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
timeout-minutes: 120
|
||||
needs:
|
||||
- ci-outpost-mark
|
||||
@@ -79,7 +78,7 @@ jobs:
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
@@ -90,7 +89,7 @@ jobs:
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
|
||||
- name: Login to Container Registry
|
||||
@@ -138,7 +137,7 @@ jobs:
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-go@v5
|
||||
|
||||
6
.github/workflows/ci-web.yml
vendored
6
.github/workflows/ci-web.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- command: lit-analyse
|
||||
project: web
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ${{ matrix.project }}/package.json
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- ci-web-mark
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
|
||||
2
.github/workflows/gen-image-compress.yml
vendored
2
.github/workflows/gen-image-compress.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Compress images
|
||||
|
||||
@@ -13,7 +13,6 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
@@ -21,7 +20,7 @@ jobs:
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Setup authentik env
|
||||
|
||||
2
.github/workflows/gh-gha-cache-cleanup.yml
vendored
2
.github/workflows/gh-gha-cache-cleanup.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
|
||||
15
.github/workflows/gh-ghcr-retention.yml
vendored
15
.github/workflows/gh-ghcr-retention.yml
vendored
@@ -5,10 +5,13 @@ on:
|
||||
# schedule:
|
||||
# - cron: "0 0 * * *" # every day at midnight
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry-run:
|
||||
type: boolean
|
||||
description: Enable dry-run mode
|
||||
|
||||
jobs:
|
||||
clean-ghcr:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
name: Delete old unused container images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -18,12 +21,12 @@ jobs:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Delete 'dev' containers older than a week
|
||||
uses: snok/container-retention-policy@v2
|
||||
uses: snok/container-retention-policy@3b0972b2276b171b212f8c4efbca59ebba26eceb # v3.0.1
|
||||
with:
|
||||
image-names: dev-server,dev-ldap,dev-proxy
|
||||
image-tags: "!gh-next,!gh-main"
|
||||
cut-off: One week ago UTC
|
||||
account-type: org
|
||||
org-name: goauthentik
|
||||
untagged-only: false
|
||||
account: goauthentik
|
||||
tag-selection: untagged
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
skip-tags: gh-next,gh-main
|
||||
dry-run: ${{ inputs.dry-run }}
|
||||
|
||||
3
.github/workflows/packages-npm-publish.yml
vendored
3
.github/workflows/packages-npm-publish.yml
vendored
@@ -14,7 +14,6 @@ on:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -26,7 +25,7 @@ jobs:
|
||||
- packages/tsconfig
|
||||
- packages/esbuild-plugin-live-reload
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/qa-codeql.yml
vendored
2
.github/workflows/qa-codeql.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
language: ["go", "javascript", "python"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Initialize CodeQL
|
||||
|
||||
2
.github/workflows/qa-semgrep.yml
vendored
2
.github/workflows/qa-semgrep.yml
vendored
@@ -26,5 +26,5 @@ jobs:
|
||||
image: semgrep/semgrep
|
||||
if: (github.actor != 'dependabot[bot]')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- run: semgrep ci
|
||||
|
||||
83
.github/workflows/release-branch-off.yml
vendored
Normal file
83
.github/workflows/release-branch-off.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
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>
|
||||
3
.github/workflows/release-next-branch.yml
vendored
3
.github/workflows/release-next-branch.yml
vendored
@@ -12,11 +12,10 @@ permissions:
|
||||
|
||||
jobs:
|
||||
update-next:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: internal-production
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
- run: |
|
||||
|
||||
35
.github/workflows/release-publish.yml
vendored
35
.github/workflows/release-publish.yml
vendored
@@ -10,6 +10,7 @@ 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
|
||||
@@ -23,13 +24,14 @@ jobs:
|
||||
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@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.6.0
|
||||
- name: Set up Docker Buildx
|
||||
@@ -66,6 +68,7 @@ jobs:
|
||||
build-outpost:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed to upload container images to ghcr.io
|
||||
packages: write
|
||||
# Needed for attestation
|
||||
@@ -80,10 +83,15 @@ jobs:
|
||||
- radius
|
||||
- rac
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.6.0
|
||||
- name: Set up Docker Buildx
|
||||
@@ -95,10 +103,10 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/${{ matrix.type }},authentik/${{ matrix.type }}
|
||||
- name: make empty clients
|
||||
- name: Generate API Clients
|
||||
run: |
|
||||
mkdir -p ./gen-ts-api
|
||||
mkdir -p ./gen-go-api
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -143,7 +151,7 @@ jobs:
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
@@ -152,10 +160,17 @@ jobs:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Build web
|
||||
- name: Install web dependencies
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm ci
|
||||
- name: Generate API Clients
|
||||
run: |
|
||||
make gen-client-ts
|
||||
make gen-client-go
|
||||
- name: Build web
|
||||
working-directory: web/
|
||||
run: |
|
||||
npm run build-proxy
|
||||
- name: Build outpost
|
||||
run: |
|
||||
@@ -183,7 +198,7 @@ jobs:
|
||||
AWS_REGION: eu-central-1
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: "arn:aws:iam::016170277896:role/github_goauthentik_authentik"
|
||||
@@ -199,7 +214,7 @@ jobs:
|
||||
- build-outpost-binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
|
||||
@@ -215,7 +230,7 @@ jobs:
|
||||
- build-outpost-binary
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
|
||||
213
.github/workflows/release-tag.yml
vendored
213
.github/workflows/release-tag.yml
vendored
@@ -1,39 +1,202 @@
|
||||
---
|
||||
name: Release - On tag
|
||||
name: Release - Tag new version
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "version/*"
|
||||
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"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Create Release from Tag
|
||||
check-inputs:
|
||||
name: Check inputs validity
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Pre-release test
|
||||
- id: check
|
||||
run: |
|
||||
make test-docker
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
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
|
||||
needs:
|
||||
- check-inputs
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
ref: "version-${{ needs.check-inputs.outputs.major_version }}"
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- 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
|
||||
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"
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/server
|
||||
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 pull
|
||||
git commit -a -m "release: ${{ inputs.version }}" --allow-empty
|
||||
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
|
||||
git push --follow-tags
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1.1.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ steps.ev.outputs.version }}
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
tag_name: "version/${{ inputs.version }}"
|
||||
name: Release ${{ inputs.version }}
|
||||
draft: true
|
||||
prerelease: ${{ steps.ev.outputs.prerelease == '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>"
|
||||
|
||||
22
.github/workflows/repo-mirror-cleanup.yml
vendored
22
.github/workflows/repo-mirror-cleanup.yml
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Repo - Cleanup internal mirror
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
to_internal:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- if: ${{ env.MIRROR_KEY != '' }}
|
||||
uses: BeryJu/repository-mirroring-action@5cf300935bc2e068f73ea69bcc411a8a997208eb
|
||||
with:
|
||||
target_repo_url: git@github.com:goauthentik/authentik-internal.git
|
||||
ssh_private_key: ${{ secrets.GH_MIRROR_KEY }}
|
||||
args: --tags --force --prune
|
||||
env:
|
||||
MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }}
|
||||
21
.github/workflows/repo-mirror.yml
vendored
21
.github/workflows/repo-mirror.yml
vendored
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Repo - Mirror to internal
|
||||
|
||||
on: [push, delete]
|
||||
|
||||
jobs:
|
||||
to_internal:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- if: ${{ env.MIRROR_KEY != '' }}
|
||||
uses: BeryJu/repository-mirroring-action@5cf300935bc2e068f73ea69bcc411a8a997208eb
|
||||
with:
|
||||
target_repo_url: git@github.com:goauthentik/authentik-internal.git
|
||||
ssh_private_key: ${{ secrets.GH_MIRROR_KEY }}
|
||||
args: --tags --force
|
||||
env:
|
||||
MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }}
|
||||
1
.github/workflows/repo-stale.yml
vendored
1
.github/workflows/repo-stale.yml
vendored
@@ -12,7 +12,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
|
||||
@@ -17,7 +17,6 @@ env:
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
@@ -26,11 +25,11 @@ jobs:
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
2
.github/workflows/translation-rename.yml
vendored
2
.github/workflows/translation-rename.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
website/docs/developer-docs/index.md
|
||||
4
CONTRIBUTING.md
Normal file
4
CONTRIBUTING.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Contributing to authentik
|
||||
|
||||
Thanks for your interest in contributing! Please see our [contributing guide](https://docs.goauthentik.io/docs/developer-docs/?utm_source=github) for more information.
|
||||
|
||||
15
Dockerfile
15
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.24-bookworm AS go-builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25-bookworm AS go-builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -44,6 +44,7 @@ RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/v
|
||||
|
||||
RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
|
||||
--mount=type=bind,target=/go/src/goauthentik.io/go.sum,src=./go.sum \
|
||||
--mount=type=bind,target=/go/src/goauthentik.io/gen-go-api,src=./gen-go-api \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go mod download
|
||||
|
||||
@@ -57,6 +58,7 @@ COPY ./go.mod /go/src/goauthentik.io/go.mod
|
||||
COPY ./go.sum /go/src/goauthentik.io/go.sum
|
||||
|
||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
--mount=type=bind,target=/go/src/goauthentik.io/gen-go-api,src=./gen-go-api \
|
||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
||||
@@ -76,9 +78,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.6 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.8 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.6-slim-bookworm-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
@@ -119,7 +121,11 @@ RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/v
|
||||
libltdl-dev && \
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
|
||||
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec"
|
||||
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec" \
|
||||
# https://github.com/rust-lang/rustup/issues/2949
|
||||
# Fixes issues where the rust version in the build cache is older than latest
|
||||
# and rustup tries to update it, which fails
|
||||
RUSTUP_PERMIT_COPY_RENAME="true"
|
||||
|
||||
RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
|
||||
--mount=type=bind,target=uv.lock,src=uv.lock \
|
||||
@@ -175,6 +181,7 @@ 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
|
||||
RUN ln -s /ak-root/packages /packages
|
||||
COPY --from=python-deps /ak-root/.venv /ak-root/.venv
|
||||
COPY --from=node-builder /work/web/dist/ /web/dist/
|
||||
COPY --from=node-builder /work/web/authentik/ /web/authentik/
|
||||
|
||||
32
Makefile
32
Makefile
@@ -16,6 +16,7 @@ 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
|
||||
|
||||
@@ -57,7 +58,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 run aws-cfn
|
||||
cd lifecycle/aws && npm i && uv run npm run aws-cfn
|
||||
|
||||
run-server: ## Run the main authentik server process
|
||||
uv run ak server
|
||||
@@ -79,10 +80,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}
|
||||
dropdb -U ${pg_user} -h ${pg_host} ${pg_name} || true
|
||||
# Also remove the test-db if it exists
|
||||
dropdb -U ${pg_user} -h ${pg_host} test_${pg_name} || true
|
||||
redis-cli -n 0 flushall
|
||||
redis-cli -n ${redis_db} flushall
|
||||
|
||||
dev-create-db:
|
||||
createdb -U ${pg_user} -h ${pg_host} ${pg_name}
|
||||
@@ -93,6 +94,17 @@ 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
|
||||
$(eval current_version := $(shell cat ${PWD}/internal/constants/VERSION))
|
||||
sed -i 's/^version = ".*"/version = "$(version)"/' pyproject.toml
|
||||
sed -i 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
|
||||
$(MAKE) gen-build gen-compose aws-cfn
|
||||
sed -i "s/\"${current_version}\"/\"$(version)\"/" ${PWD}/package.json ${PWD}/package-lock.json ${PWD}/web/package.json ${PWD}/web/package-lock.json
|
||||
echo -n $(version) > ${PWD}/internal/constants/VERSION
|
||||
|
||||
#########################
|
||||
## API Schema
|
||||
#########################
|
||||
@@ -107,6 +119,9 @@ 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
|
||||
@@ -129,12 +144,7 @@ gen-clean-ts: ## Remove generated API client for TypeScript
|
||||
rm -rf ${PWD}/web/node_modules/@goauthentik/api/
|
||||
|
||||
gen-clean-go: ## Remove generated API client for Go
|
||||
mkdir -p ${PWD}/${GEN_API_GO}
|
||||
ifneq ($(wildcard ${PWD}/${GEN_API_GO}/.*),)
|
||||
make -C ${PWD}/${GEN_API_GO} clean
|
||||
else
|
||||
rm -rf ${PWD}/${GEN_API_GO}
|
||||
endif
|
||||
|
||||
gen-clean-py: ## Remove generated API client for Python
|
||||
rm -rf ${PWD}/${GEN_API_PY}/
|
||||
@@ -172,13 +182,9 @@ gen-client-py: gen-clean-py ## Build and install the authentik API for Python
|
||||
|
||||
gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
|
||||
mkdir -p ${PWD}/${GEN_API_GO}
|
||||
ifeq ($(wildcard ${PWD}/${GEN_API_GO}/.*),)
|
||||
git clone --depth 1 https://github.com/goauthentik/client-go.git ${PWD}/${GEN_API_GO}
|
||||
else
|
||||
cd ${PWD}/${GEN_API_GO} && git pull
|
||||
endif
|
||||
cp ${PWD}/schema.yml ${PWD}/${GEN_API_GO}
|
||||
make -C ${PWD}/${GEN_API_GO} build
|
||||
make -C ${PWD}/${GEN_API_GO} build version=${NPM_VERSION}
|
||||
go mod edit -replace goauthentik.io/api/v3=./${GEN_API_GO}
|
||||
|
||||
gen-dev-config: ## Generate a local development config file
|
||||
|
||||
27
README.md
27
README.md
@@ -15,15 +15,16 @@
|
||||
|
||||
## What is authentik?
|
||||
|
||||
authentik is an open-source Identity Provider that emphasizes flexibility and versatility, with support for a wide set of protocols.
|
||||
authentik is an open-source Identity Provider (IdP) for modern SSO. It supports SAML, OAuth2/OIDC, LDAP, RADIUS, and more, designed for self-hosting from small labs to large production clusters.
|
||||
|
||||
Our [enterprise offer](https://goauthentik.io/pricing) can also be used as a self-hosted replacement for large-scale deployments of Okta/Auth0, Entra ID, Ping Identity, or other legacy IdPs for employees and B2B2C use.
|
||||
Our [enterprise offering](https://goauthentik.io/pricing) is available for organizations to securely replace existing IdPs such as Okta, Auth0, Entra ID, and Ping Identity for robust, large-scale identity management.
|
||||
|
||||
## Installation
|
||||
|
||||
For small/test setups it is recommended to use Docker Compose; refer to the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github).
|
||||
|
||||
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github).
|
||||
- Docker Compose: recommended for small/test setups. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/docker-compose/).
|
||||
- Kubernetes (Helm Chart): recommended for larger setups. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/kubernetes/) and the Helm chart [repository](https://github.com/goauthentik/helm).
|
||||
- AWS CloudFormation: deploy on AWS using our official templates. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/aws/).
|
||||
- DigitalOcean Marketplace: one-click deployment via the official Marketplace app. See the [app listing](https://marketplace.digitalocean.com/apps/authentik).
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -32,14 +33,20 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Development
|
||||
## Development and contributions
|
||||
|
||||
See [Developer Documentation](https://docs.goauthentik.io/docs/developer-docs/?utm_source=github)
|
||||
See the [Developer Documentation](https://docs.goauthentik.io/docs/developer-docs/) for information about setting up local build environments, testing your contributions, and our contribution process.
|
||||
|
||||
## Security
|
||||
|
||||
See [SECURITY.md](SECURITY.md)
|
||||
Please see [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Adoption and Contributions
|
||||
## Adoption
|
||||
|
||||
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [contribution guide](https://docs.goauthentik.io/docs/developer-docs?utm_source=github).
|
||||
Using authentik? We'd love to hear your story and feature your logo. Email us at [hello@goauthentik.io](mailto:hello@goauthentik.io) or open a GitHub Issue/PR!
|
||||
|
||||
## License
|
||||
|
||||
[](LICENSE)
|
||||
[](website/LICENSE)
|
||||
[](authentik/enterprise/LICENSE)
|
||||
|
||||
25
SECURITY.md
25
SECURITY.md
@@ -20,12 +20,33 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | --------- |
|
||||
| 2025.4.x | ✅ |
|
||||
| 2025.6.x | ✅ |
|
||||
| 2025.8.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io). Be sure to include relevant information like which version you've found the issue in, instructions on how to reproduce the issue, and anything else that might make it easier for us to find the issue.
|
||||
If you discover a potential vulnerability, please report it responsibly through one of the following channels:
|
||||
|
||||
- **Email**: [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||
- **GitHub**: Submit a private security advisory via our [repository’s advisory portal](https://github.com/goauthentik/authentik/security/advisories/new)
|
||||
|
||||
When submitting a report, please include as much detail as possible, such as:
|
||||
|
||||
- **Affected version(s)**: The version of authentik where the issue was identified.
|
||||
- **Steps to reproduce**: A clear description or proof of concept to help us verify the issue.
|
||||
- **Impact assessment**: How the vulnerability could be exploited and its potential effect.
|
||||
- **Additional information**: Logs, configuration details (if relevant), or any suggested mitigations.
|
||||
|
||||
We kindly ask that you do not disclose the vulnerability publicly until we have confirmed and addressed the issue.
|
||||
|
||||
Our team will:
|
||||
|
||||
- Acknowledge receipt of your report as quickly as possible.
|
||||
- Keep you updated on the investigation and resolution progress.
|
||||
|
||||
## Researcher Recognition
|
||||
|
||||
We value contributions from the security community. For each valid report, we will publish a dedicated entry on our Security Advisory page that optionally includes the reporter’s name (or preferred alias). Please note that while we do not currently offer monetary bounties, we are committed to giving researchers appropriate credit for their efforts in keeping authentik secure.
|
||||
|
||||
## Severity levels
|
||||
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
"""authentik root module"""
|
||||
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.6.4"
|
||||
VERSION = "2025.8.6"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
def get_build_hash(fallback: str | None = None) -> str:
|
||||
@lru_cache
|
||||
def authentik_version() -> str:
|
||||
return VERSION
|
||||
|
||||
|
||||
@lru_cache
|
||||
def authentik_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
|
||||
|
||||
|
||||
def get_full_version() -> str:
|
||||
@lru_cache
|
||||
def authentik_full_version() -> str:
|
||||
"""Get full version, with build hash appended"""
|
||||
version = __version__
|
||||
if (build_hash := get_build_hash()) != "":
|
||||
version = authentik_version()
|
||||
if (build_hash := authentik_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 get_full_version
|
||||
from authentik import authentik_full_version
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.lib.config import CONFIG
|
||||
@@ -78,7 +78,7 @@ class SystemInfoSerializer(PassiveSerializer):
|
||||
"""Get versions"""
|
||||
return {
|
||||
"architecture": platform.machine(),
|
||||
"authentik_version": get_full_version(),
|
||||
"authentik_version": authentik_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 __version__, get_build_hash
|
||||
from authentik import authentik_build_hash, authentik_version
|
||||
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 get_build_hash()
|
||||
return authentik_build_hash()
|
||||
|
||||
def get_version_current(self, _) -> str:
|
||||
"""Get current version"""
|
||||
return __version__
|
||||
return authentik_version()
|
||||
|
||||
def get_version_latest(self, _) -> str:
|
||||
"""Get latest version from cache"""
|
||||
if get_current_tenant().schema_name == get_public_schema_name():
|
||||
return __version__
|
||||
return authentik_version()
|
||||
version_in_cache = cache.get(VERSION_CACHE_KEY)
|
||||
if not version_in_cache: # pragma: no cover
|
||||
update_latest_version.send()
|
||||
return __version__
|
||||
return authentik_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 __version__, get_build_hash
|
||||
from authentik import authentik_build_hash, authentik_version
|
||||
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(__version__)
|
||||
LOCAL_VERSION = parse(authentik_version())
|
||||
|
||||
|
||||
def _set_prom_info():
|
||||
"""Set prometheus info for version"""
|
||||
PROM_INFO.info(
|
||||
{
|
||||
"version": __version__,
|
||||
"version": authentik_version(),
|
||||
"latest": cache.get(VERSION_CACHE_KEY, ""),
|
||||
"build_hash": get_build_hash(),
|
||||
"build_hash": authentik_build_hash(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from json import loads
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik import __version__
|
||||
from authentik import authentik_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"], __version__)
|
||||
self.assertEqual(body["version_current"], authentik_version())
|
||||
|
||||
def test_apps(self):
|
||||
"""Test apps API"""
|
||||
|
||||
@@ -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 __version__
|
||||
from authentik import authentik_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 {__version__} Blueprint schema",
|
||||
"title": f"authentik {authentik_version()} Blueprint schema",
|
||||
"required": ["version", "entries"],
|
||||
"properties": {
|
||||
"version": {
|
||||
|
||||
@@ -38,6 +38,7 @@ from authentik.blueprints.v1.oci import OCI_PREFIX
|
||||
from authentik.events.logs import capture_logs
|
||||
from authentik.events.utils import sanitize_dict
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.tasks.apps import PRIORITY_HIGH
|
||||
from authentik.tasks.models import Task
|
||||
from authentik.tasks.schedules.models import Schedule
|
||||
from authentik.tenants.models import Tenant
|
||||
@@ -110,7 +111,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
|
||||
|
||||
@actor(
|
||||
description=_("Find blueprints as `blueprints_find` does, but return a safe dict."),
|
||||
throws=(DatabaseError, ProgrammingError, InternalError),
|
||||
priority=PRIORITY_HIGH,
|
||||
)
|
||||
def blueprints_find_dict():
|
||||
blueprints = []
|
||||
@@ -148,10 +149,7 @@ def blueprints_find() -> list[BlueprintFile]:
|
||||
return blueprints
|
||||
|
||||
|
||||
@actor(
|
||||
description=_("Find blueprints and check if they need to be created in the database."),
|
||||
throws=(DatabaseError, ProgrammingError, InternalError),
|
||||
)
|
||||
@actor(description=_("Find blueprints and check if they need to be created in the database."))
|
||||
def blueprints_discovery(path: str | None = None):
|
||||
self: Task = CurrentTask.get_task()
|
||||
count = 0
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
from typing import Any
|
||||
|
||||
from django.db import models
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, ChoiceField, ListField
|
||||
from rest_framework.fields import CharField, ChoiceField, ListField, SerializerMethodField
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
@@ -18,6 +18,8 @@ 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
|
||||
|
||||
|
||||
@@ -110,6 +112,16 @@ 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,11 +10,20 @@ 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()
|
||||
@@ -29,6 +38,7 @@ class TestBrands(APITestCase):
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -49,27 +59,7 @@ class TestBrands(APITestCase):
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
},
|
||||
)
|
||||
|
||||
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": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -87,6 +77,7 @@ class TestBrands(APITestCase):
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -167,6 +158,7 @@ class TestBrands(APITestCase):
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@ 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 get_full_version
|
||||
from authentik import authentik_full_version
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.lib.sentry import get_http_meta
|
||||
from authentik.tenants.models import Tenant
|
||||
@@ -21,9 +20,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()), match_length=Length("domain"))
|
||||
Brand.objects.annotate(host_domain=V(request.get_host()))
|
||||
.filter(Q(host_domain__iendswith=F("domain")) | _q_default)
|
||||
.order_by("-match_length", "default")
|
||||
.order_by("default")
|
||||
)
|
||||
brands = list(db_brands.all())
|
||||
if len(brands) < 1:
|
||||
@@ -44,5 +43,5 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
|
||||
"brand_css": brand_css,
|
||||
"footer_links": tenant.footer_links,
|
||||
"html_meta": {**get_http_meta()},
|
||||
"version": get_full_version(),
|
||||
"version": authentik_full_version(),
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -66,6 +67,15 @@ 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:
|
||||
|
||||
@@ -18,10 +18,14 @@ from authentik.core.models import Provider
|
||||
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Provider Serializer"""
|
||||
|
||||
assigned_application_slug = ReadOnlyField(source="application.slug")
|
||||
assigned_application_name = ReadOnlyField(source="application.name")
|
||||
assigned_backchannel_application_slug = ReadOnlyField(source="backchannel_application.slug")
|
||||
assigned_backchannel_application_name = ReadOnlyField(source="backchannel_application.name")
|
||||
assigned_application_slug = ReadOnlyField(source="application.slug", allow_null=True)
|
||||
assigned_application_name = ReadOnlyField(source="application.name", allow_null=True)
|
||||
assigned_backchannel_application_slug = ReadOnlyField(
|
||||
source="backchannel_application.slug", allow_null=True
|
||||
)
|
||||
assigned_backchannel_application_name = ReadOnlyField(
|
||||
source="backchannel_application.name", allow_null=True
|
||||
)
|
||||
|
||||
component = SerializerMethodField()
|
||||
|
||||
|
||||
@@ -328,6 +328,12 @@ class SessionUserSerializer(PassiveSerializer):
|
||||
original = UserSelfSerializer(required=False)
|
||||
|
||||
|
||||
class UserPasswordSetSerializer(PassiveSerializer):
|
||||
"""Payload to set a users' password directly"""
|
||||
|
||||
password = CharField(required=True)
|
||||
|
||||
|
||||
class UsersFilter(FilterSet):
|
||||
"""Filter for users"""
|
||||
|
||||
@@ -585,12 +591,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
"UserPasswordSetSerializer",
|
||||
{
|
||||
"password": CharField(required=True),
|
||||
},
|
||||
),
|
||||
request=UserPasswordSetSerializer,
|
||||
responses={
|
||||
204: OpenApiResponse(description="Successfully changed password"),
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
@@ -599,9 +600,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
@action(detail=True, methods=["POST"], permission_classes=[])
|
||||
def set_password(self, request: Request, pk: int) -> Response:
|
||||
"""Set password for user"""
|
||||
data = UserPasswordSetSerializer(data=request.data)
|
||||
data.is_valid(raise_exception=True)
|
||||
user: User = self.get_object()
|
||||
try:
|
||||
user.set_password(request.data.get("password"), request=request)
|
||||
user.set_password(data.validated_data["password"], request=request)
|
||||
user.save()
|
||||
except (ValidationError, IntegrityError) as exc:
|
||||
LOGGER.debug("Failed to set password", exc=exc)
|
||||
@@ -620,7 +623,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 accounts"""
|
||||
"""Create a temporary link that a user can use to recover their account"""
|
||||
link, _ = self._create_recovery_link()
|
||||
return Response({"link": link})
|
||||
|
||||
@@ -641,7 +644,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
def recovery_email(self, request: Request, pk: int) -> Response:
|
||||
"""Create a temporary link that a user can use to recover their accounts"""
|
||||
"""Send an email with a temporary link that a user can use to recover their account"""
|
||||
for_user: User = self.get_object()
|
||||
if for_user.email == "":
|
||||
LOGGER.debug("User doesn't have an email address")
|
||||
|
||||
@@ -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 get_full_version
|
||||
from authentik import authentik_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} ({get_full_version()})
|
||||
return f"""### authentik {shell_type} ({authentik_full_version()})
|
||||
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.12 on 2025-09-25 13:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0050_user_last_updated_and_more"),
|
||||
("authentik_rbac", "0006_alter_role_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="group",
|
||||
index=models.Index(fields=["is_superuser"], name="authentik_c_is_supe_1e5a97_idx"),
|
||||
),
|
||||
]
|
||||
@@ -29,6 +29,7 @@ from authentik.blueprints.models import ManagedModel
|
||||
from authentik.core.expression.exceptions import PropertyMappingExpressionException
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.lib.avatars import get_avatar
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.expression.exceptions import ControlFlowException
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.merge import MERGE_LIST_UNIQUE
|
||||
@@ -200,7 +201,10 @@ class Group(SerializerModel, AttributesMixin):
|
||||
"parent",
|
||||
),
|
||||
)
|
||||
indexes = [models.Index(fields=["name"])]
|
||||
indexes = (
|
||||
models.Index(fields=["name"]),
|
||||
models.Index(fields=["is_superuser"]),
|
||||
)
|
||||
verbose_name = _("Group")
|
||||
verbose_name_plural = _("Groups")
|
||||
permissions = [
|
||||
@@ -548,6 +552,9 @@ 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
|
||||
@@ -560,8 +567,10 @@ class Application(SerializerModel, PolicyBindingModel):
|
||||
it is returned as-is"""
|
||||
if not self.meta_icon:
|
||||
return None
|
||||
if "://" in self.meta_icon.name or self.meta_icon.name.startswith("/static"):
|
||||
if self.meta_icon.name.startswith("http"):
|
||||
return self.meta_icon.name
|
||||
if self.meta_icon.name.startswith("/"):
|
||||
return CONFIG.get("web.path", "/")[:-1] + self.meta_icon.name
|
||||
return self.meta_icon.url
|
||||
|
||||
def get_launch_url(self, user: Optional["User"] = None) -> str | None:
|
||||
@@ -763,8 +772,10 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
starts with http it is returned as-is"""
|
||||
if not self.icon:
|
||||
return None
|
||||
if "://" in self.icon.name or self.icon.name.startswith("/static"):
|
||||
if self.icon.name.startswith("http"):
|
||||
return self.icon.name
|
||||
if self.icon.name.startswith("/"):
|
||||
return CONFIG.get("web.path", "/")[:-1] + self.icon.name
|
||||
return self.icon.url
|
||||
|
||||
def get_user_path(self) -> str:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from django import template
|
||||
from django.templatetags.static import static as static_loader
|
||||
|
||||
from authentik import get_full_version
|
||||
from authentik import authentik_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", get_full_version()))
|
||||
return static_loader(path.replace("%v", authentik_full_version()))
|
||||
|
||||
@@ -82,6 +82,51 @@ class TestApplicationsAPI(APITestCase):
|
||||
self.assertEqual(self.allowed.get_meta_icon, app["meta_icon"])
|
||||
self.assertEqual(self.allowed.meta_icon.read(), b"text")
|
||||
|
||||
def test_set_icon_relative(self):
|
||||
"""Test set_icon (relative path)"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:application-set-icon-url",
|
||||
kwargs={"slug": self.allowed.slug},
|
||||
),
|
||||
data={"url": "relative/path"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.allowed.refresh_from_db()
|
||||
self.assertEqual(self.allowed.get_meta_icon, "/media/public/relative/path")
|
||||
|
||||
def test_set_icon_absolute(self):
|
||||
"""Test set_icon (absolute path)"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:application-set-icon-url",
|
||||
kwargs={"slug": self.allowed.slug},
|
||||
),
|
||||
data={"url": "/relative/path"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.allowed.refresh_from_db()
|
||||
self.assertEqual(self.allowed.get_meta_icon, "/relative/path")
|
||||
|
||||
def test_set_icon_url(self):
|
||||
"""Test set_icon (url)"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:application-set-icon-url",
|
||||
kwargs={"slug": self.allowed.slug},
|
||||
),
|
||||
data={"url": "https://authentik.company/img.png"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.allowed.refresh_from_db()
|
||||
self.assertEqual(self.allowed.get_meta_icon, "https://authentik.company/img.png")
|
||||
|
||||
def test_check_access(self):
|
||||
"""Test check_access operation"""
|
||||
self.client.force_login(self.user)
|
||||
@@ -134,6 +179,8 @@ class TestApplicationsAPI(APITestCase):
|
||||
"provider_obj": {
|
||||
"assigned_application_name": "allowed",
|
||||
"assigned_application_slug": "allowed",
|
||||
"assigned_backchannel_application_name": None,
|
||||
"assigned_backchannel_application_slug": None,
|
||||
"authentication_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||
@@ -188,6 +235,8 @@ class TestApplicationsAPI(APITestCase):
|
||||
"provider_obj": {
|
||||
"assigned_application_name": "allowed",
|
||||
"assigned_application_slug": "allowed",
|
||||
"assigned_backchannel_application_name": None,
|
||||
"assigned_backchannel_application_slug": None,
|
||||
"authentication_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||
@@ -257,3 +306,35 @@ 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])
|
||||
|
||||
@@ -102,6 +102,16 @@ class TestUsersAPI(APITestCase):
|
||||
self.admin.refresh_from_db()
|
||||
self.assertTrue(self.admin.check_password(new_pw))
|
||||
|
||||
def test_set_password_blank(self):
|
||||
"""Test Direct password set"""
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-set-password", kwargs={"pk": self.admin.pk}),
|
||||
data={"password": ""},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(response.content, {"password": ["This field may not be blank."]})
|
||||
|
||||
def test_recovery(self):
|
||||
"""Test user recovery link"""
|
||||
flow = create_test_flow(
|
||||
|
||||
@@ -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 get_build_hash
|
||||
from authentik import authentik_build_hash
|
||||
from authentik.admin.tasks import LOCAL_VERSION
|
||||
from authentik.api.v3.config import ConfigView
|
||||
from authentik.brands.api import CurrentBrandSerializer
|
||||
@@ -52,7 +52,7 @@ class InterfaceView(TemplateView):
|
||||
kwargs["brand_json"] = dumps(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"] = get_build_hash()
|
||||
kwargs["build"] = authentik_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 __version__
|
||||
from authentik import authentik_version
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ class CertificateBuilder:
|
||||
.issuer_name(
|
||||
x509.Name(
|
||||
[
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, f"authentik {__version__}"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, f"authentik {authentik_version()}"),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ class GoogleWorkspaceGroupClient(
|
||||
"""Google client for groups"""
|
||||
|
||||
connection_type = GoogleWorkspaceProviderGroup
|
||||
connection_attr = "googleworkspaceprovidergroup_set"
|
||||
connection_type_query = "group"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: GoogleWorkspaceProvider) -> None:
|
||||
|
||||
@@ -20,7 +20,7 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
|
||||
"""Sync authentik users into google workspace"""
|
||||
|
||||
connection_type = GoogleWorkspaceProviderUser
|
||||
connection_attr = "googleworkspaceprovideruser_set"
|
||||
connection_type_query = "user"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: GoogleWorkspaceProvider) -> None:
|
||||
|
||||
@@ -139,11 +139,7 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
if type == User:
|
||||
# Get queryset of all users with consistent ordering
|
||||
# according to the provider's settings
|
||||
base = (
|
||||
User.objects.prefetch_related("googleworkspaceprovideruser_set")
|
||||
.all()
|
||||
.exclude_anonymous()
|
||||
)
|
||||
base = User.objects.all().exclude_anonymous()
|
||||
if self.exclude_users_service_account:
|
||||
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
@@ -153,11 +149,7 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
return base.order_by("pk")
|
||||
if type == Group:
|
||||
# Get queryset of all groups with consistent ordering
|
||||
return (
|
||||
Group.objects.prefetch_related("googleworkspaceprovidergroup_set")
|
||||
.all()
|
||||
.order_by("pk")
|
||||
)
|
||||
return Group.objects.all().order_by("pk")
|
||||
raise ValueError(f"Invalid type {type}")
|
||||
|
||||
def google_credentials(self):
|
||||
|
||||
@@ -29,7 +29,7 @@ class MicrosoftEntraGroupClient(
|
||||
"""Microsoft client for groups"""
|
||||
|
||||
connection_type = MicrosoftEntraProviderGroup
|
||||
connection_attr = "microsoftentraprovidergroup_set"
|
||||
connection_type_query = "group"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: MicrosoftEntraProvider) -> None:
|
||||
|
||||
@@ -24,7 +24,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
|
||||
"""Sync authentik users into microsoft entra"""
|
||||
|
||||
connection_type = MicrosoftEntraProviderUser
|
||||
connection_attr = "microsoftentraprovideruser_set"
|
||||
connection_type_query = "user"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: MicrosoftEntraProvider) -> None:
|
||||
|
||||
@@ -128,11 +128,7 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
if type == User:
|
||||
# Get queryset of all users with consistent ordering
|
||||
# according to the provider's settings
|
||||
base = (
|
||||
User.objects.prefetch_related("microsoftentraprovideruser_set")
|
||||
.all()
|
||||
.exclude_anonymous()
|
||||
)
|
||||
base = User.objects.all().exclude_anonymous()
|
||||
if self.exclude_users_service_account:
|
||||
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
@@ -142,11 +138,7 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
return base.order_by("pk")
|
||||
if type == Group:
|
||||
# Get queryset of all groups with consistent ordering
|
||||
return (
|
||||
Group.objects.prefetch_related("microsoftentraprovidergroup_set")
|
||||
.all()
|
||||
.order_by("pk")
|
||||
)
|
||||
return Group.objects.all().order_by("pk")
|
||||
raise ValueError(f"Invalid type {type}")
|
||||
|
||||
def microsoft_credentials(self):
|
||||
|
||||
@@ -23,6 +23,7 @@ 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):
|
||||
@@ -30,6 +31,18 @@ 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
|
||||
@@ -52,6 +65,8 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||
"webhook_url",
|
||||
"webhook_mapping_body",
|
||||
"webhook_mapping_headers",
|
||||
"email_subject_prefix",
|
||||
"email_template",
|
||||
"send_once",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# 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 get_full_version
|
||||
from authentik import authentik_full_version
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.brands.utils import DEFAULT_BRAND
|
||||
from authentik.core.middleware import (
|
||||
@@ -41,6 +41,7 @@ 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
|
||||
@@ -295,6 +296,15 @@ 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(
|
||||
@@ -319,12 +329,6 @@ 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"""
|
||||
@@ -434,7 +438,7 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
"title": notification.body,
|
||||
"color": "#fd4b2d",
|
||||
"fields": fields,
|
||||
"footer": f"authentik {get_full_version()}",
|
||||
"footer": f"authentik {authentik_full_version()}",
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -462,7 +466,6 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
notification=notification,
|
||||
)
|
||||
return None
|
||||
subject_prefix = "authentik Notification: "
|
||||
context = {
|
||||
"key_value": {
|
||||
"user_email": notification.user.email,
|
||||
@@ -490,10 +493,10 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
"from": self.name,
|
||||
}
|
||||
mail = TemplateEmailMessage(
|
||||
subject=subject_prefix + context["title"],
|
||||
subject=self.email_subject_prefix + context["title"],
|
||||
to=[(notification.user.name, notification.user.email)],
|
||||
language=notification.user.locale(),
|
||||
template_name="email/event_notification.html",
|
||||
template_name=self.email_template,
|
||||
template_context=context,
|
||||
)
|
||||
send_mail.send_with_options(args=(mail.__dict__,), rel_obj=self)
|
||||
|
||||
@@ -5,10 +5,12 @@ 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 get_full_version
|
||||
from authentik import authentik_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,
|
||||
@@ -18,6 +20,7 @@ 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):
|
||||
@@ -118,7 +121,7 @@ class TestEventTransports(TestCase):
|
||||
{"short": True, "title": "Event user", "value": self.user.username},
|
||||
{"title": "foo", "value": "bar,"},
|
||||
],
|
||||
"footer": f"authentik {get_full_version()}",
|
||||
"footer": f"authentik {authentik_full_version()}",
|
||||
}
|
||||
],
|
||||
},
|
||||
@@ -138,3 +141,76 @@ 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"]])
|
||||
|
||||
@@ -46,5 +46,5 @@ class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = FlowStageBindingSerializer
|
||||
filterset_fields = "__all__"
|
||||
search_fields = ["stage__name"]
|
||||
ordering = ["order"]
|
||||
ordering_fields = ["order", "stage__name"]
|
||||
ordering = ["order", "pk"]
|
||||
ordering_fields = ["order", "stage__name", "target__uuid", "pk"]
|
||||
|
||||
@@ -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 __version__
|
||||
from authentik import authentik_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: {__version__}")
|
||||
print(f"Version: {authentik_version()}")
|
||||
print(f"Processes: {len(values)}")
|
||||
print(f"\tMax: {total_max * 100}ms")
|
||||
print(f"\tMin: {total_min * 100}ms")
|
||||
|
||||
@@ -190,7 +190,7 @@ class Flow(SerializerModel, PolicyBindingModel):
|
||||
)
|
||||
if self.background.name.startswith("http"):
|
||||
return self.background.name
|
||||
if self.background.name.startswith("/static"):
|
||||
if self.background.name.startswith("/"):
|
||||
return CONFIG.get("web.path", "/")[:-1] + self.background.name
|
||||
return self.background.url
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ def start_debug_server(**kwargs) -> bool:
|
||||
)
|
||||
return False
|
||||
|
||||
listen: str = CONFIG.get("listen.listen_debug_py", "127.0.0.1:9901")
|
||||
listen: str = CONFIG.get("listen.debug_py", "127.0.0.1:9901")
|
||||
host, _, port = listen.rpartition(":")
|
||||
try:
|
||||
debugpy.listen((host, int(port)), **kwargs) # nosec
|
||||
|
||||
@@ -31,14 +31,14 @@ postgresql:
|
||||
# host: replica1.example.com
|
||||
|
||||
listen:
|
||||
listen_http: 0.0.0.0:9000
|
||||
listen_https: 0.0.0.0:9443
|
||||
listen_ldap: 0.0.0.0:3389
|
||||
listen_ldaps: 0.0.0.0:6636
|
||||
listen_radius: 0.0.0.0:1812
|
||||
listen_metrics: 0.0.0.0:9300
|
||||
listen_debug: 0.0.0.0:9900
|
||||
listen_debug_py: 0.0.0.0:9901
|
||||
http: 0.0.0.0:9000
|
||||
https: 0.0.0.0:9443
|
||||
ldap: 0.0.0.0:3389
|
||||
ldaps: 0.0.0.0:6636
|
||||
radius: 0.0.0.0:1812
|
||||
metrics: 0.0.0.0:9300
|
||||
debug: 0.0.0.0:9900
|
||||
debug_py: 0.0.0.0:9901
|
||||
trusted_proxy_cidrs:
|
||||
- 127.0.0.0/8
|
||||
- 10.0.0.0/8
|
||||
@@ -152,8 +152,9 @@ worker:
|
||||
processes: 1
|
||||
threads: 2
|
||||
consumer_listen_timeout: "seconds=30"
|
||||
task_max_retries: 20
|
||||
task_max_retries: 5
|
||||
task_default_time_limit: "minutes=10"
|
||||
lock_purge_interval: "minutes=1"
|
||||
task_purge_interval: "days=1"
|
||||
task_expiration: "days=30"
|
||||
scheduler_interval: "seconds=60"
|
||||
|
||||
@@ -36,7 +36,7 @@ ARG_SANITIZE = re.compile(r"[:.-]")
|
||||
|
||||
|
||||
def sanitize_arg(arg_name: str) -> str:
|
||||
return re.sub(ARG_SANITIZE, "_", arg_name)
|
||||
return re.sub(ARG_SANITIZE, "_", slugify(arg_name))
|
||||
|
||||
|
||||
class BaseEvaluator:
|
||||
@@ -218,7 +218,9 @@ class BaseEvaluator:
|
||||
|
||||
def wrap_expression(self, expression: str) -> str:
|
||||
"""Wrap expression in a function, call it, and save the result as `result`"""
|
||||
handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())
|
||||
handler_signature = ",".join(
|
||||
[x for x in [sanitize_arg(x) for x in self._context.keys()] if x]
|
||||
)
|
||||
full_expression = ""
|
||||
full_expression += f"def handler({handler_signature}):\n"
|
||||
full_expression += indent(expression, " ")
|
||||
|
||||
@@ -14,7 +14,7 @@ LOG_PRE_CHAIN = [
|
||||
# is not from structlog.
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.processors.TimeStamper(),
|
||||
structlog.processors.TimeStamper(fmt="iso", utc=False),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
]
|
||||
|
||||
@@ -43,7 +43,9 @@ def structlog_configure():
|
||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||
structlog.processors.TimeStamper(fmt="iso", utc=False),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.dict_tracebacks,
|
||||
structlog.processors.ExceptionRenderer(
|
||||
structlog.processors.ExceptionDictTransformer(show_locals=CONFIG.get_bool("debug"))
|
||||
),
|
||||
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
||||
],
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
@@ -65,7 +67,14 @@ def get_logger_config():
|
||||
"json": {
|
||||
"()": structlog.stdlib.ProcessorFormatter,
|
||||
"processor": structlog.processors.JSONRenderer(sort_keys=True),
|
||||
"foreign_pre_chain": LOG_PRE_CHAIN + [structlog.processors.dict_tracebacks],
|
||||
"foreign_pre_chain": LOG_PRE_CHAIN
|
||||
+ [
|
||||
structlog.processors.ExceptionRenderer(
|
||||
structlog.processors.ExceptionDictTransformer(
|
||||
show_locals=CONFIG.get_bool("debug")
|
||||
)
|
||||
),
|
||||
],
|
||||
},
|
||||
"console": {
|
||||
"()": structlog.stdlib.ProcessorFormatter,
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -21,6 +22,7 @@ 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
|
||||
@@ -29,7 +31,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 __version__, get_build_hash
|
||||
from authentik import authentik_build_hash, authentik_version
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.http import authentik_user_agent
|
||||
from authentik.lib.utils.reflection import get_env
|
||||
@@ -68,6 +70,8 @@ ignored_classes = (
|
||||
LocalProtocolError,
|
||||
# rest_framework error
|
||||
APIException,
|
||||
# dramatiq errors
|
||||
Retry,
|
||||
# custom baseclass
|
||||
SentryIgnoredException,
|
||||
# ldap errors
|
||||
@@ -106,19 +110,20 @@ def sentry_init(**sentry_init_kwargs):
|
||||
dsn=CONFIG.get("error_reporting.sentry_dsn"),
|
||||
integrations=[
|
||||
ArgvIntegration(),
|
||||
StdlibIntegration(),
|
||||
DjangoIntegration(transaction_style="function_name", cache_spans=True),
|
||||
DramatiqIntegration(),
|
||||
RedisIntegration(),
|
||||
ThreadingIntegration(propagate_hub=True),
|
||||
SocketIntegration(),
|
||||
StdlibIntegration(),
|
||||
ThreadingIntegration(propagate_hub=True),
|
||||
],
|
||||
before_send=before_send,
|
||||
traces_sampler=traces_sampler,
|
||||
release=f"authentik@{__version__}",
|
||||
release=f"authentik@{authentik_version()}",
|
||||
transport=SentryTransport,
|
||||
**kwargs,
|
||||
)
|
||||
set_tag("authentik.build_hash", get_build_hash("tagged"))
|
||||
set_tag("authentik.build_hash", authentik_build_hash("tagged"))
|
||||
set_tag("authentik.env", get_env())
|
||||
set_tag("authentik.component", "backend")
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from dramatiq.actor import Actor
|
||||
from dramatiq.results.errors import ResultFailure
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, CharField, ChoiceField
|
||||
@@ -110,9 +111,13 @@ class OutgoingSyncProviderStatusMixin:
|
||||
"override_dry_run": params.validated_data["override_dry_run"],
|
||||
"pk": params.validated_data["sync_object_id"],
|
||||
},
|
||||
retries=0,
|
||||
rel_obj=provider,
|
||||
)
|
||||
msg.get_result(block=True)
|
||||
try:
|
||||
msg.get_result(block=True)
|
||||
except ResultFailure:
|
||||
pass
|
||||
task: Task = msg.options["task"]
|
||||
task.refresh_from_db()
|
||||
return Response(SyncObjectResultSerializer(instance={"messages": task._messages}).data)
|
||||
|
||||
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class Direction(StrEnum):
|
||||
|
||||
add = "add"
|
||||
remove = "remove"
|
||||
|
||||
@@ -35,16 +36,13 @@ SAFE_METHODS = [
|
||||
|
||||
|
||||
class BaseOutgoingSyncClient[
|
||||
TModel: "Model",
|
||||
TConnection: "Model",
|
||||
TSchema: dict,
|
||||
TProvider: "OutgoingSyncProvider",
|
||||
TModel: "Model", TConnection: "Model", TSchema: dict, TProvider: "OutgoingSyncProvider"
|
||||
]:
|
||||
"""Basic Outgoing sync client Client"""
|
||||
|
||||
provider: TProvider
|
||||
connection_type: type[TConnection]
|
||||
connection_attr: str
|
||||
connection_type_query: str
|
||||
mapper: PropertyMappingManager
|
||||
|
||||
can_discover = False
|
||||
@@ -64,7 +62,9 @@ class BaseOutgoingSyncClient[
|
||||
def write(self, obj: TModel) -> tuple[TConnection, bool]:
|
||||
"""Write object to destination. Uses self.create and self.update, but
|
||||
can be overwritten for further logic"""
|
||||
connection = getattr(obj, self.connection_attr).filter(provider=self.provider).first()
|
||||
connection = self.connection_type.objects.filter(
|
||||
provider=self.provider, **{self.connection_type_query: obj}
|
||||
).first()
|
||||
try:
|
||||
if not connection:
|
||||
connection = self.create(obj)
|
||||
|
||||
@@ -16,8 +16,18 @@ def register_signals(
|
||||
"""Register sync signals"""
|
||||
uid = class_to_path(provider_type)
|
||||
|
||||
def model_post_save(sender: type[Model], instance: User | Group, created: bool, **_):
|
||||
def model_post_save(
|
||||
sender: type[Model],
|
||||
instance: User | Group,
|
||||
created: bool,
|
||||
update_fields: list[str] | None = None,
|
||||
**_,
|
||||
):
|
||||
"""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,
|
||||
|
||||
@@ -20,6 +20,7 @@ from authentik.lib.sync.outgoing.exceptions import (
|
||||
TransientSyncException,
|
||||
)
|
||||
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
|
||||
from authentik.lib.utils.errors import exception_to_dict
|
||||
from authentik.lib.utils.reflection import class_to_path, path_to_class
|
||||
from authentik.tasks.models import Task
|
||||
|
||||
@@ -164,16 +165,17 @@ class SyncTasks:
|
||||
except BadRequestSyncException as exc:
|
||||
self.logger.warning("failed to sync object", exc=exc, obj=obj)
|
||||
task.warning(
|
||||
f"Failed to sync {obj._meta.verbose_name} {str(obj)} due to error: {str(exc)}",
|
||||
f"Failed to sync {str(obj)} due to error: {str(exc)}",
|
||||
arguments=exc.args[1:],
|
||||
obj=sanitize_item(obj),
|
||||
exception=exception_to_dict(exc),
|
||||
)
|
||||
except TransientSyncException as exc:
|
||||
self.logger.warning("failed to sync object", exc=exc, user=obj)
|
||||
task.warning(
|
||||
f"Failed to sync {obj._meta.verbose_name} {str(obj)} due to "
|
||||
"transient error: {str(exc)}",
|
||||
f"Failed to sync {str(obj)} due to " f"transient error: {str(exc)}",
|
||||
obj=sanitize_item(obj),
|
||||
exception=exception_to_dict(exc),
|
||||
)
|
||||
except StopSync as exc:
|
||||
self.logger.warning("Stopping sync", exc=exc)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Test Evaluator base functions"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from jwt import decode
|
||||
@@ -77,3 +79,18 @@ class TestEvaluator(TestCase):
|
||||
jwt, provider.client_secret, algorithms=["HS256"], audience=provider.client_id
|
||||
)
|
||||
self.assertEqual(decoded["preferred_username"], user.username)
|
||||
|
||||
def test_expr_arg_escape(self):
|
||||
"""Test escaping of arguments"""
|
||||
eval = BaseEvaluator()
|
||||
eval._context = {
|
||||
'z=getattr(getattr(__import__("os"), "popen")("id > /tmp/test"), "read")()': "bar",
|
||||
"@@": "baz",
|
||||
"{{": "baz",
|
||||
"aa@@": "baz",
|
||||
}
|
||||
res = eval.evaluate("return locals()")
|
||||
self.assertEqual(
|
||||
res, {"zgetattrgetattr__import__os_popenid_tmptest_read": "bar", "aa": "baz"}
|
||||
)
|
||||
self.assertFalse(Path("/tmp/test").exists()) # nosec
|
||||
|
||||
@@ -4,9 +4,11 @@ from traceback import extract_tb
|
||||
|
||||
from structlog.tracebacks import ExceptionDictTransformer
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
|
||||
TRACEBACK_HEADER = "Traceback (most recent call last):"
|
||||
_exception_transformer = ExceptionDictTransformer(show_locals=CONFIG.get_bool("debug"))
|
||||
|
||||
|
||||
def exception_to_string(exc: Exception) -> str:
|
||||
@@ -23,4 +25,4 @@ def exception_to_string(exc: Exception) -> str:
|
||||
|
||||
def exception_to_dict(exc: Exception) -> dict:
|
||||
"""Format exception as a dictionary"""
|
||||
return ExceptionDictTransformer()((type(exc), exc, exc.__traceback__))
|
||||
return _exception_transformer((type(exc), exc, exc.__traceback__))
|
||||
|
||||
@@ -5,7 +5,7 @@ from uuid import uuid4
|
||||
from requests.sessions import PreparedRequest, Session
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import get_full_version
|
||||
from authentik import authentik_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@{get_full_version()}"
|
||||
return f"authentik@{authentik_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 get_build_hash
|
||||
from authentik import authentik_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
|
||||
@@ -47,7 +47,9 @@ class OutpostSerializer(ModelSerializer):
|
||||
)
|
||||
providers_obj = ProviderSerializer(source="providers", many=True, read_only=True)
|
||||
service_connection_obj = ServiceConnectionSerializer(
|
||||
source="service_connection", read_only=True
|
||||
source="service_connection",
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
)
|
||||
refresh_interval_s = SerializerMethodField()
|
||||
|
||||
@@ -194,7 +196,7 @@ class OutpostViewSet(UsedByMixin, ModelViewSet):
|
||||
"openssl_version": state.openssl_version,
|
||||
"fips_enabled": state.fips_enabled,
|
||||
"hostname": state.hostname,
|
||||
"build_hash_should": get_build_hash(),
|
||||
"build_hash_should": authentik_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 __version__, get_build_hash
|
||||
from authentik import authentik_build_hash, authentik_version
|
||||
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": __version__,
|
||||
"build_hash": get_build_hash(),
|
||||
"version": authentik_version(),
|
||||
"build_hash": authentik_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 __version__
|
||||
from authentik import authentik_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}:{__version__}"
|
||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:{authentik_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 __version__
|
||||
from authentik import authentik_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 __version__ to make testing easier"""
|
||||
return __version__
|
||||
"""Wrapper for authentik_version() to make testing easier"""
|
||||
return authentik_version()
|
||||
|
||||
|
||||
class KubernetesObjectReconciler(Generic[T]):
|
||||
|
||||
@@ -23,7 +23,7 @@ from kubernetes.client import (
|
||||
V1SecurityContext,
|
||||
)
|
||||
|
||||
from authentik import get_full_version
|
||||
from authentik import authentik_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 = get_full_version().replace("+", "-")
|
||||
version = authentik_full_version().replace("+", "-")
|
||||
return V1Deployment(
|
||||
metadata=meta,
|
||||
spec=V1DeploymentSpec(
|
||||
|
||||
@@ -13,6 +13,7 @@ from urllib3.exceptions import HTTPError
|
||||
from yaml import dump_all
|
||||
|
||||
from authentik.events.logs import LogEvent, capture_logs
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
@@ -105,7 +106,7 @@ class KubernetesController(BaseController):
|
||||
LogEvent(
|
||||
log_level="info",
|
||||
event=f"{reconcile_key.title()}: Disabled",
|
||||
logger=str(type(self)),
|
||||
logger=class_to_path(self.__class__),
|
||||
)
|
||||
)
|
||||
continue
|
||||
@@ -144,7 +145,7 @@ class KubernetesController(BaseController):
|
||||
LogEvent(
|
||||
log_level="info",
|
||||
event=f"{reconcile_key.title()}: Disabled",
|
||||
logger=str(type(self)),
|
||||
logger=class_to_path(self.__class__),
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -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 __version__, get_build_hash
|
||||
from authentik import authentik_build_hash, authentik_version
|
||||
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(__version__)
|
||||
OUR_VERSION = parse(authentik_version())
|
||||
OUTPOST_HELLO_INTERVAL = 10
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -76,6 +76,7 @@ class OutpostConfig:
|
||||
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
|
||||
kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls")
|
||||
kubernetes_ingress_class_name: str | None = field(default=None)
|
||||
kubernetes_ingress_path_type: str | None = field(default=None)
|
||||
kubernetes_httproute_annotations: dict[str, str] = field(default_factory=dict)
|
||||
kubernetes_httproute_parent_refs: list[dict[str, str]] = field(default_factory=list)
|
||||
kubernetes_service_type: str = field(default="ClusterIP")
|
||||
@@ -151,7 +152,7 @@ class OutpostServiceConnection(ScheduledModel, models.Model):
|
||||
|
||||
state = cache.get(self.state_key, None)
|
||||
if not state:
|
||||
outpost_service_connection_monitor.send_with_options(args=(self.pk), rel_obj=self)
|
||||
outpost_service_connection_monitor.send_with_options(args=(self.pk,), rel_obj=self)
|
||||
return OutpostServiceConnectionState("", False)
|
||||
return state
|
||||
|
||||
@@ -481,7 +482,7 @@ class OutpostState:
|
||||
"""Check if outpost version matches our version"""
|
||||
if not self.version:
|
||||
return False
|
||||
if self.build_hash != get_build_hash():
|
||||
if self.build_hash != authentik_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 __version__
|
||||
from authentik import authentik_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": __version__,
|
||||
"version": authentik_version(),
|
||||
"buildHash": "foo",
|
||||
"uuid": "123",
|
||||
},
|
||||
|
||||
@@ -124,4 +124,5 @@ class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = PolicyBindingSerializer
|
||||
search_fields = ["policy__name"]
|
||||
filterset_class = PolicyBindingFilter
|
||||
ordering = ["target", "order"]
|
||||
ordering = ["order", "pk"]
|
||||
ordering_fields = ["order", "target__uuid", "pk"]
|
||||
|
||||
@@ -7,6 +7,7 @@ For example: The 'dummy' policy is available at `authentik.policies.dummy`.
|
||||
from prometheus_client import Gauge, Histogram
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.tenants.flags import Flag
|
||||
|
||||
GAUGE_POLICIES_CACHED = Gauge(
|
||||
"authentik_policies_cached",
|
||||
@@ -25,13 +26,18 @@ HIST_POLICIES_EXECUTION_TIME = Histogram(
|
||||
"binding_order",
|
||||
"binding_target_type",
|
||||
"binding_target_name",
|
||||
"object_pk",
|
||||
"object_type",
|
||||
"mode",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class BufferedPolicyAccessViewFlag(Flag[bool], key="policies_buffered_access_view"):
|
||||
|
||||
default = False
|
||||
visibility = "public"
|
||||
|
||||
|
||||
class AuthentikPoliciesConfig(ManagedAppConfig):
|
||||
"""authentik policies app config"""
|
||||
|
||||
@@ -39,3 +45,4 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
|
||||
label = "authentik_policies"
|
||||
verbose_name = "authentik Policies"
|
||||
default = True
|
||||
mountpoint = "policy/"
|
||||
|
||||
@@ -86,7 +86,6 @@ class PolicyEngine:
|
||||
binding_order=binding.order,
|
||||
binding_target_type=binding.target_type,
|
||||
binding_target_name=binding.target_name,
|
||||
object_pk=str(self.request.obj.pk),
|
||||
object_type=class_to_path(self.request.obj.__class__),
|
||||
mode="cache_retrieve",
|
||||
).time():
|
||||
|
||||
@@ -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_lowercase:
|
||||
if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_uppercase:
|
||||
LOGGER.debug("password failed", check="static", reason="amount_uppercase")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_symbols > 0:
|
||||
|
||||
@@ -131,7 +131,6 @@ class PolicyProcess(PROCESS_CLASS):
|
||||
binding_order=self.binding.order,
|
||||
binding_target_type=self.binding.target_type,
|
||||
binding_target_name=self.binding.target_name,
|
||||
object_pk=str(self.request.obj.pk) if self.request.obj else "",
|
||||
object_type=class_to_path(self.request.obj.__class__) if self.request.obj else "",
|
||||
mode="execute_process",
|
||||
).time(),
|
||||
|
||||
121
authentik/policies/templates/policies/buffer.html
Normal file
121
authentik/policies/templates/policies/buffer.html
Normal file
@@ -0,0 +1,121 @@
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
let redirecting = false;
|
||||
|
||||
async function checkAuth() {
|
||||
if (redirecting) {
|
||||
console.debug(
|
||||
"authentik/policies/buffer: Already authenticating in another tab. This page will refresh once authentication is completed.",
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const url = "{{ check_auth_url }}";
|
||||
console.debug("authentik/policies/buffer: Checking authentication...");
|
||||
|
||||
return fetch(url, {
|
||||
method: "HEAD",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status >= 400) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.debug("authentik/policies/buffer: Continuing");
|
||||
|
||||
if ("{{ auth_req_method }}" === "post") {
|
||||
document.querySelector("form")?.submit();
|
||||
return true;
|
||||
}
|
||||
|
||||
window.location.assign("{{ continue_url|escapejs }}");
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("authentik/policies/buffer: Error checking authentication.", error);
|
||||
return false;
|
||||
})
|
||||
}
|
||||
|
||||
const offset = 20;
|
||||
|
||||
let timeoutID = -1;
|
||||
let timeout = 100;
|
||||
let attempts = 0;
|
||||
|
||||
async function main() {
|
||||
window.clearTimeout(timeoutID);
|
||||
|
||||
attempts += 1;
|
||||
|
||||
redirecting = await checkAuth();
|
||||
|
||||
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
|
||||
|
||||
timeoutID = window.setTimeout(main, timeout);
|
||||
|
||||
timeout += offset * attempts;
|
||||
|
||||
if (timeout >= 2000) {
|
||||
timeout = 2000;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("visibilitychange", async () => {
|
||||
if (document.hidden) return;
|
||||
|
||||
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
|
||||
|
||||
redirecting = await checkAuth();
|
||||
});
|
||||
|
||||
main();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
{% trans 'Waiting for authentication...' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
|
||||
{% if auth_req_method == "post" %}
|
||||
{% for key, value in auth_req_body.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<div class="pf-c-empty-state__icon">
|
||||
<span class="pf-c-spinner pf-m-xl" role="progressbar">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
|
||||
{% trans "Authenticate in this tab" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
125
authentik/policies/tests/test_views.py
Normal file
125
authentik/policies/tests/test_views.py
Normal file
@@ -0,0 +1,125 @@
|
||||
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())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user