Compare commits

...

30 Commits

Author SHA1 Message Date
authentik-automation[bot]
f7e25fff1a release: 2025.8.0 2025-08-20 18:01:34 +00:00
Marc 'risson' Schmitt
688b3a9f8b tasks: add rel_obj to system task exception event (cherry-pick #16270) (#16272) 2025-08-20 19:31:02 +02:00
Marc 'risson' Schmitt
8f48e18854 website/docs: update 2025.8 release notes (cherry-pick #16269) (#16271) 2025-08-20 19:20:18 +02:00
Marc 'risson' Schmitt
306f75be59 security: Bump supported versions (cherry-pick #16261) (#16267)
Co-authored-by: Dominic R <dominic@sdko.org>
2025-08-20 19:17:08 +02:00
Tana M Berry
982c3cf4dc website/docs: sys-mgmt/s3: Clean up and improve (cherry-pick #16242) (#16266)
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-08-20 13:24:51 +02:00
Tana M Berry
156cda6cb6 website/docs: update Advanced Queries docs and Rel Notes with new Int Guide (cherry-pick #16191) (#16258)
website/docs: Advanced queries, remove reference to QL and add more examples (#16191)

* remove reference to QL

* add Jens' examples

* tweak

* Update website/docs/users-sources/user/user_basic_operations.md




* Update website/docs/users-sources/user/user_basic_operations.md




* add note about UX ticks

* tweak

* argh

* clarify there are more values

* add link to Event actions list

* tweaks, typo

* Update website/docs/users-sources/user/user_basic_operations.md




* Update website/docs/sys-mgmt/events/logging-events.md




* jens edits

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-08-20 04:36:47 -05:00
authentik-automation[bot]
9f5125cf6b release: 2025.8.0-rc7 2025-08-18 18:44:42 +00:00
Marc 'risson' Schmitt
a84411363e web: Fix ak-flow-card footer alignment. (cherry-pick #16236) (#16238)
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Fix ak-flow-card footer alignment. (#16236)
2025-08-18 20:15:21 +02:00
Marc 'risson' Schmitt
9f3bb0210b brands: revert sort matched brand by match length (revert #15413) (cherry-pick #16233) (#16235) 2025-08-18 19:28:19 +02:00
Marc 'risson' Schmitt
d1271502ef web: bump API Client version (cherry-pick #16203) (#16226)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-08-18 16:42:35 +02:00
Marc 'risson' Schmitt
d1065b2d49 policies/password: Fix amount_uppercase in password policy check (cherry-pick #16197) (#16228)
Co-authored-by: M-Slanec <mcslanec@gmail.com>
Co-authored-by: Matthew Slanec <matthewslanec@Matthews-MacBook-Pro.local>
Fix amount_uppercase in password policy check (#16197)
2025-08-18 16:42:26 +02:00
Marc 'risson' Schmitt
aa19227e30 web/a11y: QL Search Input (cherry-pick #16198) (#16229)
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-08-18 16:41:50 +02:00
Marc 'risson' Schmitt
d7a2861bbe stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (cherry-pick #16196) (#16227)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-08-18 16:41:33 +02:00
Marc 'risson' Schmitt
5aae9c9afa core: Add email template selector (cherry-pick #16170) (#16225)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-08-18 16:41:22 +02:00
Tana M Berry
93cb48c928 website/docs: add content about new Advanced Query searches (cherry-pick #16019) (#16188)
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-08-14 18:32:05 +02:00
Marc 'risson' Schmitt
55f7f93a24 web/admin: fix settings saving (cherry-pick #16184) (#16187)
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-08-14 13:02:30 +00:00
authentik-automation[bot]
5a608a4235 release: 2025.8.0-rc6 2025-08-14 11:29:06 +00:00
Marc 'risson' Schmitt
77d023758f tasks: add sentry dramatiq integration (cherry-pick #16167) (#16183)
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-08-14 13:05:59 +02:00
Marc 'risson' Schmitt
f9edafd374 ci: release tag: fix missing env variables (cherry-pick #16172) (#16173) 2025-08-13 21:31:01 +02:00
Marc 'risson' Schmitt
d94219eb0e ci: release: consolidation bump version and on tag (cherry-pick #16164) (#16169)
Co-authored-by: Dominic R <dominic@sdko.org>
2025-08-13 18:22:11 +02:00
Marc 'risson' Schmitt
0871aa2cf3 ci: fix docker hub credentials (cherry-pick #16165) (#16166) 2025-08-13 15:20:03 +02:00
authentik-automation[bot]
e0fe99d0b8 release: 2025.8.0-rc5 2025-08-13 00:21:34 +00:00
Marc 'risson' Schmitt
c1acf53585 root: fix custom packages installation in docker (cherry-pick #16157) (#16158) 2025-08-13 02:08:56 +02:00
authentik-automation[bot]
baf4eed0d9 release: 2025.8.0-rc4 2025-08-12 22:26:57 +00:00
Marc 'risson' Schmitt
60e1192a7a ci: release publish: fix missing permissions (cherry-pick #16155) (#16156) 2025-08-13 00:20:52 +02:00
authentik-automation[bot]
2608e02d6e release: 2025.8.0-rc3 2025-08-12 21:49:51 +00:00
Marc 'risson' Schmitt
0a5928fbcb ci: docker push: fix version missing dash (cherry-pick #16153) (#16154) 2025-08-12 23:44:01 +02:00
authentik-automation[bot]
7b99b02b4a release: 2025.8.0-rc2 2025-08-12 21:12:07 +00:00
Marc 'risson' Schmitt
dfeadc9ebe root: fix custom packages installation in docker (cherry-pick #16150) (#16151) 2025-08-12 23:08:40 +02:00
authentik-automation[bot]
7ff64fbd09 release: 2025.8.0-rc1 2025-08-12 20:31:40 +00:00
52 changed files with 1424 additions and 562 deletions

View File

@@ -1,10 +1,11 @@
"""Helper script to get the actual branch name, docker safe"""
import os
from importlib.metadata import version as package_version
from json import dumps
from time import time
from authentik import authentik_version
# Decide if we should push the image or not
should_push = True
if len(os.environ.get("DOCKER_USERNAME", "")) < 1:
@@ -28,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 = package_version("authentik")
version = authentik_version()
# 2042.1
version_family = ".".join(version.split("-", 1)[0].split(".")[:-1])
prerelease = "-" in version

View File

@@ -49,7 +49,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: ${{ 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

View File

@@ -54,7 +54,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: ${{ inputs.image_name }}
merge-server:
@@ -74,15 +74,15 @@ jobs:
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

View File

@@ -81,7 +81,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

View File

@@ -278,7 +278,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-server
- name: Comment on PR

View File

@@ -90,7 +90,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

View File

@@ -1,168 +0,0 @@
---
name: Release - Bump version
on:
workflow_dispatch:
inputs:
version:
description: Version
required: true
type: string
release_reason:
description: Release reason
required: true
type: choice
options:
- bugfix
- feature
- security
- other
- prerelease
env:
POSTGRES_DB: authentik
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
jobs:
check-inputs:
name: Check inputs validity
runs-on: ubuntu-latest
steps:
- id: check
run: |
echo "${{ inputs.version }}" | grep -E "^[0-9]{4}\.[0-9]{1,2}\.[0-9]+(-rc[0-9]+)?$"
echo "major_version=${{ inputs.version }}" | grep -oE "^major_version=[0-9]{4}\.[0-9]{1,2}" >> "$GITHUB_OUTPUT"
outputs:
major_version: "${{ steps.check.outputs.major_version }}"
bump-authentik:
name: Bump authentik version
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 }}
- 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:
ref: "version-${{ needs.check-inputs.outputs.major_version }}"
token: "${{ steps.app-token.outputs.token }}"
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Run migrations
run: make migrate
- name: Bump version
run: "make bump version=${{ inputs.version }}"
- name: Commit and push
run: |
# ID from https://api.github.com/users/authentik-automation[bot]
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
git commit -a -m "release: ${{ inputs.version }}" --allow-empty
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
git push --follow-tags
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 feature 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 feature 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>"

View File

@@ -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,6 +24,7 @@ jobs:
build-docs:
runs-on: ubuntu-latest
permissions:
contents: read
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
@@ -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

View File

@@ -1,39 +1,195 @@
---
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:
- id: check
run: |
echo "${{ inputs.version }}" | grep -E '^[0-9]{4}\.(0?[1-9]|1[0-2])\.[0-9]+(-rc[0-9]+)?$'
echo "major_version=${{ inputs.version }}" | grep -oE "^major_version=[0-9]{4}\.[0-9]{1,2}" >> "$GITHUB_OUTPUT"
- id: changelog-url
run: |
if [ "${{ inputs.release_reason }}" = "feature" ] || [ "${{ inputs.release_reason }}" = "prerelease" ]; then
changelog_url="https://docs.goauthentik.io/docs/releases/${{ steps.check.outputs.major_version }}"
else
changelog_url="https://docs.goauthentik.io/docs/releases/${{ steps.check.outputs.major_version }}#fixed-in-$(echo -n ${{ inputs.version }} | sed 's/\.//g')"
fi
echo "changelog_url=${changelog_url}" >> "$GITHUB_OUTPUT"
outputs:
major_version: "${{ steps.check.outputs.major_version }}"
changelog_url: "${{ steps.changelog-url.outputs.changelog_url }}"
test:
name: Pre-release test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Pre-release test
- 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:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
- uses: actions/checkout@v5
with:
ref: "version-${{ needs.check-inputs.outputs.major_version }}"
token: "${{ steps.app-token.outputs.token }}"
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Run migrations
run: make migrate
- name: Bump version
run: "make bump version=${{ inputs.version }}"
- name: Commit and push
run: |
make test-docker
- id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/server
# ID from https://api.github.com/users/authentik-automation[bot]
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
git commit -a -m "release: ${{ inputs.version }}" --allow-empty
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
git push --follow-tags
- 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>"

View File

@@ -175,6 +175,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/

View File

@@ -20,8 +20,8 @@ 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

View File

@@ -3,7 +3,7 @@
from functools import lru_cache
from os import environ
VERSION = "2025.8.0-rc1"
VERSION = "2025.8.0"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@@ -63,28 +63,6 @@ class TestBrands(APITestCase):
},
)
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,
},
)
def test_fallback(self):
"""Test fallback brand"""
Brand.objects.all().delete()

View File

@@ -4,7 +4,6 @@ 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
@@ -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:

View File

@@ -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",
]

View File

@@ -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"),
),
]

View File

@@ -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"""
@@ -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)

View File

@@ -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 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):
@@ -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"]])

View File

@@ -22,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
@@ -109,11 +110,12 @@ 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,

View File

@@ -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:

View File

@@ -15,18 +15,10 @@ from authentik.lib.config import CONFIG
from authentik.lib.models import SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
from authentik.stages.authenticator.models import SideChannelDevice
from authentik.stages.email.models import EmailTemplates
from authentik.stages.email.utils import TemplateEmailMessage
class EmailTemplates(models.TextChoices):
"""Templates used for rendering the Email"""
EMAIL_OTP = (
"email/email_otp.html",
_("Email OTP"),
) # nosec
class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage):
"""Use Email-based authentication instead of authenticator-based."""

File diff suppressed because one or more lines are too long

View File

@@ -32,6 +32,14 @@ class EmailTemplates(models.TextChoices):
"email/account_confirmation.html",
_("Account Confirmation"),
)
EMAIL_OTP = (
"email/email_otp.html",
_("Email OTP"),
) # nosec
EVENT_NOTIFICATION = (
"email/event_notification.html",
_("Event Notification"),
)
def get_template_choices():

View File

@@ -54,7 +54,7 @@ class TestEmailStageTemplates(FlowTestCase):
chmod(file2, 0o000) # Remove all permissions so we can't read the file
choices = get_template_choices()
self.assertEqual(choices[-1][0], Path(file).name)
self.assertEqual(len(choices), 3)
self.assertEqual(len(choices), 5)
unlink(file)
unlink(file2)

View File

@@ -100,10 +100,15 @@ class MessagesMiddleware(Middleware):
TaskStatus.ERROR,
exception,
)
event_kwargs = {
"actor": task.actor_name,
}
if task.rel_obj:
event_kwargs["rel_obj"] = task.rel_obj
Event.new(
EventAction.SYSTEM_TASK_EXCEPTION,
message=f"Task {task.actor_name} encountered an error",
actor=task.actor_name,
**event_kwargs,
).with_exception(exception).save()
def after_skip_message(self, broker: Broker, message: Message):
@@ -151,7 +156,6 @@ class DescriptionMiddleware(Middleware):
class _healthcheck_handler(BaseHTTPRequestHandler):
def log_request(self, code="-", size="-"):
HEALTHCHECK_LOGGER.info(
self.path,

View File

@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2025.8.0-rc1 Blueprint schema",
"title": "authentik 2025.8.0 Blueprint schema",
"required": [
"version",
"entries"
@@ -6753,6 +6753,15 @@
"title": "Webhook mapping headers",
"description": "Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs"
},
"email_subject_prefix": {
"type": "string",
"title": "Email subject prefix"
},
"email_template": {
"type": "string",
"minLength": 1,
"title": "Email template"
},
"send_once": {
"type": "boolean",
"title": "Send once",

View File

@@ -48,7 +48,7 @@ services:
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.0-rc1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.0}
ports:
- ${COMPOSE_PORT_HTTP:-9000}:9000
- ${COMPOSE_PORT_HTTPS:-9443}:9443
@@ -72,7 +72,7 @@ services:
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.0-rc1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.0}
restart: unless-stopped
user: root
volumes:

View File

@@ -1 +1 @@
2025.8.0-rc1
2025.8.0

View File

@@ -26,7 +26,7 @@ Parameters:
Description: authentik Docker image
AuthentikVersion:
Type: String
Default: 2025.8.0-rc1
Default: 2025.8.0
Description: authentik Docker image tag
AuthentikServerCPU:
Type: Number

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/authentik",
"version": "2025.8.0-rc1",
"version": "2025.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/authentik",
"version": "2025.8.0-rc1",
"version": "2025.8.0",
"dependencies": {
"@eslint/js": "^9.31.0",
"@typescript-eslint/eslint-plugin": "^8.38.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@goauthentik/authentik",
"version": "2025.8.0-rc1",
"version": "2025.8.0",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
[project]
name = "authentik"
version = "2025.8.0-rc1"
version = "2025.8.0"
description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.13.*"

View File

@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2025.8.0-rc1
version: 2025.8.0
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@@ -6057,7 +6057,8 @@ paths:
/core/users/{id}/recovery_email/:
post:
operationId: core_users_recovery_email_create
description: Send an email with a temporary link that a user can use to recover their account
description: Send an email with a temporary link that a user can use to recover
their account
parameters:
- in: query
name: email_stage
@@ -49344,6 +49345,10 @@ components:
nullable: true
description: Configure additional headers to be sent. Mapping should return
a dictionary of key-value pairs
email_subject_prefix:
type: string
email_template:
type: string
send_once:
type: boolean
description: Only send notification once, for example when sending a webhook
@@ -49383,6 +49388,11 @@ components:
nullable: true
description: Configure additional headers to be sent. Mapping should return
a dictionary of key-value pairs
email_subject_prefix:
type: string
email_template:
type: string
minLength: 1
send_once:
type: boolean
description: Only send notification once, for example when sending a webhook
@@ -54480,6 +54490,11 @@ components:
nullable: true
description: Configure additional headers to be sent. Mapping should return
a dictionary of key-value pairs
email_subject_prefix:
type: string
email_template:
type: string
minLength: 1
send_once:
type: boolean
description: Only send notification once, for example when sending a webhook

4
uv.lock generated
View File

@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = "==3.13.*"
[manifest]
@@ -159,7 +159,7 @@ wheels = [
[[package]]
name = "authentik"
version = "2025.8.0rc1"
version = "2025.8.0"
source = { editable = "." }
dependencies = [
{ name = "argon2-cffi" },

12
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/web",
"version": "2025.8.0-rc1",
"version": "2025.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/web",
"version": "2025.8.0-rc1",
"version": "2025.8.0",
"license": "MIT",
"workspaces": [
"./packages/*"
@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.7.3",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^7.0.0",
"@goauthentik/api": "^2025.8.0-rc1-1755026430",
"@goauthentik/api": "^2025.10.0-rc1-1755254677",
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
"@goauthentik/eslint-config": "^1.0.5",
@@ -1509,9 +1509,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2025.8.0-rc1-1755026430",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.8.0-rc1-1755026430.tgz",
"integrity": "sha512-nkXlhnI8ILnpnSqseklRHRWsq9kBCT0CPU/7MotO3kQwl54Ze4j8HAMGNJNLr1VjuKuG5m/qt6SQseXLnazb+w=="
"version": "2025.10.0-rc1-1755254677",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.10.0-rc1-1755254677.tgz",
"integrity": "sha512-hq+xGPtwaeptEDn92Y40Yb4e7yL2KVvuqy2kWAZLPtr/r9ML82vzNYCfW6bFNPnopDRizjOBIzlD3gNP/2rs8Q=="
},
"node_modules/@goauthentik/core": {
"resolved": "packages/core",

View File

@@ -1,6 +1,6 @@
{
"name": "@goauthentik/web",
"version": "2025.8.0-rc1",
"version": "2025.8.0",
"license": "MIT",
"private": true,
"scripts": {
@@ -94,7 +94,7 @@
"@floating-ui/dom": "^1.7.3",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^7.0.0",
"@goauthentik/api": "^2025.8.0-rc1-1755026430",
"@goauthentik/api": "^2025.10.0-rc1-1755254677",
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
"@goauthentik/eslint-config": "^1.0.5",

View File

@@ -8,15 +8,19 @@ import "#elements/forms/Radio";
import "#elements/forms/SearchSelect/index";
import "#elements/utils/TimeDeltaHelp";
import "./AdminSettingsFooterLinks.js";
import "#elements/CodeMirror";
import { akFooterLinkInput, IFooterLinkInput } from "./AdminSettingsFooterLinks.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { CodeMirrorMode } from "#elements/CodeMirror";
import { Form } from "#elements/forms/Form";
import { AdminApi, FooterLink, Settings, SettingsRequest } from "@goauthentik/api";
import YAML from "yaml";
import { msg } from "@lit/localize";
import { css, CSSResult, html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
@@ -254,6 +258,16 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
value="${this._settings?.defaultTokenLength ?? 60}"
help=${msg("Default length of generated tokens")}
></ak-number-input>
<ak-form-element-horizontal label=${msg("Flags")} name="flags" required>
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(this._settings?.flags ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Modify flags to opt into new authentik behaviours early.")}
</p>
</ak-form-element-horizontal>
`;
}
}

View File

@@ -14,6 +14,8 @@ import {
NotificationWebhookMapping,
PropertymappingsApi,
PropertymappingsNotificationListRequest,
StagesApi,
TypeCreate,
} from "@goauthentik/api";
import { msg } from "@lit/localize";
@@ -33,10 +35,18 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
return transport;
});
}
async load(): Promise<void> {
this.templates = await new StagesApi(DEFAULT_CONFIG).stagesEmailTemplatesList();
}
templates?: TypeCreate[];
@property({ type: Boolean })
showWebhook = false;
@property({ type: Boolean })
showEmail = false;
getSuccessMessage(): string {
return this.instance
? msg("Successfully updated transport.")
@@ -56,18 +66,28 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
}
onModeChange(mode: string | undefined): void {
if (
mode === NotificationTransportModeEnum.Webhook ||
mode === NotificationTransportModeEnum.WebhookSlack
) {
this.showWebhook = true;
} else {
this.showWebhook = false;
// Reset all flags
this.showWebhook = false;
this.showEmail = false;
switch (mode) {
case NotificationTransportModeEnum.Webhook:
case NotificationTransportModeEnum.WebhookSlack:
this.showWebhook = true;
break;
case NotificationTransportModeEnum.Email:
this.showEmail = true;
break;
case NotificationTransportModeEnum.Local:
default:
// Both flags remain false
break;
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} required name="name">
return html`
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
@@ -75,6 +95,26 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="sendOnce">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${this.instance?.sendOnce ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Send once")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"Only send notification once, for example when sending a webhook into a chat channel.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<ak-radio
@change=${(ev: CustomEvent<{ value: NotificationTransportModeEnum }>) => {
@@ -109,7 +149,7 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
value="${this.instance?.webhookUrl || ""}"
input-hint="code"
?hidden=${!this.showWebhook}
required
?required=${this.showWebhook}
>
</ak-hidden-text-input>
<ak-form-element-horizontal
@@ -178,26 +218,39 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
>
</ak-search-select>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="sendOnce">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${this.instance?.sendOnce ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Send once")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"Only send notification once, for example when sending a webhook into a chat channel.",
)}
</p>
</ak-form-element-horizontal>`;
<ak-form-element-horizontal
?hidden=${!this.showEmail}
?required=${this.showEmail}
label=${msg("Email Subject Prefix")}
name="emailSubjectPrefix"
>
<input
type="text"
value="${this.instance?.emailSubjectPrefix || "authentik Notification: "}"
class="pf-c-form-control"
?hidden=${!this.showEmail}
?required=${this.showEmail}
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
?hidden=${!this.showEmail}
?required=${this.showEmail}
label=${msg("Email Template")}
name="emailTemplate"
>
<select name="users" class="pf-c-form-control">
${this.templates?.map((template) => {
const selected =
this.instance?.emailTemplate === template.name ||
(!this.instance?.emailTemplate &&
template.name === "email/event_notification.html");
return html`<option value=${ifDefined(template.name)} ?selected=${selected}>
${template.description}
</option>`;
})}
</select>
</ak-form-element-horizontal>
`;
}
}

View File

@@ -75,7 +75,6 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
type="text"
value="${ifDefined(this.instance?.staticSettings.username)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
@@ -86,7 +85,6 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
type="password"
value="${ifDefined(this.instance?.staticSettings.password)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
</div>
@@ -137,11 +135,7 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
</ak-form-group>
<ak-form-group label="${msg("Advanced settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Expression")}
required
name="expression"
>
<ak-form-element-horizontal label=${msg("Expression")} name="expression">
<ak-codemirror
mode=${CodeMirrorMode.Python}
value="${ifDefined(this.instance?.expression)}"

View File

@@ -16,6 +16,7 @@ import {
FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest,
StagesApi,
TypeCreate,
} from "@goauthentik/api";
import { msg } from "@lit/localize";
@@ -33,6 +34,12 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
return stage;
}
async load(): Promise<void> {
this.templates = await new StagesApi(DEFAULT_CONFIG).stagesEmailTemplatesList();
}
templates?: TypeCreate[];
@property({ type: Boolean })
showConnectionSettings = false;
@@ -262,6 +269,28 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Template")} name="template">
<select
class="pf-c-form-control"
?disabled=${!this.templates || this.templates.length === 0}
>
${this.templates && this.templates.length > 0
? this.templates.map((template: TypeCreate) => {
return html`<option
value="${template.name}"
?selected=${this.instance?.template === template.name ||
(!this.instance?.template &&
template.name === "email/email_otp.html")}
>
${template.description}
</option>`;
})
: html`<option value="">${msg("Loading templates...")}</option>`}
</select>
<p class="pf-c-form__helper-text">
${msg("Template used for the verification email.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
}

View File

@@ -20,6 +20,22 @@ export const EscapeTrustPolicy = trustedTypes.createPolicy("authentik-escape", {
},
});
/**
* Trusted types policy that removes all HTML content.
*
*
* @returns {TrustedHTML} All remaining text content.
*/
export const StripHTMLTrustPolicy = trustedTypes.createPolicy("authentik-strip-html", {
createHTML: (untrustedHTML: string) => {
return DOMPurify.sanitize(untrustedHTML, {
RETURN_TRUSTED_TYPE: false,
ALLOWED_TAGS: [],
ALLOWED_ATTR: [],
});
},
});
/**
* Trusted types policy, stripping all HTML content.
*

View File

@@ -1,14 +1,18 @@
import "#elements/buttons/Dropdown";
import { AKElement } from "#elements/Base";
import { StripHTMLTrustPolicy } from "#common/purify";
import { rootInterface } from "#common/theme";
import { FormAssociated, FormAssociatedElement } from "#elements/forms/form-associated-element";
import { PaginatedResponse } from "#elements/table/Table";
import DjangoQL, { Introspections } from "@mrmarble/djangoql-completion";
import { msg } from "@lit/localize";
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { css, CSSResult, html, LitElement, nothing, PropertyValues, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { createRef, ref, Ref } from "lit/directives/ref.js";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFSearchInput from "@patternfly/patternfly/components/SearchInput/search-input.css";
@@ -25,40 +29,26 @@ export class QL extends DjangoQL {
textareaResize() {}
}
@customElement("ak-search-ql")
export class QLSearch extends AKElement {
@property()
value?: string;
/**
* Given an array or length, return logical index of the element at the given delta.
* This is effectively a modulo loop, allowing for positive and negative deltas.
*/
function torusIndex(lengthLike: number | ArrayLike<number>, delta: number): number {
const length = typeof lengthLike === "number" ? lengthLike : lengthLike.length;
@query("[name=search]")
searchElement?: HTMLTextAreaElement;
@state()
menuOpen = false;
@property()
onSearch?: (value: string) => void;
@state()
selected?: number;
@state()
cursorX: number = 0;
@state()
cursorY: number = 0;
ql?: QL;
canvas?: CanvasRenderingContext2D;
set apiResponse(value: PaginatedResponse<unknown> | undefined) {
if (!value || !value.autocomplete || !this.ql) {
return;
}
this.ql.loadIntrospections(value.autocomplete as unknown as Introspections);
if (delta < 0) {
return (length + delta) % length;
}
static styles: CSSResult[] = [
return ((delta % length) + length) % length;
}
@customElement("ak-search-ql")
export class QLSearch extends FormAssociatedElement<string> implements FormAssociated {
declare anchorRef: Ref<HTMLTextAreaElement>;
declare anchor: HTMLTextAreaElement | null;
public static styles: CSSResult[] = [
PFBase,
PFFormControl,
PFSearchInput,
@@ -66,207 +56,409 @@ export class QLSearch extends AKElement {
::-webkit-search-cancel-button {
display: none;
}
.ql.pf-c-form-control {
font-family: monospace;
--input-height: 2.25em;
height: var(--input-height);
min-height: var(--input-height);
max-height: calc(var(--input-height) * 6);
resize: vertical;
height: 2.25em;
}
.selected {
background-color: var(--pf-c-search-input__menu-item--hover--BackgroundColor);
}
:host([theme="dark"]) .pf-c-search-input__menu {
--pf-c-search-input__menu--BackgroundColor: var(--ak-dark-background-light-ish);
color: var(--ak-dark-foreground);
}
:host([theme="dark"]) .pf-c-search-input__menu-item {
--pf-c-search-input__menu-item--Color: var(--ak-dark-foreground);
}
:host([theme="dark"]) .pf-c-search-input__menu-item:hover {
--pf-c-search-input__menu-item--BackgroundColor: var(--ak-dark-background-lighter);
}
:host([theme="dark"]) .pf-c-search-input__menu-list-item.selected {
--pf-c-search-input__menu-item--hover--BackgroundColor: var(
--ak-dark-background-light
);
}
:host([theme="dark"]) .pf-c-search-input__text::before {
border: 0;
:host([theme="dark"]) {
.pf-c-search-input__menu {
--pf-c-search-input__menu--BackgroundColor: var(--ak-dark-background-light-ish);
color: var(--ak-dark-foreground);
}
.pf-c-search-input__menu-item {
--pf-c-search-input__menu-item--Color: var(--ak-dark-foreground);
}
.pf-c-search-input__menu-item:hover {
--pf-c-search-input__menu-item--BackgroundColor: var(
--ak-dark-background-lighter
);
}
.pf-c-search-input__menu-list-item.selected {
--pf-c-search-input__menu-item--hover--BackgroundColor: var(
--ak-dark-background-light
);
}
.pf-c-search-input__text::before {
border: 0;
}
}
.pf-c-search-input__menu {
position: fixed;
min-width: 0;
overflow-y: auto;
max-height: 50vh;
}
`,
];
firstUpdated() {
if (!this.searchElement) {
//#region Properties
@property({ type: Boolean })
public open = false;
@property({ type: Number, attribute: false })
public selectionIndex = -1;
#value = "";
@property({ type: String })
public get value(): string {
return this.#value;
}
public set value(value: unknown) {
const parsed = typeof value === "string" ? value : "";
this.setFormValue(parsed.trim(), parsed);
this.#value = parsed;
if (this.anchor) {
this.anchor.value = this.#value;
}
}
//#endregion
//#region State
#menuRef = createRef<HTMLDivElement>();
#ql: QL | null = null;
#ctx: OffscreenCanvasRenderingContext2D | null = null;
#letterWidth = -1;
#scrollContainer: HTMLElement | null = null;
public set apiResponse(value: PaginatedResponse<unknown> | undefined) {
if (!value?.autocomplete || !this.#ql) {
return;
}
this.ql = new QL({
this.#ql.loadIntrospections(value.autocomplete as unknown as Introspections);
}
//#endregion
//#region Lifecycle
public override connectedCallback() {
super.connectedCallback();
this.internals.ariaAutoComplete = "list";
this.internals.role = "combobox";
this.internals.ariaHasPopup = "listbox";
this.#scrollContainer =
rootInterface<LitElement>().renderRoot.querySelector("#main-content");
this.#scrollContainer?.addEventListener("scroll", this.#updateDropdownPosition, {
passive: true,
});
this.tabIndex = 0;
}
public override disconnectedCallback() {
super.disconnectedCallback();
this.#scrollContainer?.removeEventListener("scroll", this.#updateDropdownPosition);
}
public formStateRestoreCallback(state: string) {
this.value = state;
}
public formResetCallback() {
this.value = "";
}
public toJSON() {
return this.value;
}
public override updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("open")) {
this.internals.ariaExpanded = this.open ? "true" : "false";
}
if (changedProperties.has("selectionIndex")) {
const id = `suggestion-${this.selectionIndex}`;
this.setAttribute("aria-activedescendant", this.selectionIndex === -1 ? "" : id);
this.renderRoot.querySelector(`#${id}`)?.scrollIntoView({
behavior: "auto",
block: "nearest",
});
}
}
public override firstUpdated() {
const textarea = this.anchorRef.value;
if (!textarea) return;
this.#ql = new QL({
completionEnabled: true,
introspections: {
current_model: "",
models: {},
},
selector: this.searchElement,
selector: textarea,
autoResize: false,
});
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!context) {
const canvas = new OffscreenCanvas(300, 150);
this.#ctx = canvas.getContext("2d");
if (!this.#ctx) {
console.error("authentik/ql: failed to get canvas context");
return;
}
context.font = window.getComputedStyle(this.searchElement).font;
this.canvas = context;
}
refreshCompletions() {
this.value = this.searchElement?.value;
if (!this.ql) {
return;
}
this.ql.generateSuggestions();
if (this.ql.suggestions.length < 1 || this.ql.loading) {
this.menuOpen = false;
return;
}
this.menuOpen = true;
this.updateDropdownPosition();
this.requestUpdate();
}
this.#ctx.font = window.getComputedStyle(textarea).font;
updateDropdownPosition() {
if (!this.searchElement) {
return;
}
const bcr = this.getBoundingClientRect();
// We need the width of a letter to measure x; we use a monospaced font but still
// check the length for `m` as its the widest ASCII char
const metrics = this.canvas?.measureText("m");
const letterWidth = Math.ceil(metrics?.width || 0) + 1;
const metrics = this.#ctx?.measureText("m");
this.#letterWidth = Math.ceil(metrics?.width || 0) + 1;
}
//#endregion
//#region Completions
#refreshCompletions = () => {
if (this.anchor) {
this.value = this.anchor.value;
}
if (!this.#ql) {
return;
}
this.#ql.generateSuggestions();
if (this.#ql.suggestions.length < 1 || this.#ql.loading) {
this.open = false;
return;
}
this.open = true;
this.requestUpdate();
requestAnimationFrame(this.#updateDropdownPosition);
};
#updateDropdownPosition = () => {
const anchor = this.anchorRef.value;
const menu = this.#menuRef.value;
if (!anchor || !menu) return;
const bcr = this.getBoundingClientRect();
const style = window.getComputedStyle(anchor);
// Mostly static variables for padding, font line-height and how many
const lineHeight = parseInt(window.getComputedStyle(this.searchElement).lineHeight, 10);
const paddingTop = parseInt(window.getComputedStyle(this.searchElement).paddingTop, 10);
const paddingLeft = parseInt(window.getComputedStyle(this.searchElement).paddingLeft, 10);
const paddingRight = parseInt(window.getComputedStyle(this.searchElement).paddingRight, 10);
const lineHeight = parseInt(style.lineHeight, 10);
const paddingTop = parseInt(style.paddingTop, 10);
const paddingLeft = parseInt(style.paddingLeft, 10);
const paddingRight = parseInt(style.paddingRight, 10);
const actualInnerWidth = bcr.width - paddingLeft - paddingRight;
let relX = 0;
let relY = 1;
let letterIndex = 0;
this.searchElement.value.split(" ").some((word, idx) => {
for (const word of anchor.value.split(" ")) {
letterIndex += word.length;
const newRelX = relX + word.length * letterWidth;
const newRelX = relX + word.length * this.#letterWidth;
if (newRelX > actualInnerWidth) {
relY += 1;
if (letterIndex > this.searchElement!.selectionStart) {
if (letterIndex > anchor.selectionStart) {
relX =
letterWidth * word.length -
(letterIndex - this.searchElement!.selectionStart) * letterWidth;
return true;
this.#letterWidth * word.length -
(letterIndex - anchor.selectionStart) * this.#letterWidth;
break;
}
relX = word.length * letterWidth;
relX = word.length * this.#letterWidth;
} else {
relX = newRelX + 1;
}
});
}
this.cursorX = bcr.x + paddingLeft + relX;
this.cursorY = bcr.y + paddingTop + relY * lineHeight;
}
const x = bcr.x + paddingLeft + relX;
const y = bcr.y + paddingTop + relY * lineHeight;
Object.assign(menu.style, {
left: `${x}px`,
top: `${y}px`,
} satisfies Partial<CSSStyleDeclaration>);
};
//#endregion
//#region Event Listeners
#keydownListener = (event: KeyboardEvent) => {
this.#updateDropdownPosition();
const suggestionsLength = this.#ql?.suggestions.length;
if (event.key === "Enter" && !this.open && this.form) {
const submitEvent = new SubmitEvent("submit", {
submitter: this,
bubbles: true,
composed: true,
cancelable: true,
});
this.form.dispatchEvent(submitEvent);
onKeyDown(ev: KeyboardEvent) {
this.updateDropdownPosition();
if (ev.key === "Enter" && ev.metaKey && this.onSearch && this.searchElement) {
this.onSearch(this.searchElement?.value);
return;
}
if (!this.menuOpen) return;
switch (ev.key) {
if (event.key === "ArrowDown") {
event.preventDefault();
if (this.open && suggestionsLength) {
if (this.selectionIndex === -1) {
this.selectionIndex = 0;
} else {
this.selectionIndex = torusIndex(suggestionsLength, this.selectionIndex + 1);
}
this.#refreshCompletions();
return;
}
this.selectionIndex = 0;
this.#refreshCompletions();
return;
}
if (!this.open) return;
switch (event.key) {
case "ArrowUp":
if (this.ql?.suggestions.length) {
if (this.selected === undefined) {
this.selected = this.ql?.suggestions.length - 1;
} else if (this.selected === 0) {
this.selected = undefined;
if (suggestionsLength) {
if (this.selectionIndex === -1) {
this.selectionIndex = suggestionsLength - 1;
} else {
this.selected -= 1;
this.selectionIndex = torusIndex(
suggestionsLength,
this.selectionIndex - 1,
);
}
this.refreshCompletions();
ev.preventDefault();
this.#refreshCompletions();
event.preventDefault();
}
break;
case "ArrowDown":
if (this.ql?.suggestions.length) {
if (this.selected === undefined) {
this.selected = 0;
} else if (this.selected < this.ql?.suggestions.length - 1) {
this.selected += 1;
} else {
this.selected = undefined;
}
this.refreshCompletions();
ev.preventDefault();
}
break;
return;
case "Tab":
if (this.selected) {
this.ql?.selectCompletion(this.selected);
ev.preventDefault();
if (this.selectionIndex) {
this.#ql?.selectCompletion(this.selectionIndex);
event.preventDefault();
}
break;
return;
case "Enter":
// Technically this is a textarea, due to automatic multi-line feature,
// but other than that it should look and behave like a normal input.
// So expected behavior when pressing Enter is to submit the form,
// not to add a new line.
if (this.selected !== undefined) {
this.ql?.selectCompletion(this.selected);
if (this.selectionIndex !== -1) {
this.#ql?.selectCompletion(this.selectionIndex);
this.selectionIndex = 0;
}
ev.preventDefault();
break;
case "Escape":
this.menuOpen = false;
break;
case "Shift": // Shift
case "Control": // Ctrl
case "Alt": // Alt
case "Meta": // Windows Key or Cmd on Mac
// Control keys shouldn't trigger completion popup
break;
}
}
renderMenu() {
if (!this.menuOpen || !this.ql) {
event.preventDefault();
return;
case "Escape":
this.open = false;
return;
}
};
#blurListener = ({ relatedTarget }: FocusEvent) => {
if (relatedTarget instanceof Node && this.renderRoot.contains(relatedTarget)) {
return;
}
this.open = false;
};
#focusListener = () => {
this.selectionIndex = this.selectionIndex === -1 ? 0 : this.selectionIndex;
this.#refreshCompletions();
};
//#endregion
//#region Render
protected renderMenu() {
if (!this.open || !this.#ql) {
return nothing;
}
return html`
<div
class="pf-c-search-input__menu"
style="left: ${this.cursorX}px; top: ${this.cursorY}px;"
>
<ul class="pf-c-search-input__menu-list">
${this.ql.suggestions.map((suggestion, idx) => {
<div ${ref(this.#menuRef)} class="pf-c-search-input__menu">
<ul
class="pf-c-search-input__menu-list"
role="listbox"
id="ql-suggestions"
aria-label=${msg("Query suggestions")}
>
${this.#ql.suggestions.map((suggestion, idx) => {
// Cast to string to sooth Lit Analyzer's primitive type rule.
const label = `${StripHTMLTrustPolicy.createHTML(suggestion.suggestionText)}`;
return html`<li
class="pf-c-search-input__menu-list-item ${this.selected === idx
role="option"
id="suggestion-${idx}"
aria-selected=${this.selectionIndex === idx ? "true" : "false"}
class="pf-c-search-input__menu-list-item ${this.selectionIndex === idx
? "selected"
: ""}"
>
<button
class="pf-c-search-input__menu-item"
type="button"
aria-label=${label}
@click=${() => {
this.ql?.selectCompletion(idx);
this.refreshCompletions();
this.#ql?.selectCompletion(idx);
this.#refreshCompletions();
}}
>
<span class="pf-c-search-input__menu-item-text"
>${suggestion.text}</span
<span class="pf-c-search-input__menu-item-text pf-m-monospace">
${suggestion.text}</span
>
</button>
</li>`;
@@ -276,25 +468,33 @@ export class QLSearch extends AKElement {
`;
}
render(): TemplateResult {
public override render(): TemplateResult {
return html`<div class="pf-c-search-input">
<div class="pf-c-search-input__bar">
<span class="pf-c-search-input__text">
<textarea
class="pf-c-form-control ql"
${ref(this.anchorRef)}
class="pf-c-form-control pf-m-monospace ql"
name="search"
autocomplete="off"
aria-controls="ql-suggestions"
?required=${this.required}
placeholder=${msg("Search...")}
spellcheck="false"
@input=${(ev: InputEvent) => this.refreshCompletions()}
@keydown=${this.onKeyDown}
@input=${this.#refreshCompletions}
@focus=${this.#focusListener}
@blur=${this.#blurListener}
@keydown=${this.#keydownListener}
>
${ifDefined(this.value)}</textarea
${ifDefined(this.#value)}</textarea
>
</span>
</div>
${this.renderMenu()}
</div>`;
}
//#endregion
}
declare global {

View File

@@ -0,0 +1,216 @@
import { AKElement } from "#elements/Base";
import { Jsonifiable } from "type-fest";
import { msg } from "@lit/localize";
import { LitElement } from "lit";
import { property } from "lit/decorators.js";
import { createRef, Ref } from "lit/directives/ref.js";
/**
* A subset of form associated {@linkcode ElementInternals} properties.
*
* @see {@linkcode FormAssociatedElement} for usage.
*/
export interface FormAssociated
extends Pick<
ElementInternals,
| "form"
| "validity"
| "validationMessage"
| "willValidate"
| "labels"
| "checkValidity"
| "reportValidity"
> {
/**
* The name of the input, provided to the form.
*/
readonly name: string | null;
/**
* The type of the input, provided to the form.
*/
readonly type: string;
/**
* Whether or not the input is required.
*/
required?: boolean;
/**
* Whether or not the input is read-only.
*/
readonly?: boolean;
/**
* A JSON representation of the value.
*/
toJSON(): Jsonifiable;
}
export type FormValue = File | string | FormData | null;
/**
* A base element which provides reactive properties and methods for interacting with a parent form.
*
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals | MDN}
*/
export abstract class FormAssociatedElement<
V extends FormValue = string,
T extends Jsonifiable = V extends string ? V : Jsonifiable,
S extends FormValue = V,
>
extends AKElement
implements FormAssociated
{
static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true };
public static readonly formAssociated = true;
/**
* The internals of the element.
*
* @protected
* @see {@linkcode FormAssociated}
*/
protected internals = this.attachInternals();
//#region Reactive Properties
@property({ type: Boolean })
public get required() {
return this.internals.ariaRequired === "true";
}
public set required(value: boolean) {
this.internals.ariaRequired = value ? "true" : "false";
}
@property({ type: Boolean, attribute: "readonly" })
public get readOnly() {
return this.internals.ariaReadOnly === "true";
}
public set readOnly(value: boolean) {
this.internals.ariaReadOnly = value ? "true" : "false";
}
@property({ type: Boolean })
public get disabled() {
return this.internals.ariaDisabled === "true";
}
public set disabled(value: boolean) {
this.internals.ariaDisabled = value ? "true" : "false";
}
//#endregion
//#region Aliased Properties
public get form(): HTMLFormElement | null {
return this.internals.form;
}
public get name() {
return this.getAttribute("name");
}
public get type() {
return this.localName;
}
public get validity() {
return this.internals.validity;
}
public get validationMessage() {
return this.internals.validationMessage;
}
public get willValidate() {
return this.internals.willValidate;
}
public get labels() {
return this.internals.labels;
}
//#endregion
//#region Values
/**
* A reference to an element that is focusable when validation fails.
*/
protected anchorRef: Ref<HTMLElement>;
/**
* The element that is focusable when validation fails.
*/
declare protected anchor: HTMLElement | null;
/**
* Set the value of the form.
*
* @param value The value visible to the form during submission.
* @param state The value as provided by the user.
*/
protected setFormValue(value: V, state?: S) {
this.internals.setFormValue(value, state);
if (this.required) {
if (value) {
this.internals.setValidity({});
} else {
this.internals.setValidity(
{
valueMissing: true,
},
msg("This field is required."),
this.anchorRef.value,
);
}
}
}
public abstract toJSON(): T;
//#endregion
//#region Validation
public checkValidity = this.internals.checkValidity.bind(this.internals);
public reportValidity = this.internals.reportValidity.bind(this.internals);
//#endregion
/**
* Set the validity state of the form.
*
* @param flags The validity state flags.
* @param message The validation message.
* @param element The element to set the validity state on.
*/
protected setValidity(flags: ValidityStateFlags = {}, message?: string, element?: HTMLElement) {
this.internals.setValidity(flags, message, element ?? this.anchorRef.value);
}
//#endregion
public constructor() {
super();
this.anchorRef = createRef<HTMLElement>();
// We define the getter here to allow the base type to be extended,
// letting the subclasses define a more accurate HTMLElement type.
Object.defineProperty(this, "anchor", {
get() {
return this.anchorRef.value || null;
},
enumerable: true,
configurable: true,
});
}
}

41
web/src/elements/forms/types.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
/**
* @file Type definitions for form-associated elements.
*
* While these types are part of the HTML standard, they're not yet defined
* in the TypeScript standard library, so we define them here.
*
* @expires 2026-01-01
*/
/**
* Callbacks for form-associated elements.
*/
interface HTMLElement {
/**
* A callback invoked when the browser autofilling sets a value.
*/
formStateRestoreCallback?(state: FormValue, mode: "autocomplete"): void;
/**
* A callback invoked when the browser restores a value from a previous session.
*/
formStateRestoreCallback?(state: FormValue, mode: "restore"): void;
/**
* A callback invoked when the browser restores a value from a previous session.
*/
formStateRestoreCallback?(state: FormValue, mode: "restore" | "autocomplete"): void;
/**
* A callback that is invoked when the form is reset.
*/
formResetCallback?(): void;
/**
* A callback that is invoked when the element's disabled state changes.
*/
formDisabledCallback?(disabled: boolean): void;
/**
* A callback that is invoked when the element is associated with a form.
*/
formAssociatedCallback?(form: HTMLFormElement): void;
}

View File

@@ -124,8 +124,8 @@ export abstract class Table<T extends object>
@property({ type: String })
public order?: string;
@property({ type: String })
public search: string = "";
@property({ type: String, attribute: false })
public search?: string;
@property({ type: Boolean })
public checkbox = false;
@@ -547,11 +547,11 @@ export abstract class Table<T extends object>
return html`<div class="pf-c-toolbar__group pf-m-search-filter ${isQL ? "ql" : ""}">
<ak-table-search
class="pf-c-toolbar__item pf-m-search-filter ${isQL ? "ql" : ""}"
value=${ifDefined(this.search)}
.defaultValue=${this.search}
label=${ifDefined(this.searchLabel)}
placeholder=${ifDefined(this.searchPlaceholder)}
.onSearch=${this.#searchListener}
?supportsQL=${this.supportsQL}
.supportsQL=${this.supportsQL}
.apiResponse=${this.data}
>
</ak-table-search>

View File

@@ -8,6 +8,7 @@ import { msg } from "@lit/localize";
import { css, CSSResult, html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { createRef, ref } from "lit/directives/ref.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@@ -16,17 +17,23 @@ import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-table-search")
export class TableSearch extends WithLicenseSummary(AKElement) {
@property()
public value?: string;
export class TableSearchForm extends WithLicenseSummary(AKElement) {
@property({ type: String, reflect: false })
public defaultValue?: string;
@property({ type: Boolean })
@property({ type: String })
public label = msg("Table Search");
@property({ type: String })
public placeholder = msg("Search...");
@property({ attribute: false })
public supportsQL: boolean = false;
@property({ attribute: false })
public apiResponse?: PaginatedResponse<unknown>;
@property()
@property({ attribute: false })
public onSearch?: (value: string) => void;
static styles: CSSResult[] = [
@@ -45,25 +52,26 @@ export class TableSearch extends WithLicenseSummary(AKElement) {
`,
];
public reset = () => {
if (!this.onSearch) return;
this.value = "";
this.onSearch("");
#formRef = createRef<HTMLFormElement>();
public reset = (): void => {
this.#formRef.value?.reset();
this.onSearch?.("");
};
#submitListener = (event: SubmitEvent) => {
event.preventDefault();
if (!this.onSearch) return;
const form = this.#formRef.value;
if (!form || !this.onSearch) return;
form.reportValidity();
const form = event.target as HTMLFormElement;
const data = new FormData(form);
const value = data.get("search")?.toString().trim();
if (!value) {
return;
}
const value = data.get("search")?.toString() ?? "";
this.onSearch(value);
};
@@ -71,27 +79,31 @@ export class TableSearch extends WithLicenseSummary(AKElement) {
renderInput(): TemplateResult {
if (this.supportsQL && this.hasEnterpriseLicense) {
return html`<ak-search-ql
.apiResponse=${this.apiResponse}
.value=${this.value}
.onSearch=${(value: string) => {
if (!this.onSearch) return;
this.onSearch(value);
}}
aria-label=${ifDefined(this.label)}
name="search"
required
placeholder=${ifDefined(this.placeholder)}
value=${ifDefined(this.defaultValue)}
.apiResponse=${this.apiResponse}
></ak-search-ql>`;
}
return html`<input
class="pf-c-form-control"
aria-label=${ifDefined(this.label)}
name="search"
type="search"
placeholder=${msg("Search...")}
value="${ifDefined(this.value)}"
required
placeholder=${ifDefined(this.placeholder)}
value=${ifDefined(this.defaultValue)}
class="pf-c-form-control"
/>`;
}
render(): TemplateResult {
return html`<form class="pf-c-input-group" method="get" @submit=${this.#submitListener}>
return html`<form
${ref(this.#formRef)}
class="pf-c-input-group"
@submit=${this.#submitListener}
>
${this.renderInput()}
<button
aria-label=${msg("Clear search")}
@@ -110,6 +122,6 @@ export class TableSearch extends WithLicenseSummary(AKElement) {
declare global {
interface HTMLElementTagNameMap {
"ak-table-search": TableSearch;
"ak-table-search": TableSearchForm;
}
}

View File

@@ -33,13 +33,10 @@ export class FlowCard extends AKElement {
PFLogin,
PFTitle,
css`
slot[name="footer"],
slot[name="footer-band"] {
display: flex;
flex-wrap: wrap;
justify-content: center;
flex-basis: 100%;
.pf-c-login__main-footer {
display: block;
}
slot[name="footer-band"] {
text-align: center;
background-color: var(--pf-c-login__main-footer-band--BackgroundColor);

View File

@@ -78,6 +78,13 @@ After the installation is complete, access authentik at `https://<ingress-host-n
You will get `Not Found` error if initial setup URL doesn't include the trailing forward slash `/`. Make sure you use the complete url (`http://<ingress-host-name>/if/flow/initial-setup/`) including the trailing forward slash.
:::
### PostgreSQL production setup
We recommend using another installation method for PostgreSQL than the one provided that is only intended for demonstration and testing purposes. We recommend the following operators:
- [CloudNativePG](https://github.com/cloudnative-pg/cloudnative-pg)
- [Zalando Postgres Operator](https://github.com/zalando/postgres-operator)
### Optional step: Configure global email credentials
It is recommended to configure global email credentials as well. These are used by authentik to notify you about alerts and configuration issues. Additionally, they can be utilized by [Email stages](../../add-secure-apps/flows-stages/stages/email/index.mdx) to send verification and recovery emails.

View File

@@ -3,12 +3,6 @@ title: Release 2025.8
slug: "/releases/2025.8"
---
:::::note
2025.8 has not been released yet! We're publishing these release notes as a preview of what's to come, and for our awesome beta testers trying out release candidates.
To try out the release candidate, replace your Docker image tag with the latest release candidate number, such as 2025.8.0-rc1. You can find the latest one in [the latest releases on GitHub](https://github.com/goauthentik/authentik/releases). If you don't find any, it means we haven't released one yet.
:::::
## Highlights
- **OAuth2/OpenID Connect back-channel logout**: :ak-preview A server-to-server notification mechanism that allows authentik to notify OAuth2/OpenID providers whenever a user's session is terminated.
@@ -17,7 +11,7 @@ To try out the release candidate, replace your Docker image tag with the latest
![Screenshot of the admin interface showing events plotted on a histogram chart and on a map](../../sys-mgmt/events/event-map-chart.png)
- **Advanced search**: :ak-enterprise Search for users and events with custom query language to filter on their properties and attributes.
- **Advanced search**: :ak-enterprise Search for [users](../../users-sources/user/user_basic_operations.md#tell-me-more) and [event logs](../../sys-mgmt/events/logging-events.md#tell-me-more) with custom query language to filter on their properties and attributes.
- **Email stage rate limiting**: The email stage can now be configured to set a maximum number of emails that can be sent within a specified time period.
@@ -118,6 +112,26 @@ Instead, the following metrics are now available:
- `authentik_tasks_delayed_in_progress`
- `authentik_tasks_duration_milliseconds`
### Prometheus metrics
The tasks metrics are no longer exposed by the server, but by the worker. For Helm chart users, add the following values to enable a `ServiceMonitor` to scrape those metrics:
```yaml
worker:
metrics:
enabled: true
serviceMonitor:
enabled: true
```
### Helm chart changes
Due to [Bitnami upcoming changes](https://github.com/bitnami/containers/issues/83267) to availability of their container images, the Helm chart default values have been updated to instead use [docker.io/library/postgres](https://hub.docker.com/_/postgres) and [docker.io/library/redis](https://hub.docker.com/_/redis). If you are setting custom values for either PostgreSQL or Redis, please review the [associated Helm chart changes](https://github.com/goauthentik/helm/pull/385) to update your values.
Redis has also been updated from 8.0 to 8.2.
From this point on, we recommend using the bundled PostgreSQL dependency for demonstration and test purposes only. See our [installation documentation](../../install-config/install/kubernetes.md) for alternatives to run PostgreSQL in a production environment.
## New features and improvements
- **LDAP Provider improvements**:
@@ -139,6 +153,7 @@ An integration is a how authentik connects to third-party applications, director
- [Papra](https://integrations.goauthentik.io/documentation/papra/)
- [Planka](https://integrations.goauthentik.io/chat-communication-collaboration/planka/)
- [Seafile](https://integrations.goauthentik.io/media/seafile/)
- [Vaultwarden](https://integrations.goauthentik.io/security/vaultwarden/)
- [Zoho](https://integrations.goauthentik.io/platforms/zoho/)
## Upgrading
@@ -180,8 +195,10 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2025.8
- blueprints: add JSON tag to parse JSON from string (#15235)
- blueprints: add section support for organisation (#15045)
- blueprints: sort schema items (#15022)
- brands: revert sort matched brand by match length (revert #15413) (cherry-pick #16233) (#16235)
- brands: sort matched brand by match length (#15413)
- core, providers/ldap: add parent/child groups to api and ldap results (#14974)
- core: Add email template selector (cherry-pick #16170) (#16225)
- core: Prevent application creation with reserved slugs (#15930)
- core: add updated_at field to user (#15571)
- core: better API validation for JSON fields (#15236)
@@ -215,6 +232,7 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2025.8
- packages/django-dramatiq-postgres: broker: remember previously fetched notifies (#16128)
- packages/django-dramatiq-postgres: fix typo (#15932)
- packages/django-dramatiq-postgres: run worker in the same base process, use structlog (#16061)
- policies/password: Fix amount_uppercase in password policy check (cherry-pick #16197) (#16228)
- policies/reputation: fix updated for reputation not updating (#15782)
- policies: Optimize policy checking for static bindings (#14957)
- policies: buffered policy access view for concurrent authorization attempts when unauthenticated (#15034)
@@ -234,6 +252,9 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2025.8
- root: add system check for database encoding (#15186)
- root: enhance custom middleware experience (#15919)
- root: extract custom setup code (#15150)
- root: fix custom packages installation in docker (cherry-pick #16150) (#16151)
- root: fix custom packages installation in docker (cherry-pick #16157) (#16158)
- root: fix missing uv run in makefile (#16146)
- root: fix some cases of invalid data triggering exceptions (#14799)
- root: monitoring: force db connection reload before healthcheck (#9970)
- root: remove /if/help (#14929)
@@ -253,6 +274,7 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2025.8
- stages/prompt: fix list policy for prompt validation failing with multiple policies (#15522)
- stages/user_login: unknown device (#14459)
- tasks/schedules: fix IntegrityError on schedule update (#15871)
- tasks: add sentry dramatiq integration (cherry-pick #16167) (#16183)
- tasks: fix rel_obj being removed when task is retried (#15862)
- tests/e2e: WebAuthn E2E tests (#14461)
- web/a11y -- ak-form-group (#15688)
@@ -262,10 +284,12 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2025.8
- web/a11y: Form Inputs (#15878)
- web/a11y: License notice ARIA attributes. (#15872)
- web/a11y: Navigation Banner (#15880)
- web/a11y: QL Search Input (cherry-pick #16198) (#16229)
- web/a11y: Tables & Modals (#15877)
- web/admin: Text and Textarea Fields that "hide" their contents until prompted (#15024)
- web/admin: adopt ak-hidden-text (#15042)
- web/admin: fix language in certificate import (#14953)
- web/admin: fix settings saving (cherry-pick #16184) (#16187)
- web/admin: fix variable name (#15934)
- web/admin: hide webhook URL by default (#15136)
- web/admin: improve admin UI for tasks slightly (#15829)
@@ -298,6 +322,7 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2025.8
- web: Clean up file methods. (#15479)
- web: Consistent use of static styles (#15510)
- web: Disable autocomplete. (#15551)
- web: Fix ak-flow-card footer alignment. (cherry-pick #16236) (#16238)
- web: Fix cursor using pointer in modals. (#16009)
- web: Fix dangling div. (#15478)
- web: Fix form captcha submission (#15482)
@@ -547,6 +572,10 @@ Changed response : **200 OK**
- Changed content type : `application/json`
- Changed property `attributes` (object -> object)
##### `POST` /core/users/&#123;id&#125;/recovery/
##### `POST` /core/users/&#123;id&#125;/recovery_email/
##### `GET` /events/events/volume/
###### Parameters:
@@ -1915,6 +1944,55 @@ Changed response : **200 OK**
* Deleted property `group_obj` (object)
##### `GET` /events/transports/&#123;uuid&#125;/
###### Return Type:
Changed response : **200 OK**
- Changed content type : `application/json`
- Added property `email_subject_prefix` (string)
- Added property `email_template` (string)
##### `PUT` /events/transports/&#123;uuid&#125;/
###### Request:
Changed content type : `application/json`
- Added property `email_subject_prefix` (string)
- Added property `email_template` (string)
###### Return Type:
Changed response : **200 OK**
- Changed content type : `application/json`
- Added property `email_subject_prefix` (string)
- Added property `email_template` (string)
##### `PATCH` /events/transports/&#123;uuid&#125;/
###### Request:
Changed content type : `application/json`
- Added property `email_subject_prefix` (string)
- Added property `email_template` (string)
###### Return Type:
Changed response : **200 OK**
- Changed content type : `application/json`
- Added property `email_subject_prefix` (string)
- Added property `email_template` (string)
##### `POST` /managed/blueprints/
###### Request:
@@ -3676,6 +3754,25 @@ Changed response : **200 OK**
- Changed property `brand` (object -> object)
##### `POST` /events/transports/
###### Request:
Changed content type : `application/json`
- Added property `email_subject_prefix` (string)
- Added property `email_template` (string)
###### Return Type:
Changed response : **201 Created**
- Changed content type : `application/json`
- Added property `email_subject_prefix` (string)
- Added property `email_template` (string)
##### `GET` /events/transports/
###### Return Type:
@@ -3688,6 +3785,13 @@ Changed response : **200 OK**
- `autocomplete`
* Added property `autocomplete` (object)
* Changed property `results` (array)
Changed items (object): > NotificationTransport Serializer
- Added property `email_subject_prefix` (string)
- Added property `email_template` (string)
##### `GET` /flows/instances/
###### Return Type:

View File

@@ -26,8 +26,36 @@ You can view audit details in the following areas of the authentik Admin interfa
- **Admin interface > Events > Logs**: In the event list, click the arrow toggle next to the event you want to view.
## Viewing events in maps and charts :ak-enterprise :ak-version[2025.8]
## Viewing events in maps and charts :ak-enterprise
With the enterprise version, you can view recent events on both a world map view with pinpoints indicating where each event occurred and also a color-coded chart that highlights event types and volume.
![](./event-map-chart.png)
## Advanced queries for event logs:ak-enterprise {#tell-me-more}
You can construct advanced queries to find specific event logs. In the Admin interface, navigate to **Events > Logs**, and then use the auto-complete in the **Search** field or enter your own queries to return results with greater specificity.
- **Field**: `action`, `event_uuid`, `app`, `client_ip`, `user`, `brand`, `context`, `created`
- **Operators**: `=`, `!=`, `~`, `!~`, `startswith`, `not startswith`, `endswidth`, `not endswith`, `in`, `not in`
- **Values**: `True`, `False`, `None`, and more
- **Example queries**:
- search event by application name: `app startswith "N"`
- search event by action: `action = "login"`
- search event by authorized application context: `authorized_application.name = "My app"`
- search event by country: `context.geo.country = "Germany"`
- search event by IP address: `client_ip = "10.0.0.1"`
- search event by brand: `brand.name = "my brand"`
- search event by user: `user.username in ["ana", "akadmin"]`
For more examples, refer to the list of [Event actions](./event-actions.md) and the related examples for each type of event.
:::info
1. To dismiss the drop-down menu option, click **ESC**.
2. If the list of operators does not appear in a drop-down menu you will need to manually enter it.
3. For queries that include `user`, `brand`, or `context` you need to use a compound term such as `user.username` or `brand.name`.
:::

View File

@@ -2,43 +2,43 @@
title: S3 storage setup
---
### Preparation
## Preparation
First, create a user on your S3 storage provider and get access credentials for S3, hereafter referred as `access_key` and `secret_key`.
First, create a user on your S3 storage provider and get access credentials (hereafter referred to as `access_key` and `secret_key`).
You will also need to know which endpoint authentik is going to use to access the S3 API, hereafter referred as `https://s3.provider`.
You will also need the S3 API endpoint that authentik will use (hereafter referred to as `https://s3.provider`). When using AWS S3, theres no need to set the endpoint, but for S3-compatible services like Azure Blob Storage or Cloudflare R2, use the provider's endpoint URL.
The bucket in which authentik is going to store files is going to be called `authentik-media`. You may need to change this name depending on your S3 provider limitations. Also, we are suffixing the bucket name with `-media` as authentik currently only stores media files, but may use other buckets in the future.
Create or pick a bucket for authentik media, for example `authentik-media`. Adjust the name to your providers bucket naming rules. We suffix with `-media` as authentik currently only stores media files (icons, etc.).
The domain used to access authentik is going to be referred to as `authentik.company`.
The domain you use to access authentik is referred to as `authentik.company` in the examples below.
You will also need the AWS CLI.
You will also need the AWS CLI available locally.
### S3 configuration
## S3 configuration
#### Bucket creation
### Bucket creation
Create the bucket in which authentik is going to store files:
Create the bucket that authentik will use for media files:
```bash
AWS_ACCESS_KEY_ID=access_key AWS_SECRET_ACCESS_KEY=secret_key aws s3api --endpoint-url=https://s3.provider create-bucket --bucket=authentik-media --acl=private
```
If using AWS S3, you can omit the `--endpoint-url` option, but may need to specify the `--region` option. If using Google Cloud Storage, refer to its documentation on how to create buckets.
If using AWS S3, you can omit `--endpoint-url`, but you may need to specify `--region`. Some regions require `--create-bucket-configuration LocationConstraint=<region>`.
The bucket ACL is set to private, although that is not strictly necessary, as an ACL associated with each object stored in the bucket will be private as well.
The bucket ACL is set to private. Depending on your provider you can alternatively disable ACLs and rely on bucket policies.
#### CORS policy
### CORS policy
Next, associate a CORS policy to the bucket to allow the authentik web interface to show images stored in the bucket.
Apply a CORS policy to the bucket, allowing the authentik web interface to access images directly.
First, save the following file locally as `cors.json`:
Save the following as `cors.json` (use your deployments origin; include scheme and port if nonstandard):
```json
{
"CORSRules": [
{
"AllowedOrigins": ["authentik.company"],
"AllowedOrigins": ["https://authentik.company"],
"AllowedHeaders": ["Authorization"],
"AllowedMethods": ["GET"],
"MaxAgeSeconds": 3000
@@ -47,9 +47,9 @@ First, save the following file locally as `cors.json`:
}
```
If authentik is accessed from multiple domains, you can add them to the `AllowedOrigins` list.
If authentik is accessed from multiple domains, include each one in `AllowedOrigins`.
Apply that policy to the bucket:
Apply the policy to the bucket:
```bash
AWS_ACCESS_KEY_ID=access_key AWS_SECRET_ACCESS_KEY=secret_key aws s3api --endpoint-url=https://s3.provider put-bucket-cors --bucket=authentik-media --cors-configuration=file://cors.json
@@ -66,39 +66,51 @@ AUTHENTIK_STORAGE__MEDIA__S3__SECRET_KEY=secret_key
AUTHENTIK_STORAGE__MEDIA__S3__BUCKET_NAME=authentik-media
```
If you are using AWS S3 as your S3 provider, add the following:
If you are using AWS S3, add:
```env
AUTHENTIK_STORAGE__MEDIA__S3__REGION=us-east-1 # Use the region of the bucket
```
If you are not using AWS S3 as your S3 provider, add the following:
If you are using an S3compatible provider (nonAWS), add:
```env
AUTHENTIK_STORAGE__MEDIA__S3__ENDPOINT=https://s3.provider
AUTHENTIK_STORAGE__MEDIA__S3__CUSTOM_DOMAIN=s3.provider/authentik-media
```
The `ENDPOINT` setting specifies how authentik talks to the S3 provider.
The `AUTHENTIK_STORAGE__MEDIA__S3__ENDPOINT` setting controls how authentik communicates with the S3 provider. When set, it overrides region/`USE_SSL`.
The `CUSTOM_DOMAIN` setting specifies how URLs are constructed to be shown on the web interface. For example, an object stored at `application-icons/application.png` with a `CUSTOM__DOMAIN` setting of `s3.provider/authentik-media` will result in a URL of `https://s3.provider/authentik-media/application-icons/application.png`. You can also use subdomains for your buckets depending on what your S3 provider offers: `authentik-media.s3.provider`. Whether HTTPS is used is controlled by `AUTHENTIK_STORAGE__MEDIA__S3__SECURE_URLS`, which defaults to true.
The `AUTHENTIK_STORAGE__MEDIA__S3__CUSTOM_DOMAIN` setting controls how media URLs are built for the web interface. It must include the bucket name and must not include a scheme.
For more control over settings, refer to the [configuration reference](../../install-config/configuration/configuration.mdx#media-storage-settings)
For a path-style domain, set `AUTHENTIK_STORAGE__MEDIA__S3__CUSTOM_DOMAIN=s3.provider/authentik-media`. The object `application-icons/application.png` will be available at `https://s3.provider/authentik-media/application-icons/application.png`.
### Migrating between storage backends
Whether URLs use HTTPS is controlled by `AUTHENTIK_STORAGE__MEDIA__S3__SECURE_URLS` (defaults to `true`). Depending on your provider, you can also use a virtual hosted-style domain such as `authentik-media.s3.provider`.
The following section assumes that the local storage path is `/media` and the bucket name is `authentik-media`. It also assumes you have a working `aws` CLI that can interact with the bucket.
:::info
You can omit `ACCESS_KEY` and `SECRET_KEY` when using AWS SDK authentication (instance roles or profiles). See `AUTHENTIK_STORAGE__MEDIA__S3__SESSION_PROFILE` and related options in the configuration reference](../../install-config/configuration/configuration.mdx#media-storage-settings).
:::
#### From file to s3
For more options (including `AUTHENTIK_STORAGE__MEDIA__S3__USE_SSL`, session profiles, and security tokens), see the [configuration reference](../../install-config/configuration/configuration.mdx#media-storage-settings).
Follow the setup steps above, and then migrate the files from your local directory to s3:
## Migrating between storage backends
The following assumes the local storage path is `/media` and the bucket is `authentik-media`. Ensure your `aws` CLI is configured to talk to your provider (add `--endpoint-url` or `--region` as needed).
### From file to s3
Follow the setup steps above, then sync files from the local directory to S3 (to the bucket root):
```bash
aws s3 sync /media s3://authentik-media/media
aws s3 sync /media s3://authentik-media/
# For non-AWS providers, include the endpoint:
# aws --endpoint-url=https://s3.provider s3 sync /media s3://authentik-media/
```
#### From s3 to file
### From s3 to file
```bash
aws s3 sync s3://authentik-media/media /media
aws s3 sync s3://authentik-media/ /media
# For non-AWS providers:
# aws --endpoint-url=https://s3.provider s3 sync s3://authentik-media/ /media
```

View File

@@ -6,7 +6,7 @@ The following topics are for the basic management of users: how to create, modif
[Policies](../../customize/policies/index.md) can be used to further manage how users are authenticated. For example, by default authentik does not require email addresses be unique, but you can use a policy to [enforce unique email addresses](../../customize/policies/expression/unique_email.md).
### Create a user
## Create a user
> If you want to automate user creation, you can do that either by [invitations](./invitations.md), [`user_write` stage](../../add-secure-apps/flows-stages/stages/user_write.md), or [using the API](/api/reference/core-users-create).
@@ -33,7 +33,29 @@ You should see a confirmation pop-up on the top-right of the screen that the use
To create a super-user, you need to add the user to a group that has super-user permissions. For more information, refer to [Create a Group](../groups/manage_groups.mdx#create-a-group).
:::
### View user details
## Advanced queries for users:ak-enterprise {#tell-me-more}
You can create advanced queries to locate specific users within the list shown under **Directory** > **Users** in the Admin interface. Use the auto-complete in the **Search** field or enter your own queries to return results with greater specificity.
- **Field**: `username`, `path`, `name`, `email`, `path`, `is_active`, `type`, `attributes`
- **Operators**: `=`, `!=`, `~`, `!~`, `startswith`, `not startswith`, `endswidth`, `not endswith`, `in`, `not in`
- **Values**: `True`, `False`, `None`, and more
- **Example queries**:
- search user by status: `is_active = False`
- search user by username: `username = "bob"`
- search user by email address: `email = "bob@authentik.company"`
- search user by attribute: `attribute.my_custom_attribute = "foo"`
:::info
1. To dismiss the drop-down menu option, click **ESC**.
2. If the list of operators does not appear in a drop-down menu you will need to manually enter it.
:::
## View user details
In the **Directory > Users** menu of the Admin interface, you can browse all the users in your authentik instance.
@@ -58,7 +80,7 @@ After the creation of the user, you can edit any parameter defined during the cr
To modify a user object, go to **Directory > Users**, and click the edit icon beside the name. You can also go into [user details](#view-user-details), and click **Edit**.
### Assign, modify, or remove permissions for a user
## Assign, modify, or remove permissions for a user
You can grant a user specific global or object-level permissions. Alternatively, you can add a user to a group that has the appropriate permissions, and the user inherits all of the group's permissions.
@@ -86,7 +108,7 @@ This option is only available if a default recovery flow was configured for the
A pop-up will appear on your browser with the link for you to copy and to send to the user.
### Email them a recovery link
### Email a recovery link
:::info
This option is only available if a default recovery flow was configured for the currently active brand and if the configured flow has an [Email Stage](../../add-secure-apps/flows-stages/stages/email/index.mdx) bound to it.
@@ -100,7 +122,7 @@ You can send a link with the URL for the user to reset their password via Email.
If the user does not receive the email, check if the mail server parameters [are properly configured](../../troubleshooting/emails.md).
### Reset the password for the user
## Reset the password for the user
As an Admin, you can simply reset the password for the user.
@@ -110,14 +132,14 @@ As an Admin, you can simply reset the password for the user.
## Deactivate or Delete user
#### To deactivate a user:
### To deactivate a user:
1. Go into the user list or detail, and click **Deactivate**.
2. Review the changes and click **Update**.
The active sessions are revoked and the authentication of the user blocked. You can reactivate the account by following the same procedure.
#### To delete a user:
### To delete a user:
:::caution
This deletion is not reversible, so be sure you do not need to recover any identity data of the user.