Merge branch 'main' into celery-2-dramatiq

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
Marc 'risson' Schmitt
2025-03-19 20:44:04 +01:00
141 changed files with 11294 additions and 7469 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2025.2.1 current_version = 2025.2.2
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -9,17 +9,22 @@ inputs:
runs: runs:
using: "composite" using: "composite"
steps: steps:
- name: Install poetry & deps - name: Install apt deps
shell: bash shell: bash
run: | run: |
pipx install poetry || true
sudo apt-get update sudo apt-get update
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
- name: Setup python and restore poetry - name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Setup python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version-file: "pyproject.toml" python-version-file: "pyproject.toml"
cache: "poetry" - name: Install Python deps
shell: bash
run: uv sync --all-extras --dev --frozen
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@ -39,10 +44,9 @@ runs:
run: | run: |
export PSQL_TAG=${{ inputs.postgresql_version }} export PSQL_TAG=${{ inputs.postgresql_version }}
docker compose -f .github/actions/setup/docker-compose.yml up -d docker compose -f .github/actions/setup/docker-compose.yml up -d
poetry sync
cd web && npm ci cd web && npm ci
- name: Generate config - name: Generate config
shell: poetry run python {0} shell: uv run python {0}
run: | run: |
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from yaml import safe_dump from yaml import safe_dump

View File

@ -98,7 +98,7 @@ updates:
prefix: "lifecycle/aws:" prefix: "lifecycle/aws:"
labels: labels:
- dependencies - dependencies
- package-ecosystem: pip - package-ecosystem: uv
directory: "/" directory: "/"
schedule: schedule:
interval: daily interval: daily

View File

@ -33,7 +33,7 @@ jobs:
npm ci npm ci
- name: Check changes have been applied - name: Check changes have been applied
run: | run: |
poetry run make aws-cfn uv run make aws-cfn
git diff --exit-code git diff --exit-code
ci-aws-cfn-mark: ci-aws-cfn-mark:
if: always() if: always()

View File

@ -34,7 +34,7 @@ jobs:
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: run job - name: run job
run: poetry run make ci-${{ matrix.job }} run: uv run make ci-${{ matrix.job }}
test-migrations: test-migrations:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -42,7 +42,7 @@ jobs:
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: run migrations - name: run migrations
run: poetry run python -m lifecycle.migrate run: uv run python -m lifecycle.migrate
test-make-seed: test-make-seed:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -69,19 +69,21 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: checkout stable - name: checkout stable
run: | run: |
# Delete all poetry envs
rm -rf /home/runner/.cache/pypoetry
# Copy current, latest config to local # Copy current, latest config to local
# Temporarly comment the .github backup while migrating to uv
cp authentik/lib/default.yml local.env.yml cp authentik/lib/default.yml local.env.yml
cp -R .github .. # cp -R .github ..
cp -R scripts .. cp -R scripts ..
git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1) git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
rm -rf .github/ scripts/ # rm -rf .github/ scripts/
mv ../.github ../scripts . # mv ../.github ../scripts .
rm -rf scripts/
mv ../scripts .
- name: Setup authentik env (stable) - name: Setup authentik env (stable)
uses: ./.github/actions/setup uses: ./.github/actions/setup
with: with:
postgresql_version: ${{ matrix.psql }} postgresql_version: ${{ matrix.psql }}
continue-on-error: true
- name: run migrations to stable - name: run migrations to stable
run: poetry run python -m lifecycle.migrate run: poetry run python -m lifecycle.migrate
- name: checkout current code - name: checkout current code
@ -91,15 +93,13 @@ jobs:
git reset --hard HEAD git reset --hard HEAD
git clean -d -fx . git clean -d -fx .
git checkout $GITHUB_SHA git checkout $GITHUB_SHA
# Delete previous poetry env
rm -rf /home/runner/.cache/pypoetry/virtualenvs/*
- name: Setup authentik env (ensure latest deps are installed) - name: Setup authentik env (ensure latest deps are installed)
uses: ./.github/actions/setup uses: ./.github/actions/setup
with: with:
postgresql_version: ${{ matrix.psql }} postgresql_version: ${{ matrix.psql }}
- name: migrate to latest - name: migrate to latest
run: | run: |
poetry run python -m lifecycle.migrate uv run python -m lifecycle.migrate
- name: run tests - name: run tests
env: env:
# Test in the main database that we just migrated from the previous stable version # Test in the main database that we just migrated from the previous stable version
@ -108,7 +108,7 @@ jobs:
CI_RUN_ID: ${{ matrix.run_id }} CI_RUN_ID: ${{ matrix.run_id }}
CI_TOTAL_RUNS: "5" CI_TOTAL_RUNS: "5"
run: | run: |
poetry run make ci-test uv run make ci-test
test-unittest: test-unittest:
name: test-unittest - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5 name: test-unittest - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -133,7 +133,7 @@ jobs:
CI_RUN_ID: ${{ matrix.run_id }} CI_RUN_ID: ${{ matrix.run_id }}
CI_TOTAL_RUNS: "5" CI_TOTAL_RUNS: "5"
run: | run: |
poetry run make ci-test uv run make ci-test
- if: ${{ always() }} - if: ${{ always() }}
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5
with: with:
@ -156,8 +156,8 @@ jobs:
uses: helm/kind-action@v1.12.0 uses: helm/kind-action@v1.12.0
- name: run integration - name: run integration
run: | run: |
poetry run coverage run manage.py test tests/integration uv run coverage run manage.py test tests/integration
poetry run coverage xml uv run coverage xml
- if: ${{ always() }} - if: ${{ always() }}
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5
with: with:
@ -214,8 +214,8 @@ jobs:
npm run build npm run build
- name: run e2e - name: run e2e
run: | run: |
poetry run coverage run manage.py test ${{ matrix.job.glob }} uv run coverage run manage.py test ${{ matrix.job.glob }}
poetry run coverage xml uv run coverage xml
- if: ${{ always() }} - if: ${{ always() }}
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5
with: with:

View File

@ -2,7 +2,7 @@ name: authentik-gen-update-webauthn-mds
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: '30 1 1,15 * *' - cron: "30 1 1,15 * *"
env: env:
POSTGRES_DB: authentik POSTGRES_DB: authentik
@ -24,7 +24,7 @@ jobs:
token: ${{ steps.generate_token.outputs.token }} token: ${{ steps.generate_token.outputs.token }}
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- run: poetry run ak update_webauthn_mds - run: uv run ak update_webauthn_mds
- uses: peter-evans/create-pull-request@v7 - uses: peter-evans/create-pull-request@v7
id: cpr id: cpr
with: with:

View File

@ -21,8 +21,8 @@ jobs:
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: generate docs - name: generate docs
run: | run: |
poetry run make migrate uv run make migrate
poetry run ak build_source_docs uv run ak build_source_docs
- name: Publish - name: Publish
uses: netlify/actions/cli@master uses: netlify/actions/cli@master
with: with:

View File

@ -36,10 +36,10 @@ jobs:
run: make gen-client-ts run: make gen-client-ts
- name: run extract - name: run extract
run: | run: |
poetry run make i18n-extract uv run make i18n-extract
- name: run compile - name: run compile
run: | run: |
poetry run ak compilemessages uv run ak compilemessages
make web-check-compile make web-check-compile
- name: Create Pull Request - name: Create Pull Request
if: ${{ github.event_name != 'pull_request' }} if: ${{ github.event_name != 'pull_request' }}

46
.vscode/tasks.json vendored
View File

@ -3,8 +3,13 @@
"tasks": [ "tasks": [
{ {
"label": "authentik/core: make", "label": "authentik/core: make",
"command": "poetry", "command": "uv",
"args": ["run", "make", "lint-fix", "lint"], "args": [
"run",
"make",
"lint-fix",
"lint"
],
"presentation": { "presentation": {
"panel": "new" "panel": "new"
}, },
@ -12,8 +17,12 @@
}, },
{ {
"label": "authentik/core: run", "label": "authentik/core: run",
"command": "poetry", "command": "uv",
"args": ["run", "ak", "server"], "args": [
"run",
"ak",
"server"
],
"group": "build", "group": "build",
"presentation": { "presentation": {
"panel": "dedicated", "panel": "dedicated",
@ -23,13 +32,17 @@
{ {
"label": "authentik/web: make", "label": "authentik/web: make",
"command": "make", "command": "make",
"args": ["web"], "args": [
"web"
],
"group": "build" "group": "build"
}, },
{ {
"label": "authentik/web: watch", "label": "authentik/web: watch",
"command": "make", "command": "make",
"args": ["web-watch"], "args": [
"web-watch"
],
"group": "build", "group": "build",
"presentation": { "presentation": {
"panel": "dedicated", "panel": "dedicated",
@ -39,19 +52,26 @@
{ {
"label": "authentik: install", "label": "authentik: install",
"command": "make", "command": "make",
"args": ["install", "-j4"], "args": [
"install",
"-j4"
],
"group": "build" "group": "build"
}, },
{ {
"label": "authentik/website: make", "label": "authentik/website: make",
"command": "make", "command": "make",
"args": ["website"], "args": [
"website"
],
"group": "build" "group": "build"
}, },
{ {
"label": "authentik/website: watch", "label": "authentik/website: watch",
"command": "make", "command": "make",
"args": ["website-watch"], "args": [
"website-watch"
],
"group": "build", "group": "build",
"presentation": { "presentation": {
"panel": "dedicated", "panel": "dedicated",
@ -60,8 +80,12 @@
}, },
{ {
"label": "authentik/api: generate", "label": "authentik/api: generate",
"command": "poetry", "command": "uv",
"args": ["run", "make", "gen"], "args": [
"run",
"make",
"gen"
],
"group": "build" "group": "build"
} }
] ]

View File

@ -10,7 +10,7 @@ schemas/ @goauthentik/backend
scripts/ @goauthentik/backend scripts/ @goauthentik/backend
tests/ @goauthentik/backend tests/ @goauthentik/backend
pyproject.toml @goauthentik/backend pyproject.toml @goauthentik/backend
poetry.lock @goauthentik/backend uv.lock @goauthentik/backend
go.mod @goauthentik/backend go.mod @goauthentik/backend
go.sum @goauthentik/backend go.sum @goauthentik/backend
# Infrastructure # Infrastructure

View File

@ -5,7 +5,7 @@
We as members, contributors, and leaders pledge to make participation in our We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status, identity and expression, level of experience, education, socioeconomic status,
nationality, personal appearance, race, religion, or sexual identity nationality, personal appearance, race, religion, or sexual identity
and orientation. and orientation.

View File

@ -3,8 +3,7 @@
# Stage 1: Build website # Stage 1: Build website
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
ENV NODE_ENV=production \ ENV NODE_ENV=production
GIT_UNAVAILABLE=true
WORKDIR /work/website WORKDIR /work/website
@ -94,53 +93,59 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
mkdir -p /usr/share/GeoIP && \ mkdir -p /usr/share/GeoIP && \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Python dependencies # Stage 5: Download uv
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-deps FROM ghcr.io/astral-sh/uv:0.6.8 AS uv
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_NATIVE_TLS=1 \
UV_PYTHON_DOWNLOADS=0
WORKDIR /ak-root/
COPY --from=uv /uv /uvx /bin/
# Stage 7: Python dependencies
FROM python-base AS python-deps
ARG TARGETARCH ARG TARGETARCH
ARG TARGETVARIANT ARG TARGETVARIANT
WORKDIR /ak-root/poetry
ENV VENV_PATH="/ak-root/venv" \
POETRY_VIRTUALENVS_CREATE=false \
PATH="/ak-root/venv/bin:$PATH"
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
ENV PATH="/root/.cargo/bin:$PATH"
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \ RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
apt-get update && \ apt-get update && \
# Required for installing pip packages # Required for installing pip packages
apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev libkrb5-dev
RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
--mount=type=bind,target=./poetry.lock,src=./poetry.lock \
--mount=type=cache,target=/root/.cache/pip \
--mount=type=cache,target=/root/.cache/pypoetry \
pip install --no-cache cffi && \
apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
build-essential libffi-dev \ # Build essentials
# Required for cryptography build-essential pkg-config libffi-dev git \
curl pkg-config \ # cryptography
# Required for lxml curl \
libxslt-dev zlib1g-dev \ # libxml
# Required for xmlsec libxslt-dev zlib1g-dev \
libltdl-dev \ # postgresql
# Required for kadmin libpq-dev \
sccache clang && \ # python-kadmin-rs
curl https://sh.rustup.rs -sSf | sh -s -- -y && \ clang libkrb5-dev sccache \
. "$HOME/.cargo/env" && \ # xmlsec
python -m venv /ak-root/venv/ && \ libltdl-dev && \
bash -c "source ${VENV_PATH}/bin/activate && \ curl https://sh.rustup.rs -sSf | sh -s -- -y
pip3 install --upgrade pip poetry && \
poetry config --local installer.no-binary cryptography,xmlsec,lxml,python-kadmin-rs && \
poetry install --only=main --no-ansi --no-interaction --no-root && \
pip uninstall cryptography -y && \
poetry install --only=main --no-ansi --no-interaction --no-root"
# Stage 6: Run ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec"
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS final-image
RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
--mount=type=bind,target=uv.lock,src=uv.lock \
--mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
# Stage 8: Run
FROM python-base AS final-image
ARG VERSION ARG VERSION
ARG GIT_BUILD_HASH ARG GIT_BUILD_HASH
@ -172,7 +177,7 @@ RUN apt-get update && \
COPY ./authentik/ /authentik COPY ./authentik/ /authentik
COPY ./pyproject.toml / COPY ./pyproject.toml /
COPY ./poetry.lock / COPY ./uv.lock /
COPY ./schemas /schemas COPY ./schemas /schemas
COPY ./locale /locale COPY ./locale /locale
COPY ./tests /tests COPY ./tests /tests
@ -181,7 +186,7 @@ COPY ./blueprints /blueprints
COPY ./lifecycle/ /lifecycle COPY ./lifecycle/ /lifecycle
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
COPY --from=go-builder /go/authentik /bin/authentik COPY --from=go-builder /go/authentik /bin/authentik
COPY --from=python-deps /ak-root/venv /ak-root/venv COPY --from=python-deps /ak-root/.venv /ak-root/.venv
COPY --from=web-builder /work/web/dist/ /web/dist/ COPY --from=web-builder /work/web/dist/ /web/dist/
COPY --from=web-builder /work/web/authentik/ /web/authentik/ COPY --from=web-builder /work/web/authentik/ /web/authentik/
COPY --from=website-builder /work/website/build/ /website/help/ COPY --from=website-builder /work/website/build/ /website/help/
@ -192,9 +197,6 @@ USER 1000
ENV TMPDIR=/dev/shm/ \ ENV TMPDIR=/dev/shm/ \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PATH="/ak-root/venv/bin:/lifecycle:$PATH" \
VENV_PATH="/ak-root/venv" \
POETRY_VIRTUALENVS_CREATE=false \
GOFIPS=1 GOFIPS=1
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ] HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]

View File

@ -4,7 +4,7 @@
PWD = $(shell pwd) PWD = $(shell pwd)
UID = $(shell id -u) UID = $(shell id -u)
GID = $(shell id -g) GID = $(shell id -g)
NPM_VERSION = $(shell poetry run python -m scripts.generate_semver) NPM_VERSION = $(shell python -m scripts.generate_semver)
PY_SOURCES = authentik tests scripts lifecycle .github PY_SOURCES = authentik tests scripts lifecycle .github
DOCKER_IMAGE ?= "authentik:test" DOCKER_IMAGE ?= "authentik:test"
@ -12,9 +12,9 @@ GEN_API_TS = "gen-ts-api"
GEN_API_PY = "gen-py-api" GEN_API_PY = "gen-py-api"
GEN_API_GO = "gen-go-api" GEN_API_GO = "gen-go-api"
pg_user := $(shell poetry run python -m authentik.lib.config postgresql.user 2>/dev/null) pg_user := $(shell uv run python -m authentik.lib.config postgresql.user 2>/dev/null)
pg_host := $(shell poetry run python -m authentik.lib.config postgresql.host 2>/dev/null) pg_host := $(shell uv run python -m authentik.lib.config postgresql.host 2>/dev/null)
pg_name := $(shell poetry run python -m authentik.lib.config postgresql.name 2>/dev/null) pg_name := $(shell uv run python -m authentik.lib.config postgresql.name 2>/dev/null)
all: lint-fix lint test gen web ## Lint, build, and test everything all: lint-fix lint test gen web ## Lint, build, and test everything
@ -32,34 +32,37 @@ go-test:
go test -timeout 0 -v -race -cover ./... go test -timeout 0 -v -race -cover ./...
test: ## Run the server tests and produce a coverage report (locally) test: ## Run the server tests and produce a coverage report (locally)
poetry run coverage run manage.py test --keepdb authentik uv run coverage run manage.py test --keepdb authentik
poetry run coverage html uv run coverage html
poetry run coverage report uv run coverage report
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors. lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
poetry run black $(PY_SOURCES) uv run black $(PY_SOURCES)
poetry run ruff check --fix $(PY_SOURCES) uv run ruff check --fix $(PY_SOURCES)
lint-codespell: ## Reports spelling errors. lint-codespell: ## Reports spelling errors.
poetry run codespell -w uv run codespell -w
lint: ## Lint the python and golang sources lint: ## Lint the python and golang sources
poetry run bandit -c pyproject.toml -r $(PY_SOURCES) uv run bandit -c pyproject.toml -r $(PY_SOURCES)
golangci-lint run -v golangci-lint run -v
core-install: core-install:
poetry install uv sync --frozen
migrate: ## Run the Authentik Django server's migrations migrate: ## Run the Authentik Django server's migrations
poetry run python -m lifecycle.migrate uv run python -m lifecycle.migrate
i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that require translation into files to send to a translation service i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that require translation into files to send to a translation service
aws-cfn: aws-cfn:
cd lifecycle/aws && npm run aws-cfn cd lifecycle/aws && npm run aws-cfn
run: ## Run the main authentik server process
uv run ak server
core-i18n-extract: core-i18n-extract:
poetry run ak makemessages \ uv run ak makemessages \
--add-location file \ --add-location file \
--no-obsolete \ --no-obsolete \
--ignore web \ --ignore web \
@ -90,11 +93,11 @@ gen-build: ## Extract the schema from the database
AUTHENTIK_DEBUG=true \ AUTHENTIK_DEBUG=true \
AUTHENTIK_TENANTS__ENABLED=true \ AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \ AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
poetry run ak make_blueprint_schema > blueprints/schema.json uv run ak make_blueprint_schema > blueprints/schema.json
AUTHENTIK_DEBUG=true \ AUTHENTIK_DEBUG=true \
AUTHENTIK_TENANTS__ENABLED=true \ AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \ AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
poetry run ak spectacular --file schema.yml uv run ak spectacular --file schema.yml
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
@ -145,7 +148,7 @@ gen-client-py: gen-clean-py ## Build and install the authentik API for Python
docker run \ docker run \
--rm -v ${PWD}:/local \ --rm -v ${PWD}:/local \
--user ${UID}:${GID} \ --user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.4.0 generate \ docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
-i /local/schema.yml \ -i /local/schema.yml \
-g python \ -g python \
-o /local/${GEN_API_PY} \ -o /local/${GEN_API_PY} \
@ -173,7 +176,7 @@ gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/ rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/
gen-dev-config: ## Generate a local development config file gen-dev-config: ## Generate a local development config file
poetry run scripts/generate_config.py uv run scripts/generate_config.py
gen: gen-build gen-client-ts gen: gen-build gen-client-ts
@ -254,21 +257,21 @@ ci--meta-debug:
node --version node --version
ci-black: ci--meta-debug ci-black: ci--meta-debug
poetry run black --check $(PY_SOURCES) uv run black --check $(PY_SOURCES)
ci-ruff: ci--meta-debug ci-ruff: ci--meta-debug
poetry run ruff check $(PY_SOURCES) uv run ruff check $(PY_SOURCES)
ci-codespell: ci--meta-debug ci-codespell: ci--meta-debug
poetry run codespell -s uv run codespell -s
ci-bandit: ci--meta-debug ci-bandit: ci--meta-debug
poetry run bandit -r $(PY_SOURCES) uv run bandit -r $(PY_SOURCES)
ci-pending-migrations: ci--meta-debug ci-pending-migrations: ci--meta-debug
poetry run ak makemigrations --check uv run ak makemigrations --check
ci-test: ci--meta-debug ci-test: ci--meta-debug
poetry run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik uv run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
poetry run coverage report uv run coverage report
poetry run coverage xml uv run coverage xml

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2025.2.1" __version__ = "2025.2.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -5,6 +5,7 @@ from collections.abc import Iterable
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
@ -154,6 +155,17 @@ class SourceViewSet(
matching_sources.append(source_settings.validated_data) matching_sources.append(source_settings.validated_data)
return Response(matching_sources) return Response(matching_sources)
def destroy(self, request: Request, *args, **kwargs):
"""Prevent deletion of built-in sources"""
instance: Source = self.get_object()
if instance.managed == Source.MANAGED_INBUILT:
raise ValidationError(
{"detail": "Built-in sources cannot be deleted"}, code="protected"
)
return super().destroy(request, *args, **kwargs)
class UserSourceConnectionSerializer(SourceSerializer): class UserSourceConnectionSerializer(SourceSerializer):
"""User source connection""" """User source connection"""

View File

@ -32,5 +32,5 @@ class AuthentikCoreConfig(ManagedAppConfig):
"name": "authentik Built-in", "name": "authentik Built-in",
"slug": "authentik-built-in", "slug": "authentik-built-in",
}, },
managed="goauthentik.io/sources/inbuilt", managed=Source.MANAGED_INBUILT,
) )

View File

@ -678,6 +678,8 @@ class SourceGroupMatchingModes(models.TextChoices):
class Source(ManagedModel, SerializerModel, PolicyBindingModel): class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
MANAGED_INBUILT = "goauthentik.io/sources/inbuilt"
name = models.TextField(help_text=_("Source's display Name.")) name = models.TextField(help_text=_("Source's display Name."))
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True) slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)

View File

@ -1,5 +1,6 @@
"""Base Kubernetes Reconciler""" """Base Kubernetes Reconciler"""
import re
from dataclasses import asdict from dataclasses import asdict
from json import dumps from json import dumps
from typing import TYPE_CHECKING, Generic, TypeVar from typing import TYPE_CHECKING, Generic, TypeVar
@ -67,7 +68,8 @@ class KubernetesObjectReconciler(Generic[T]):
@property @property
def name(self) -> str: def name(self) -> str:
"""Get the name of the object this reconciler manages""" """Get the name of the object this reconciler manages"""
return (
base_name = (
self.controller.outpost.config.object_naming_template self.controller.outpost.config.object_naming_template
% { % {
"name": slugify(self.controller.outpost.name), "name": slugify(self.controller.outpost.name),
@ -75,6 +77,16 @@ class KubernetesObjectReconciler(Generic[T]):
} }
).lower() ).lower()
formatted = slugify(base_name)
formatted = re.sub(r"[^a-z0-9-]", "-", formatted)
formatted = re.sub(r"-+", "-", formatted)
formatted = formatted[:63]
if not formatted:
formatted = f"outpost-{self.controller.outpost.uuid.hex}"[:63]
return formatted
def get_patched_reference_object(self) -> T: def get_patched_reference_object(self) -> T:
"""Get patched reference object""" """Get patched reference object"""
reference = self.get_reference_object() reference = self.get_reference_object()
@ -112,7 +124,6 @@ class KubernetesObjectReconciler(Generic[T]):
try: try:
current = self.retrieve() current = self.retrieve()
except (OpenApiException, HTTPError) as exc: except (OpenApiException, HTTPError) as exc:
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code: if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
self.logger.debug("Failed to get current, triggering recreate") self.logger.debug("Failed to get current, triggering recreate")
raise NeedsRecreate from exc raise NeedsRecreate from exc
@ -156,7 +167,6 @@ class KubernetesObjectReconciler(Generic[T]):
self.delete(current) self.delete(current)
self.logger.debug("Removing") self.logger.debug("Removing")
except (OpenApiException, HTTPError) as exc: except (OpenApiException, HTTPError) as exc:
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code: if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
self.logger.debug("Failed to get current, assuming non-existent") self.logger.debug("Failed to get current, assuming non-existent")
return return

View File

@ -61,9 +61,14 @@ class KubernetesController(BaseController):
client: KubernetesClient client: KubernetesClient
connection: KubernetesServiceConnection connection: KubernetesServiceConnection
def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection) -> None: def __init__(
self,
outpost: Outpost,
connection: KubernetesServiceConnection,
client: KubernetesClient | None = None,
) -> None:
super().__init__(outpost, connection) super().__init__(outpost, connection)
self.client = KubernetesClient(connection) self.client = client if client else KubernetesClient(connection)
self.reconcilers = { self.reconcilers = {
SecretReconciler.reconciler_name(): SecretReconciler, SecretReconciler.reconciler_name(): SecretReconciler,
DeploymentReconciler.reconciler_name(): DeploymentReconciler, DeploymentReconciler.reconciler_name(): DeploymentReconciler,

View File

@ -0,0 +1,44 @@
"""Kubernetes controller tests"""
from django.test import TestCase
from authentik.blueprints.tests import reconcile_app
from authentik.lib.generators import generate_id
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
from authentik.outposts.controllers.kubernetes import KubernetesController
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
class KubernetesControllerTests(TestCase):
"""Kubernetes controller tests"""
@reconcile_app("authentik_outposts")
def setUp(self) -> None:
self.outpost = Outpost.objects.create(
name="test",
type=OutpostType.PROXY,
)
self.integration = KubernetesServiceConnection(name="test")
def test_gen_name(self):
"""Ensure the generated name is valid"""
controller = KubernetesController(
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(),
self.integration,
# Pass something not-none as client so we don't
# attempt to connect to K8s as that's not needed
client=self,
)
rec = DeploymentReconciler(controller)
self.assertEqual(rec.name, "ak-outpost-authentik-embedded-outpost")
controller.outpost.name = generate_id()
self.assertLess(len(rec.name), 64)
# Test custom naming template
_cfg = controller.outpost.config
_cfg.object_naming_template = ""
controller.outpost.config = _cfg
self.assertEqual(rec.name, f"outpost-{controller.outpost.uuid.hex}")
self.assertLess(len(rec.name), 64)

View File

@ -254,10 +254,10 @@ class OAuthAuthorizationParams:
raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type, self.state) raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type, self.state)
if SCOPE_OFFLINE_ACCESS in self.scope: if SCOPE_OFFLINE_ACCESS in self.scope:
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess # https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
if PROMPT_CONSENT not in self.prompt: # Don't explicitly request consent with offline_access, as the spec allows for
# Instead of ignoring the `offline_access` scope when `prompt` # "other conditions for processing the request permitting offline access to the
# isn't set to `consent`, we set override it ourselves # requested resources are in place"
self.prompt.add(PROMPT_CONSENT) # which we interpret as "the admin picks an authorization flow with or without consent"
if self.response_type not in [ if self.response_type not in [
ResponseTypes.CODE, ResponseTypes.CODE,
ResponseTypes.CODE_TOKEN, ResponseTypes.CODE_TOKEN,

View File

@ -1,9 +1,9 @@
"""RAC app config""" """RAC app config"""
from django.apps import AppConfig from authentik.blueprints.apps import ManagedAppConfig
class AuthentikProviderRAC(AppConfig): class AuthentikProviderRAC(ManagedAppConfig):
"""authentik rac app config""" """authentik rac app config"""
name = "authentik.providers.rac" name = "authentik.providers.rac"

View File

@ -4,8 +4,7 @@ from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer from channels.layers import get_channel_layer
from django.contrib.auth.signals import user_logged_out from django.contrib.auth.signals import user_logged_out
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Model from django.db.models.signals import post_delete, post_save, pre_delete
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.http import HttpRequest from django.http import HttpRequest
@ -46,12 +45,8 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
) )
@receiver(post_save, sender=Endpoint) @receiver([post_save, post_delete], sender=Endpoint)
def post_save_endpoint(sender: type[Model], instance, created: bool, **_): def post_save_post_delete_endpoint(**_):
"""Clear user's endpoint cache upon endpoint creation""" """Clear user's endpoint cache upon endpoint creation or deletion"""
if not created: # pragma: no cover
return
# Delete user endpoint cache
keys = cache.keys(user_endpoint_cache_key("*")) keys = cache.keys(user_endpoint_cache_key("*"))
cache.delete_many(keys) cache.delete_many(keys)

View File

@ -180,6 +180,7 @@ class SAMLProviderSerializer(ProviderSerializer):
"session_valid_not_on_or_after", "session_valid_not_on_or_after",
"property_mappings", "property_mappings",
"name_id_mapping", "name_id_mapping",
"authn_context_class_ref_mapping",
"digest_algorithm", "digest_algorithm",
"signature_algorithm", "signature_algorithm",
"signing_kp", "signing_kp",

View File

@ -0,0 +1,28 @@
# Generated by Django 5.0.13 on 2025-03-18 17:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0016_samlprovider_encryption_kp_and_more"),
]
operations = [
migrations.AddField(
model_name="samlprovider",
name="authn_context_class_ref_mapping",
field=models.ForeignKey(
blank=True,
default=None,
help_text="Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="authentik_providers_saml.samlpropertymapping",
verbose_name="AuthnContextClassRef Property Mapping",
),
),
]

View File

@ -71,6 +71,20 @@ class SAMLProvider(Provider):
"the NameIDPolicy of the incoming request will be considered" "the NameIDPolicy of the incoming request will be considered"
), ),
) )
authn_context_class_ref_mapping = models.ForeignKey(
"SAMLPropertyMapping",
default=None,
blank=True,
null=True,
on_delete=models.SET_DEFAULT,
verbose_name=_("AuthnContextClassRef Property Mapping"),
related_name="+",
help_text=_(
"Configure how the AuthnContextClassRef value will be created. When left empty, "
"the AuthnContextClassRef will be set based on which authentication methods the user "
"used to authenticate."
),
)
assertion_valid_not_before = models.TextField( assertion_valid_not_before = models.TextField(
default="minutes=-5", default="minutes=-5",
@ -170,7 +184,6 @@ class SAMLProvider(Provider):
def launch_url(self) -> str | None: def launch_url(self) -> str | None:
"""Use IDP-Initiated SAML flow as launch URL""" """Use IDP-Initiated SAML flow as launch URL"""
try: try:
return reverse( return reverse(
"authentik_providers_saml:sso-init", "authentik_providers_saml:sso-init",
kwargs={"application_slug": self.application.slug}, kwargs={"application_slug": self.application.slug},

View File

@ -1,5 +1,6 @@
"""SAML Assertion generator""" """SAML Assertion generator"""
from datetime import datetime
from hashlib import sha256 from hashlib import sha256
from types import GeneratorType from types import GeneratorType
@ -52,6 +53,7 @@ class AssertionProcessor:
_assertion_id: str _assertion_id: str
_response_id: str _response_id: str
_auth_instant: str
_valid_not_before: str _valid_not_before: str
_session_not_on_or_after: str _session_not_on_or_after: str
_valid_not_on_or_after: str _valid_not_on_or_after: str
@ -65,6 +67,11 @@ class AssertionProcessor:
self._assertion_id = get_random_id() self._assertion_id = get_random_id()
self._response_id = get_random_id() self._response_id = get_random_id()
_login_event = get_login_event(self.http_request)
_login_time = datetime.now()
if _login_event:
_login_time = _login_event.created
self._auth_instant = get_time_string(_login_time)
self._valid_not_before = get_time_string( self._valid_not_before = get_time_string(
timedelta_from_string(self.provider.assertion_valid_not_before) timedelta_from_string(self.provider.assertion_valid_not_before)
) )
@ -131,7 +138,7 @@ class AssertionProcessor:
def get_assertion_auth_n_statement(self) -> Element: def get_assertion_auth_n_statement(self) -> Element:
"""Generate AuthnStatement with AuthnContext and ContextClassRef Elements.""" """Generate AuthnStatement with AuthnContext and ContextClassRef Elements."""
auth_n_statement = Element(f"{{{NS_SAML_ASSERTION}}}AuthnStatement") auth_n_statement = Element(f"{{{NS_SAML_ASSERTION}}}AuthnStatement")
auth_n_statement.attrib["AuthnInstant"] = self._valid_not_before auth_n_statement.attrib["AuthnInstant"] = self._auth_instant
auth_n_statement.attrib["SessionIndex"] = sha256( auth_n_statement.attrib["SessionIndex"] = sha256(
self.http_request.session.session_key.encode("ascii") self.http_request.session.session_key.encode("ascii")
).hexdigest() ).hexdigest()
@ -158,6 +165,28 @@ class AssertionProcessor:
auth_n_context_class_ref.text = ( auth_n_context_class_ref.text = (
"urn:oasis:names:tc:SAML:2.0:ac:classes:MobileOneFactorContract" "urn:oasis:names:tc:SAML:2.0:ac:classes:MobileOneFactorContract"
) )
if self.provider.authn_context_class_ref_mapping:
try:
value = self.provider.authn_context_class_ref_mapping.evaluate(
user=self.http_request.user,
request=self.http_request,
provider=self.provider,
)
if value is not None:
auth_n_context_class_ref.text = str(value)
return auth_n_statement
except PropertyMappingExpressionException as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=(
"Failed to evaluate property-mapping: "
f"'{self.provider.authn_context_class_ref_mapping.name}'"
),
provider=self.provider,
mapping=self.provider.authn_context_class_ref_mapping,
).from_http(self.http_request)
LOGGER.warning("Failed to evaluate property mapping", exc=exc)
return auth_n_statement
return auth_n_statement return auth_n_statement
def get_assertion_conditions(self) -> Element: def get_assertion_conditions(self) -> Element:

View File

@ -294,6 +294,61 @@ class TestAuthNRequest(TestCase):
self.assertEqual(parsed_request.id, "aws_LDxLGeubpc5lx12gxCgS6uPbix1yd5re") self.assertEqual(parsed_request.id, "aws_LDxLGeubpc5lx12gxCgS6uPbix1yd5re")
self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_EMAIL) self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_EMAIL)
def test_authn_context_class_ref_mapping(self):
"""Test custom authn_context_class_ref"""
authn_context_class_ref = generate_id()
mapping = SAMLPropertyMapping.objects.create(
name=generate_id(), expression=f"""return '{authn_context_class_ref}'"""
)
self.provider.authn_context_class_ref_mapping = mapping
self.provider.save()
user = create_test_admin_user()
http_request = get_request("/", user=user)
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")
request = request_proc.build_auth_n()
# To get an assertion we need a parsed request (parsed by provider)
parsed_request = AuthNRequestParser(self.provider).parse(
b64encode(request.encode()).decode(), "test_state"
)
# Now create a response and convert it to string (provider)
response_proc = AssertionProcessor(self.provider, http_request, parsed_request)
response = response_proc.build_response()
self.assertIn(user.username, response)
self.assertIn(authn_context_class_ref, response)
def test_authn_context_class_ref_mapping_invalid(self):
"""Test custom authn_context_class_ref (invalid)"""
mapping = SAMLPropertyMapping.objects.create(name=generate_id(), expression="q")
self.provider.authn_context_class_ref_mapping = mapping
self.provider.save()
user = create_test_admin_user()
http_request = get_request("/", user=user)
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")
request = request_proc.build_auth_n()
# To get an assertion we need a parsed request (parsed by provider)
parsed_request = AuthNRequestParser(self.provider).parse(
b64encode(request.encode()).decode(), "test_state"
)
# Now create a response and convert it to string (provider)
response_proc = AssertionProcessor(self.provider, http_request, parsed_request)
response = response_proc.build_response()
self.assertIn(user.username, response)
events = Event.objects.filter(
action=EventAction.CONFIGURATION_ERROR,
)
self.assertTrue(events.exists())
self.assertEqual(
events.first().context["message"],
f"Failed to evaluate property-mapping: '{mapping.name}'",
)
def test_request_attributes(self): def test_request_attributes(self):
"""Test full SAML Request/Response flow, fully signed""" """Test full SAML Request/Response flow, fully signed"""
user = create_test_admin_user() user = create_test_admin_user()
@ -321,8 +376,10 @@ class TestAuthNRequest(TestCase):
request = request_proc.build_auth_n() request = request_proc.build_auth_n()
# Create invalid PropertyMapping # Create invalid PropertyMapping
scope = SAMLPropertyMapping.objects.create(name="test", saml_name="test", expression="q") mapping = SAMLPropertyMapping.objects.create(
self.provider.property_mappings.add(scope) name=generate_id(), saml_name="test", expression="q"
)
self.provider.property_mappings.add(mapping)
# To get an assertion we need a parsed request (parsed by provider) # To get an assertion we need a parsed request (parsed by provider)
parsed_request = AuthNRequestParser(self.provider).parse( parsed_request = AuthNRequestParser(self.provider).parse(
@ -338,7 +395,7 @@ class TestAuthNRequest(TestCase):
self.assertTrue(events.exists()) self.assertTrue(events.exists())
self.assertEqual( self.assertEqual(
events.first().context["message"], events.first().context["message"],
"Failed to evaluate property-mapping: 'test'", f"Failed to evaluate property-mapping: '{mapping.name}'",
) )
def test_idp_initiated(self): def test_idp_initiated(self):

View File

@ -1,12 +1,16 @@
"""Time utilities""" """Time utilities"""
import datetime from datetime import datetime, timedelta
from django.utils.timezone import now
def get_time_string(delta: datetime.timedelta | None = None) -> str: def get_time_string(delta: timedelta | datetime | None = None) -> str:
"""Get Data formatted in SAML format""" """Get Data formatted in SAML format"""
if delta is None: if delta is None:
delta = datetime.timedelta() delta = timedelta()
now = datetime.datetime.now() if isinstance(delta, timedelta):
final = now + delta final = now() + delta
else:
final = delta
return final.strftime("%Y-%m-%dT%H:%M:%SZ") return final.strftime("%Y-%m-%dT%H:%M:%SZ")

View File

@ -24,7 +24,9 @@ class SCIMProviderGroupSerializer(ModelSerializer):
"group", "group",
"group_obj", "group_obj",
"provider", "provider",
"attributes",
] ]
extra_kwargs = {"attributes": {"read_only": True}}
class SCIMProviderGroupViewSet( class SCIMProviderGroupViewSet(

View File

@ -28,6 +28,7 @@ class SCIMProviderSerializer(ProviderSerializer):
"url", "url",
"verify_certificates", "verify_certificates",
"token", "token",
"compatibility_mode",
"exclude_users_service_account", "exclude_users_service_account",
"filter_group", "filter_group",
"dry_run", "dry_run",

View File

@ -24,7 +24,9 @@ class SCIMProviderUserSerializer(ModelSerializer):
"user", "user",
"user_obj", "user_obj",
"provider", "provider",
"attributes",
] ]
extra_kwargs = {"attributes": {"read_only": True}}
class SCIMProviderUserViewSet( class SCIMProviderUserViewSet(

View File

@ -22,7 +22,7 @@ from authentik.lib.sync.outgoing.exceptions import (
from authentik.lib.utils.http import get_http_session from authentik.lib.utils.http import get_http_session
from authentik.providers.scim.clients.exceptions import SCIMRequestException from authentik.providers.scim.clients.exceptions import SCIMRequestException
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
from authentik.providers.scim.models import SCIMProvider from authentik.providers.scim.models import SCIMCompatibilityMode, SCIMProvider
if TYPE_CHECKING: if TYPE_CHECKING:
from django.db.models import Model from django.db.models import Model
@ -90,9 +90,14 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
"""Get Service provider config""" """Get Service provider config"""
default_config = ServiceProviderConfiguration.default() default_config = ServiceProviderConfiguration.default()
try: try:
return ServiceProviderConfiguration.model_validate( config = ServiceProviderConfiguration.model_validate(
self._request("GET", "/ServiceProviderConfig") self._request("GET", "/ServiceProviderConfig")
) )
if self.provider.compatibility_mode == SCIMCompatibilityMode.AWS:
config.patch.supported = False
if self.provider.compatibility_mode == SCIMCompatibilityMode.SLACK:
config.filter.supported = True
return config
except (ValidationError, SCIMRequestException, NotFoundSyncException) as exc: except (ValidationError, SCIMRequestException, NotFoundSyncException) as exc:
self.logger.warning("failed to get ServiceProviderConfig", exc=exc) self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
return default_config return default_config

View File

@ -102,7 +102,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
if not scim_id or scim_id == "": if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`") raise StopSync("SCIM Response with missing or invalid `id`")
connection = SCIMProviderGroup.objects.create( connection = SCIMProviderGroup.objects.create(
provider=self.provider, group=group, scim_id=scim_id provider=self.provider, group=group, scim_id=scim_id, attributes=response
) )
users = list(group.users.order_by("id").values_list("id", flat=True)) users = list(group.users.order_by("id").values_list("id", flat=True))
self._patch_add_users(connection, users) self._patch_add_users(connection, users)

View File

@ -77,21 +77,24 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
if len(users_res) < 1: if len(users_res) < 1:
raise exc raise exc
return SCIMProviderUser.objects.create( return SCIMProviderUser.objects.create(
provider=self.provider, user=user, scim_id=users_res[0]["id"] provider=self.provider,
user=user,
scim_id=users_res[0]["id"],
attributes=users_res[0],
) )
else: else:
scim_id = response.get("id") scim_id = response.get("id")
if not scim_id or scim_id == "": if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`") raise StopSync("SCIM Response with missing or invalid `id`")
return SCIMProviderUser.objects.create( return SCIMProviderUser.objects.create(
provider=self.provider, user=user, scim_id=scim_id provider=self.provider, user=user, scim_id=scim_id, attributes=response
) )
def update(self, user: User, connection: SCIMProviderUser): def update(self, user: User, connection: SCIMProviderUser):
"""Update existing user""" """Update existing user"""
scim_user = self.to_schema(user, connection) scim_user = self.to_schema(user, connection)
scim_user.id = connection.scim_id scim_user.id = connection.scim_id
self._request( response = self._request(
"PUT", "PUT",
f"/Users/{connection.scim_id}", f"/Users/{connection.scim_id}",
json=scim_user.model_dump( json=scim_user.model_dump(
@ -99,3 +102,5 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
exclude_unset=True, exclude_unset=True,
), ),
) )
connection.attributes = response
connection.save()

View File

@ -0,0 +1,24 @@
# Generated by Django 5.0.12 on 2025-03-07 23:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_scim", "0011_scimprovider_dry_run"),
]
operations = [
migrations.AddField(
model_name="scimprovider",
name="compatibility_mode",
field=models.CharField(
choices=[("default", "Default"), ("aws", "AWS"), ("slack", "Slack")],
default="default",
help_text="Alter authentik behavior for vendor-specific SCIM implementations.",
max_length=30,
verbose_name="SCIM Compatibility Mode",
),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.13 on 2025-03-18 13:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_scim", "0012_scimprovider_compatibility_mode"),
]
operations = [
migrations.AddField(
model_name="scimprovidergroup",
name="attributes",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="scimprovideruser",
name="attributes",
field=models.JSONField(default=dict),
),
]

View File

@ -22,6 +22,7 @@ class SCIMProviderUser(SerializerModel):
scim_id = models.TextField() scim_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey("SCIMProvider", on_delete=models.CASCADE) provider = models.ForeignKey("SCIMProvider", on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property @property
def serializer(self) -> type[Serializer]: def serializer(self) -> type[Serializer]:
@ -43,6 +44,7 @@ class SCIMProviderGroup(SerializerModel):
scim_id = models.TextField() scim_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey("SCIMProvider", on_delete=models.CASCADE) provider = models.ForeignKey("SCIMProvider", on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property @property
def serializer(self) -> type[Serializer]: def serializer(self) -> type[Serializer]:
@ -57,6 +59,14 @@ class SCIMProviderGroup(SerializerModel):
return f"SCIM Provider Group {self.group_id} to {self.provider_id}" return f"SCIM Provider Group {self.group_id} to {self.provider_id}"
class SCIMCompatibilityMode(models.TextChoices):
"""SCIM compatibility mode"""
DEFAULT = "default", _("Default")
AWS = "aws", _("AWS")
SLACK = "slack", _("Slack")
class SCIMProvider(OutgoingSyncProvider, BackchannelProvider): class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
"""SCIM 2.0 provider to create users and groups in external applications""" """SCIM 2.0 provider to create users and groups in external applications"""
@ -77,6 +87,14 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
help_text=_("Property mappings used for group creation/updating."), help_text=_("Property mappings used for group creation/updating."),
) )
compatibility_mode = models.CharField(
max_length=30,
choices=SCIMCompatibilityMode.choices,
default=SCIMCompatibilityMode.DEFAULT,
verbose_name=_("SCIM Compatibility Mode"),
help_text=_("Alter authentik behavior for vendor-specific SCIM implementations."),
)
@property @property
def icon_url(self) -> str | None: def icon_url(self) -> str | None:
return static("authentik/sources/scim.png") return static("authentik/sources/scim.png")

View File

@ -68,8 +68,6 @@ class OAuth2Client(BaseOAuthClient):
error_desc = self.get_request_arg("error_description", None) error_desc = self.get_request_arg("error_description", None)
return {"error": error_desc or error or _("No token received.")} return {"error": error_desc or error or _("No token received.")}
args = { args = {
"client_id": self.get_client_id(),
"client_secret": self.get_client_secret(),
"redirect_uri": callback, "redirect_uri": callback,
"code": code, "code": code,
"grant_type": "authorization_code", "grant_type": "authorization_code",

View File

@ -28,7 +28,7 @@ def update_well_known_jwks(self: SystemTask):
LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text) LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text)
messages.append(f"Failed to update OIDC configuration for {source.slug}") messages.append(f"Failed to update OIDC configuration for {source.slug}")
continue continue
config = well_known_config.json() config: dict = well_known_config.json()
try: try:
dirty = False dirty = False
source_attr_key = ( source_attr_key = (
@ -40,7 +40,9 @@ def update_well_known_jwks(self: SystemTask):
for source_attr, config_key in source_attr_key: for source_attr, config_key in source_attr_key:
# Check if we're actually changing anything to only # Check if we're actually changing anything to only
# save when something has changed # save when something has changed
if getattr(source, source_attr, "") != config[config_key]: if config_key not in config:
continue
if getattr(source, source_attr, "") != config.get(config_key, ""):
dirty = True dirty = True
setattr(source, source_attr, config[config_key]) setattr(source, source_attr, config[config_key])
except (IndexError, KeyError) as exc: except (IndexError, KeyError) as exc:

View File

@ -25,8 +25,10 @@ class RedditOAuth2Client(UserprofileHeaderAuthClient):
def get_access_token(self, **request_kwargs): def get_access_token(self, **request_kwargs):
"Fetch access token from callback request." "Fetch access token from callback request."
auth = HTTPBasicAuth(self.source.consumer_key, self.source.consumer_secret) request_kwargs["auth"] = HTTPBasicAuth(
return super().get_access_token(auth=auth) self.source.consumer_key, self.source.consumer_secret
)
return super().get_access_token(**request_kwargs)
class RedditOAuth2Callback(OAuthCallback): class RedditOAuth2Callback(OAuthCallback):

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,54 @@
# Generated by Django 5.0.12 on 2025-02-27 04:32
import authentik.lib.utils.time
from authentik.lib.utils.time import timedelta_from_string
from django.db import migrations, models
def convert_integer_to_string_format(apps, schema_editor):
db_alias = schema_editor.connection.alias
EmailStage = apps.get_model("authentik_stages_email", "EmailStage")
for stage in EmailStage.objects.using(db_alias).all():
stage.token_expiry = f"minutes={stage.token_expiry}"
stage.save(using=db_alias)
def convert_string_to_integer_format(apps, schema_editor):
db_alias = schema_editor.connection.alias
EmailStage = apps.get_model("authentik_stages_email", "EmailStage")
for stage in EmailStage.objects.using(db_alias).all():
# Check if token_expiry is a string
if isinstance(stage.token_expiry, str):
try:
# Use the timedelta_from_string utility to convert to timedelta
# then convert to minutes by dividing seconds by 60
td = timedelta_from_string(stage.token_expiry)
minutes_value = int(td.total_seconds() / 60)
stage.token_expiry = minutes_value
stage.save(using=db_alias)
except (ValueError, TypeError):
# If the string can't be parsed or converted properly, skip
pass
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_email", "0004_emailstage_activate_user_on_success"),
]
operations = [
migrations.AlterField(
model_name="emailstage",
name="token_expiry",
field=models.TextField(
default="minutes=30",
help_text="Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
migrations.RunPython(
convert_integer_to_string_format,
convert_string_to_integer_format,
),
]

View File

@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
from authentik.flows.models import Stage from authentik.flows.models import Stage
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.utils.time import timedelta_string_validator
LOGGER = get_logger() LOGGER = get_logger()
@ -74,8 +75,10 @@ class EmailStage(Stage):
default=False, help_text=_("Activate users upon completion of stage.") default=False, help_text=_("Activate users upon completion of stage.")
) )
token_expiry = models.IntegerField( token_expiry = models.TextField(
default=30, help_text=_("Time in minutes the token sent is valid.") default="minutes=30",
validators=[timedelta_string_validator],
help_text=_("Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."),
) )
subject = models.TextField(default="authentik") subject = models.TextField(default="authentik")
template = models.TextField(default=EmailTemplates.PASSWORD_RESET) template = models.TextField(default=EmailTemplates.PASSWORD_RESET)

View File

@ -22,6 +22,7 @@ from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDI
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.time import timedelta_from_string
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
@ -73,8 +74,8 @@ class EmailStageView(ChallengeStageView):
"""Get token""" """Get token"""
pending_user = self.get_pending_user() pending_user = self.get_pending_user()
current_stage: EmailStage = self.executor.current_stage current_stage: EmailStage = self.executor.current_stage
valid_delta = timedelta( valid_delta = timedelta_from_string(current_stage.token_expiry) + timedelta(
minutes=current_stage.token_expiry + 1 minutes=1
) # + 1 because django timesince always rounds down ) # + 1 because django timesince always rounds down
identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}") identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
# Don't check for validity here, we only care if the token exists # Don't check for validity here, we only care if the token exists

View File

@ -142,38 +142,35 @@ class IdentificationChallengeResponse(ChallengeResponse):
raise ValidationError("Failed to authenticate.") raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user self.pre_user = pre_user
# Password check
if current_stage.password_stage:
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
name="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
# Captcha check # Captcha check
if captcha_stage := current_stage.captcha_stage: if captcha_stage := current_stage.captcha_stage:
captcha_token = attrs.get("captcha_token", None) captcha_token = attrs.get("captcha_token", None)
if not captcha_token: if not captcha_token:
self.stage.logger.warning("Token not set for captcha attempt") self.stage.logger.warning("Token not set for captcha attempt")
verify_captcha_token(captcha_stage, captcha_token, client_ip) verify_captcha_token(captcha_stage, captcha_token, client_ip)
# Password check
if not current_stage.password_stage:
# No password stage select, don't validate the password
return attrs
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
name="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
return attrs return attrs

View File

@ -57,7 +57,7 @@ entries:
use_ssl: false use_ssl: false
timeout: 10 timeout: 10
from_address: system@authentik.local from_address: system@authentik.local
token_expiry: 30 token_expiry: minutes=30
subject: authentik subject: authentik
template: email/password_reset.html template: email/password_reset.html
activate_user_on_success: true activate_user_on_success: true

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json", "$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object", "type": "object",
"title": "authentik 2025.2.1 Blueprint schema", "title": "authentik 2025.2.2 Blueprint schema",
"required": [ "required": [
"version", "version",
"entries" "entries"
@ -6462,6 +6462,11 @@
"title": "NameID Property Mapping", "title": "NameID Property Mapping",
"description": "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be considered" "description": "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be considered"
}, },
"authn_context_class_ref_mapping": {
"type": "integer",
"title": "AuthnContextClassRef Property Mapping",
"description": "Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate."
},
"digest_algorithm": { "digest_algorithm": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -6661,6 +6666,16 @@
"title": "Token", "title": "Token",
"description": "Authentication token" "description": "Authentication token"
}, },
"compatibility_mode": {
"type": "string",
"enum": [
"default",
"aws",
"slack"
],
"title": "SCIM Compatibility Mode",
"description": "Alter authentik behavior for vendor-specific SCIM implementations."
},
"exclude_users_service_account": { "exclude_users_service_account": {
"type": "boolean", "type": "boolean",
"title": "Exclude users service account" "title": "Exclude users service account"
@ -11369,11 +11384,10 @@
"title": "From address" "title": "From address"
}, },
"token_expiry": { "token_expiry": {
"type": "integer", "type": "string",
"minimum": -2147483648, "minLength": 1,
"maximum": 2147483647,
"title": "Token expiry", "title": "Token expiry",
"description": "Time in minutes the token sent is valid." "description": "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
}, },
"subject": { "subject": {
"type": "string", "type": "string",

View File

@ -31,7 +31,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.1} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -54,7 +54,7 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.1} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

10
go.mod
View File

@ -6,7 +6,7 @@ toolchain go1.24.0
require ( require (
beryju.io/ldap v0.1.0 beryju.io/ldap v0.1.0
github.com/coreos/go-oidc/v3 v3.12.0 github.com/coreos/go-oidc/v3 v3.13.0
github.com/getsentry/sentry-go v0.31.1 github.com/getsentry/sentry-go v0.31.1
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.10 github.com/go-ldap/ldap/v3 v3.4.10
@ -29,7 +29,7 @@ require (
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025021.2 goauthentik.io/api/v3 v3.2025022.3
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.28.0 golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0 golang.org/x/sync v0.12.0
@ -76,9 +76,9 @@ require (
go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

22
go.sum
View File

@ -55,8 +55,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025021.2 h1:9y87piH47omtkWxQpKZaKai/+jh+cJdLxj5MC2Y/ZLI= goauthentik.io/api/v3 v3.2025022.3 h1:cipaxl0il4/s1fU2f6+CD7nzgAktbV0XD7r5qHh0fUc=
goauthentik.io/api/v3 v3.2025021.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2025022.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -313,8 +313,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -386,8 +386,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -449,8 +450,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -471,8 +472,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion()) return fmt.Sprintf("authentik@%s", FullVersion())
} }
const VERSION = "2025.2.1" const VERSION = "2025.2.2"

View File

@ -62,12 +62,12 @@ function prepare_debug {
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update apt-get update
apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server libkrb5-dev gcc apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server libkrb5-dev gcc
VIRTUAL_ENV=/ak-root/venv poetry install --no-ansi --no-interaction VIRTUAL_ENV=/ak-root/.venv uv sync --frozen
touch /unittest.xml touch /unittest.xml
chown authentik:authentik /unittest.xml chown authentik:authentik /unittest.xml
} }
if [[ "$(python -m authentik.lib.config debugger 2> /dev/null)" == "True" ]]; then if [[ "$(python -m authentik.lib.config debugger 2>/dev/null)" == "True" ]]; then
prepare_debug prepare_debug
fi fi

View File

@ -1,4 +1,4 @@
"""Wrapper for lifecycle/ak, to be installed by poetry""" """Wrapper for lifecycle/ak, to be installed by uv"""
from os import system, waitstatus_to_exitcode from os import system, waitstatus_to_exitcode
from pathlib import Path from pathlib import Path

View File

@ -9,7 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1003.0", "aws-cdk": "^2.1005.0",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
}, },
"engines": { "engines": {
@ -17,9 +17,9 @@
} }
}, },
"node_modules/aws-cdk": { "node_modules/aws-cdk": {
"version": "2.1003.0", "version": "2.1005.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1003.0.tgz", "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz",
"integrity": "sha512-FORPDGW8oUg4tXFlhX+lv/j+152LO9wwi3/CwNr1WY3c3HwJUtc0fZGb2B3+Fzy6NhLWGHJclUsJPEhjEt8Nhg==", "integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View File

@ -10,7 +10,7 @@
"node": ">=20" "node": ">=20"
}, },
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1003.0", "aws-cdk": "^2.1005.0",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
} }
} }

View File

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

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-02 00:10+0000\n" "POT-Creation-Date: 2025-03-13 00:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -1883,6 +1883,18 @@ msgstr ""
msgid "SAML Providers from Metadata" msgid "SAML Providers from Metadata"
msgstr "" msgstr ""
#: authentik/providers/scim/models.py
msgid "Default"
msgstr ""
#: authentik/providers/scim/models.py
msgid "AWS"
msgstr ""
#: authentik/providers/scim/models.py
msgid "Slack"
msgstr ""
#: authentik/providers/scim/models.py #: authentik/providers/scim/models.py
msgid "Base URL to SCIM requests, usually ends in /v2" msgid "Base URL to SCIM requests, usually ends in /v2"
msgstr "" msgstr ""
@ -1891,6 +1903,14 @@ msgstr ""
msgid "Authentication token" msgid "Authentication token"
msgstr "" msgstr ""
#: authentik/providers/scim/models.py
msgid "SCIM Compatibility Mode"
msgstr ""
#: authentik/providers/scim/models.py
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
msgstr ""
#: authentik/providers/scim/models.py #: authentik/providers/scim/models.py
msgid "SCIM Provider" msgid "SCIM Provider"
msgstr "" msgstr ""
@ -2535,6 +2555,7 @@ msgid ""
msgstr "" msgstr ""
#: authentik/stages/authenticator_email/models.py #: authentik/stages/authenticator_email/models.py
#: authentik/stages/email/models.py
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)." msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
msgstr "" msgstr ""
@ -2873,10 +2894,6 @@ msgstr ""
msgid "Activate users upon completion of stage." msgid "Activate users upon completion of stage."
msgstr "" msgstr ""
#: authentik/stages/email/models.py
msgid "Time in minutes the token sent is valid."
msgstr ""
#: authentik/stages/email/models.py #: authentik/stages/email/models.py
msgid "Email Stage" msgid "Email Stage"
msgstr "" msgstr ""

View File

@ -9,9 +9,9 @@
# Kyllian Delaye-Maillot, 2023 # Kyllian Delaye-Maillot, 2023
# Manuel Viens, 2023 # Manuel Viens, 2023
# Mordecai, 2023 # Mordecai, 2023
# Charles Leclerc, 2024
# nerdinator <florian.dupret@gmail.com>, 2024 # nerdinator <florian.dupret@gmail.com>, 2024
# Tina, 2024 # Tina, 2024
# Charles Leclerc, 2025
# Marc Schmitt, 2025 # Marc Schmitt, 2025
# #
#, fuzzy #, fuzzy
@ -19,7 +19,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-02 00:10+0000\n" "POT-Creation-Date: 2025-03-13 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n" "Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -2097,6 +2097,18 @@ msgstr "Fournisseur SAML depuis métadonnées"
msgid "SAML Providers from Metadata" msgid "SAML Providers from Metadata"
msgstr "Fournisseurs SAML depuis métadonnées" msgstr "Fournisseurs SAML depuis métadonnées"
#: authentik/providers/scim/models.py
msgid "Default"
msgstr "Par défaut"
#: authentik/providers/scim/models.py
msgid "AWS"
msgstr "AWS"
#: authentik/providers/scim/models.py
msgid "Slack"
msgstr "Slack"
#: authentik/providers/scim/models.py #: authentik/providers/scim/models.py
msgid "Base URL to SCIM requests, usually ends in /v2" msgid "Base URL to SCIM requests, usually ends in /v2"
msgstr "URL de base pour les requêtes SCIM, se terminant généralement par /v2" msgstr "URL de base pour les requêtes SCIM, se terminant généralement par /v2"
@ -2105,6 +2117,16 @@ msgstr "URL de base pour les requêtes SCIM, se terminant généralement par /v2
msgid "Authentication token" msgid "Authentication token"
msgstr "Jeton d'authentification" msgstr "Jeton d'authentification"
#: authentik/providers/scim/models.py
msgid "SCIM Compatibility Mode"
msgstr "Mode de compatibilité SCIM"
#: authentik/providers/scim/models.py
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
msgstr ""
"Change le comportement d'authentik en fonction des spécificités "
"d'implémentations des fournisseurs SCIM."
#: authentik/providers/scim/models.py #: authentik/providers/scim/models.py
msgid "SCIM Provider" msgid "SCIM Provider"
msgstr "Fournisseur SCIM" msgstr "Fournisseur SCIM"
@ -2797,6 +2819,7 @@ msgstr ""
"les paramètres de connexion ci-dessous seront ignorés." "les paramètres de connexion ci-dessous seront ignorés."
#: authentik/stages/authenticator_email/models.py #: authentik/stages/authenticator_email/models.py
#: authentik/stages/email/models.py
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)." msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
msgstr "" msgstr ""
"Durée de validité du jeton envoyé (Format : hours=3,minutes=17,seconds=300)." "Durée de validité du jeton envoyé (Format : hours=3,minutes=17,seconds=300)."
@ -3168,10 +3191,6 @@ msgstr "Confirmation du Compte"
msgid "Activate users upon completion of stage." msgid "Activate users upon completion of stage."
msgstr "Activer les utilisateurs à la complétion de l'étape." msgstr "Activer les utilisateurs à la complétion de l'étape."
#: authentik/stages/email/models.py
msgid "Time in minutes the token sent is valid."
msgstr "Temps en minutes durant lequel le jeton envoyé est valide."
#: authentik/stages/email/models.py #: authentik/stages/email/models.py
msgid "Email Stage" msgid "Email Stage"
msgstr "Étape Email" msgstr "Étape Email"

Binary file not shown.

View File

@ -7,7 +7,7 @@
# Chen Zhikai, 2022 # Chen Zhikai, 2022
# 刘松, 2022 # 刘松, 2022
# Tianhao Chai <cth451@gmail.com>, 2024 # Tianhao Chai <cth451@gmail.com>, 2024
# Jens L. <jens@goauthentik.io>, 2024 # Jens L. <jens@goauthentik.io>, 2025
# deluxghost, 2025 # deluxghost, 2025
# #
#, fuzzy #, fuzzy
@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-02 00:10+0000\n" "POT-Creation-Date: 2025-03-13 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n" "Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@ -1909,6 +1909,18 @@ msgstr "来自元数据的 SAML 提供程序"
msgid "SAML Providers from Metadata" msgid "SAML Providers from Metadata"
msgstr "来自元数据的 SAML 提供程序" msgstr "来自元数据的 SAML 提供程序"
#: authentik/providers/scim/models.py
msgid "Default"
msgstr "默认"
#: authentik/providers/scim/models.py
msgid "AWS"
msgstr "AWS"
#: authentik/providers/scim/models.py
msgid "Slack"
msgstr "Slack"
#: authentik/providers/scim/models.py #: authentik/providers/scim/models.py
msgid "Base URL to SCIM requests, usually ends in /v2" msgid "Base URL to SCIM requests, usually ends in /v2"
msgstr "SCIM 请求的基础 URL通常以 /v2 结尾" msgstr "SCIM 请求的基础 URL通常以 /v2 结尾"
@ -1917,6 +1929,14 @@ msgstr "SCIM 请求的基础 URL通常以 /v2 结尾"
msgid "Authentication token" msgid "Authentication token"
msgstr "身份验证令牌" msgstr "身份验证令牌"
#: authentik/providers/scim/models.py
msgid "SCIM Compatibility Mode"
msgstr "SCIM 兼容模式"
#: authentik/providers/scim/models.py
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
msgstr "更改 authentik 的行为,以兼容特定厂商的 SCIM 实现。"
#: authentik/providers/scim/models.py #: authentik/providers/scim/models.py
msgid "SCIM Provider" msgid "SCIM Provider"
msgstr "SCIM 提供程序" msgstr "SCIM 提供程序"
@ -2571,6 +2591,7 @@ msgid ""
msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。" msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。"
#: authentik/stages/authenticator_email/models.py #: authentik/stages/authenticator_email/models.py
#: authentik/stages/email/models.py
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)." msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
msgstr "发出令牌有效的时间格式hours=3,minutes=17,seconds=300。" msgstr "发出令牌有效的时间格式hours=3,minutes=17,seconds=300。"
@ -2920,10 +2941,6 @@ msgstr "账户确认"
msgid "Activate users upon completion of stage." msgid "Activate users upon completion of stage."
msgstr "完成阶段后激活用户。" msgstr "完成阶段后激活用户。"
#: authentik/stages/email/models.py
msgid "Time in minutes the token sent is valid."
msgstr "发出令牌的有效时间(单位为分钟)。"
#: authentik/stages/email/models.py #: authentik/stages/email/models.py
msgid "Email Stage" msgid "Email Stage"
msgstr "电子邮件阶段" msgstr "电子邮件阶段"

Binary file not shown.

View File

@ -6,7 +6,7 @@
# Translators: # Translators:
# Chen Zhikai, 2022 # Chen Zhikai, 2022
# 刘松, 2022 # 刘松, 2022
# Jens L. <jens@goauthentik.io>, 2024 # Jens L. <jens@goauthentik.io>, 2025
# deluxghost, 2025 # deluxghost, 2025
# #
#, fuzzy #, fuzzy
@ -14,7 +14,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-02 00:10+0000\n" "POT-Creation-Date: 2025-03-13 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n" "Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -1908,6 +1908,18 @@ msgstr "来自元数据的 SAML 提供程序"
msgid "SAML Providers from Metadata" msgid "SAML Providers from Metadata"
msgstr "来自元数据的 SAML 提供程序" msgstr "来自元数据的 SAML 提供程序"
#: authentik/providers/scim/models.py
msgid "Default"
msgstr "默认"
#: authentik/providers/scim/models.py
msgid "AWS"
msgstr "AWS"
#: authentik/providers/scim/models.py
msgid "Slack"
msgstr "Slack"
#: authentik/providers/scim/models.py #: authentik/providers/scim/models.py
msgid "Base URL to SCIM requests, usually ends in /v2" msgid "Base URL to SCIM requests, usually ends in /v2"
msgstr "SCIM 请求的基础 URL通常以 /v2 结尾" msgstr "SCIM 请求的基础 URL通常以 /v2 结尾"
@ -1916,6 +1928,14 @@ msgstr "SCIM 请求的基础 URL通常以 /v2 结尾"
msgid "Authentication token" msgid "Authentication token"
msgstr "身份验证令牌" msgstr "身份验证令牌"
#: authentik/providers/scim/models.py
msgid "SCIM Compatibility Mode"
msgstr "SCIM 兼容模式"
#: authentik/providers/scim/models.py
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
msgstr "更改 authentik 的行为,以兼容特定厂商的 SCIM 实现。"
#: authentik/providers/scim/models.py #: authentik/providers/scim/models.py
msgid "SCIM Provider" msgid "SCIM Provider"
msgstr "SCIM 提供程序" msgstr "SCIM 提供程序"
@ -2570,6 +2590,7 @@ msgid ""
msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。" msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。"
#: authentik/stages/authenticator_email/models.py #: authentik/stages/authenticator_email/models.py
#: authentik/stages/email/models.py
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)." msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
msgstr "发出令牌有效的时间格式hours=3,minutes=17,seconds=300。" msgstr "发出令牌有效的时间格式hours=3,minutes=17,seconds=300。"
@ -2919,10 +2940,6 @@ msgstr "账户确认"
msgid "Activate users upon completion of stage." msgid "Activate users upon completion of stage."
msgstr "完成阶段后激活用户。" msgstr "完成阶段后激活用户。"
#: authentik/stages/email/models.py
msgid "Time in minutes the token sent is valid."
msgstr "发出令牌的有效时间(单位为分钟)。"
#: authentik/stages/email/models.py #: authentik/stages/email/models.py
msgid "Email Stage" msgid "Email Stage"
msgstr "电子邮件阶段" msgstr "电子邮件阶段"

12
package-lock.json generated Normal file
View File

@ -0,0 +1,12 @@
{
"name": "@goauthentik/authentik",
"version": "2025.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/authentik",
"version": "2025.2.1"
}
}
}

View File

@ -1,5 +1,5 @@
{ {
"name": "@goauthentik/authentik", "name": "@goauthentik/authentik",
"version": "2025.2.1", "version": "2025.2.2",
"private": true "private": true
} }

6359
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,119 @@
[tool.poetry] [project]
name = "authentik" name = "authentik"
version = "2025.2.1" version = "2025.2.2"
description = "" description = ""
authors = ["authentik Team <hello@goauthentik.io>"] authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.12.*"
dependencies = [
"argon2-cffi",
"celery",
"channels",
"channels-redis",
"cryptography",
"dacite",
"deepmerge",
"defusedxml",
"django",
"django-countries",
"django-cte",
"django-filter",
"django-guardian",
"django-model-utils",
"django-pglock",
"django-pgtrigger",
"django-prometheus",
"django-redis",
"django-storages[s3]",
"django-tenants",
"djangorestframework ==3.14.0",
"djangorestframework-guardian",
"docker",
"dramatiq[watch]",
"drf-orjson-renderer",
"drf-spectacular",
"dumb-init",
"duo-client",
"fido2",
"flower",
"geoip2",
"geopy",
"google-api-python-client",
"gssapi",
"gunicorn",
"jsonpatch",
"jwcrypto",
"kubernetes",
"ldap3",
"lxml",
"msgraph-sdk",
"opencontainers",
"packaging",
"paramiko",
"psycopg[c]",
"pydantic",
"pydantic-scim",
"pyjwt",
"pyrad",
"python-kadmin-rs ==0.5.3",
"pyyaml",
"requests-oauthlib",
"scim2-filter-parser",
"sentry-sdk",
"service_identity",
"setproctitle",
"structlog",
"swagger-spec-validator",
"tenacity",
"tenant-schemas-celery",
"twilio",
"ua-parser",
"unidecode",
"urllib3 <3",
"uvicorn[standard]",
"watchdog",
"webauthn",
"wsproto",
"xmlsec <= 1.3.14",
"zxcvbn",
]
[dependency-groups]
dev = [
"aws-cdk-lib",
"bandit",
"black",
"bump2version",
"channels[daphne]",
"codespell",
"colorama",
"constructs",
"coverage[toml]",
"debugpy",
"drf-jsonschema-serializer",
"freezegun",
"importlib-metadata",
"k5test",
"pdoc",
"pytest",
"pytest-django",
"pytest-github-actions-annotate-failures",
"pytest-randomly",
"pytest-timeout",
"requests-mock",
"ruff",
"selenium",
]
[tool.uv.sources]
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" }
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf" }
[project.scripts]
ak = "lifecycle.ak:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.bandit] [tool.bandit]
exclude_dirs = ["**/node_modules/**"] exclude_dirs = ["**/node_modules/**"]
@ -20,6 +131,7 @@ skip = [
"**/storybook-static", "**/storybook-static",
"**/web/src/locales", "**/web/src/locales",
"**/web/xliff", "**/web/xliff",
"./web/storybook-static",
"./website/build", "./website/build",
"./gen-ts-api", "./gen-ts-api",
"./gen-py-api", "./gen-py-api",
@ -30,6 +142,7 @@ skip = [
] ]
dictionary = ".github/codespell-dictionary.txt,-" dictionary = ".github/codespell-dictionary.txt,-"
ignore-words = ".github/codespell-words.txt" ignore-words = ".github/codespell-words.txt"
[tool.black] [tool.black]
line-length = 100 line-length = 100
target-version = ['py312'] target-version = ['py312']
@ -60,6 +173,7 @@ select = [
ignore = [ ignore = [
"DJ001", # Avoid using `null=True` on string-based fields, "DJ001", # Avoid using `null=True` on string-based fields,
] ]
[tool.ruff.lint.pylint] [tool.ruff.lint.pylint]
max-args = 7 max-args = 7
max-branches = 18 max-branches = 18
@ -107,112 +221,3 @@ filterwarnings = [
"ignore:defusedxml.lxml is no longer supported and will be removed in a future release.:DeprecationWarning", "ignore:defusedxml.lxml is no longer supported and will be removed in a future release.:DeprecationWarning",
"ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning", "ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning",
] ]
[tool.poetry.dependencies]
argon2-cffi = "*"
celery = "*"
channels = "*"
channels-redis = "*"
cryptography = "*"
dacite = "*"
deepmerge = "*"
defusedxml = "*"
django = "*"
django-countries = "*"
django-cte = "*"
django-filter = "*"
django-guardian = "*"
django-model-utils = "*"
django-pglock = "*"
django-pgtrigger = "*"
django-prometheus = "*"
django-redis = "*"
django-storages = { extras = ["s3"], version = "*" }
# See https://github.com/django-tenants/django-tenants/pull/997
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" }
djangorestframework = "3.14.0"
djangorestframework-guardian = "*"
docker = "*"
dramatiq = { version = "*", extras = ["watch"] }
drf-orjson-renderer = "*"
drf-spectacular = "*"
dumb-init = "*"
duo-client = "*"
fido2 = "*"
flower = "*"
geoip2 = "*"
geopy = "*"
google-api-python-client = "*"
gunicorn = "*"
gssapi = "*"
jsonpatch = "*"
jwcrypto = "*"
kubernetes = "*"
ldap3 = "*"
lxml = "*"
msgraph-sdk = "*"
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = [
"reggie",
] }
packaging = "*"
paramiko = "*"
psycopg = { extras = ["c"], version = "*" }
pydantic = "*"
pydantic-scim = "*"
pyjwt = "*"
pyrad = "*"
python = "~3.12"
python-kadmin-rs = "0.5.3"
pyyaml = "*"
requests-oauthlib = "*"
scim2-filter-parser = "*"
sentry-sdk = "*"
service_identity = "*"
setproctitle = "*"
structlog = "*"
swagger-spec-validator = "*"
tenacity = "*"
tenant-schemas-celery = "*"
twilio = "*"
ua-parser = "*"
unidecode = "*"
# Pinned because of botocore https://github.com/orgs/python-poetry/discussions/7937
urllib3 = { extras = ["secure"], version = "<3" }
uvicorn = { extras = ["standard"], version = "*" }
watchdog = "*"
webauthn = "*"
wsproto = "*"
xmlsec = "*"
zxcvbn = "*"
[tool.poetry.group.dev.dependencies]
aws-cdk-lib = "*"
bandit = "*"
black = "*"
bump2version = "*"
channels = { version = "*", extras = ["daphne"] }
codespell = "*"
colorama = "*"
constructs = "*"
coverage = { extras = ["toml"], version = "*" }
debugpy = "*"
drf-jsonschema-serializer = "*"
freezegun = "*"
importlib-metadata = "*"
k5test = "*"
pdoc = "*"
pytest = "*"
pytest-django = "*"
pytest-github-actions-annotate-failures = "*"
pytest-randomly = "*"
pytest-timeout = "*"
requests-mock = "*"
ruff = "*"
selenium = "*"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
ak = "lifecycle.ak:main"

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2025.2.1 version: 2025.2.2
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io
@ -22191,6 +22191,11 @@ paths:
schema: schema:
type: string type: string
format: uuid format: uuid
- in: query
name: authn_context_class_ref_mapping
schema:
type: string
format: uuid
- in: query - in: query
name: authorization_flow name: authorization_flow
schema: schema:
@ -25745,7 +25750,7 @@ paths:
description: '' description: ''
delete: delete:
operationId: sources_all_destroy operationId: sources_all_destroy
description: Source Viewset description: Prevent deletion of built-in sources
parameters: parameters:
- in: path - in: path
name: slug name: slug
@ -35146,7 +35151,7 @@ paths:
- in: query - in: query
name: token_expiry name: token_expiry
schema: schema:
type: integer type: string
- in: query - in: query
name: use_global_settings name: use_global_settings
schema: schema:
@ -41582,6 +41587,12 @@ components:
- confidential - confidential
- public - public
type: string type: string
CompatibilityModeEnum:
enum:
- default
- aws
- slack
type: string
Config: Config:
type: object type: object
description: Serialize authentik Config into DRF Object description: Serialize authentik Config into DRF Object
@ -42774,10 +42785,8 @@ components:
format: email format: email
maxLength: 254 maxLength: 254
token_expiry: token_expiry:
type: integer type: string
maximum: 2147483647 description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
minimum: -2147483648
description: Time in minutes the token sent is valid.
subject: subject:
type: string type: string
template: template:
@ -42833,10 +42842,9 @@ components:
minLength: 1 minLength: 1
maxLength: 254 maxLength: 254
token_expiry: token_expiry:
type: integer type: string
maximum: 2147483647 minLength: 1
minimum: -2147483648 description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
description: Time in minutes the token sent is valid.
subject: subject:
type: string type: string
minLength: 1 minLength: 1
@ -50389,10 +50397,9 @@ components:
minLength: 1 minLength: 1
maxLength: 254 maxLength: 254
token_expiry: token_expiry:
type: integer type: string
maximum: 2147483647 minLength: 1
minimum: -2147483648 description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
description: Time in minutes the token sent is valid.
subject: subject:
type: string type: string
minLength: 1 minLength: 1
@ -52226,6 +52233,14 @@ components:
title: NameID Property Mapping title: NameID Property Mapping
description: Configure how the NameID value will be created. When left empty, description: Configure how the NameID value will be created. When left empty,
the NameIDPolicy of the incoming request will be considered the NameIDPolicy of the incoming request will be considered
authn_context_class_ref_mapping:
type: string
format: uuid
nullable: true
title: AuthnContextClassRef Property Mapping
description: Configure how the AuthnContextClassRef value will be created.
When left empty, the AuthnContextClassRef will be set based on which authentication
methods the user used to authenticate.
digest_algorithm: digest_algorithm:
$ref: '#/components/schemas/DigestAlgorithmEnum' $ref: '#/components/schemas/DigestAlgorithmEnum'
signature_algorithm: signature_algorithm:
@ -52445,6 +52460,11 @@ components:
type: string type: string
minLength: 1 minLength: 1
description: Authentication token description: Authentication token
compatibility_mode:
allOf:
- $ref: '#/components/schemas/CompatibilityModeEnum'
title: SCIM Compatibility Mode
description: Alter authentik behavior for vendor-specific SCIM implementations.
exclude_users_service_account: exclude_users_service_account:
type: boolean type: boolean
filter_group: filter_group:
@ -55176,6 +55196,14 @@ components:
title: NameID Property Mapping title: NameID Property Mapping
description: Configure how the NameID value will be created. When left empty, description: Configure how the NameID value will be created. When left empty,
the NameIDPolicy of the incoming request will be considered the NameIDPolicy of the incoming request will be considered
authn_context_class_ref_mapping:
type: string
format: uuid
nullable: true
title: AuthnContextClassRef Property Mapping
description: Configure how the AuthnContextClassRef value will be created.
When left empty, the AuthnContextClassRef will be set based on which authentication
methods the user used to authenticate.
digest_algorithm: digest_algorithm:
$ref: '#/components/schemas/DigestAlgorithmEnum' $ref: '#/components/schemas/DigestAlgorithmEnum'
signature_algorithm: signature_algorithm:
@ -55341,6 +55369,14 @@ components:
title: NameID Property Mapping title: NameID Property Mapping
description: Configure how the NameID value will be created. When left empty, description: Configure how the NameID value will be created. When left empty,
the NameIDPolicy of the incoming request will be considered the NameIDPolicy of the incoming request will be considered
authn_context_class_ref_mapping:
type: string
format: uuid
nullable: true
title: AuthnContextClassRef Property Mapping
description: Configure how the AuthnContextClassRef value will be created.
When left empty, the AuthnContextClassRef will be set based on which authentication
methods the user used to authenticate.
digest_algorithm: digest_algorithm:
$ref: '#/components/schemas/DigestAlgorithmEnum' $ref: '#/components/schemas/DigestAlgorithmEnum'
signature_algorithm: signature_algorithm:
@ -55845,6 +55881,11 @@ components:
token: token:
type: string type: string
description: Authentication token description: Authentication token
compatibility_mode:
allOf:
- $ref: '#/components/schemas/CompatibilityModeEnum'
title: SCIM Compatibility Mode
description: Alter authentik behavior for vendor-specific SCIM implementations.
exclude_users_service_account: exclude_users_service_account:
type: boolean type: boolean
filter_group: filter_group:
@ -55885,7 +55926,10 @@ components:
readOnly: true readOnly: true
provider: provider:
type: integer type: integer
attributes:
readOnly: true
required: required:
- attributes
- group - group
- group_obj - group_obj
- id - id
@ -55935,6 +55979,11 @@ components:
type: string type: string
minLength: 1 minLength: 1
description: Authentication token description: Authentication token
compatibility_mode:
allOf:
- $ref: '#/components/schemas/CompatibilityModeEnum'
title: SCIM Compatibility Mode
description: Alter authentik behavior for vendor-specific SCIM implementations.
exclude_users_service_account: exclude_users_service_account:
type: boolean type: boolean
filter_group: filter_group:
@ -55967,7 +56016,10 @@ components:
readOnly: true readOnly: true
provider: provider:
type: integer type: integer
attributes:
readOnly: true
required: required:
- attributes
- id - id
- provider - provider
- scim_id - scim_id

View File

@ -7,6 +7,8 @@ services:
environment: environment:
POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DB: authentik POSTGRES_DB: authentik
command:
["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"]
ports: ports:
- 127.0.0.1:5432:5432 - 127.0.0.1:5432:5432
restart: always restart: always

3528
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@ const importInlinePatterns = [
'import AKGlobal from "@goauthentik/common/styles/authentik\\.css', 'import AKGlobal from "@goauthentik/common/styles/authentik\\.css',
'import PF.+ from "@patternfly/patternfly/\\S+\\.css', 'import PF.+ from "@patternfly/patternfly/\\S+\\.css',
'import ThemeDark from "@goauthentik/common/styles/theme-dark\\.css', 'import ThemeDark from "@goauthentik/common/styles/theme-dark\\.css',
'import OneDark from "@goauthentik/common/styles/one-dark\\.css',
'import styles from "\\./LibraryPageImpl\\.css', 'import styles from "\\./LibraryPageImpl\\.css',
]; ];
@ -39,6 +40,10 @@ const config: StorybookConfig = {
from: "../src/common/styles/theme-dark.css", from: "../src/common/styles/theme-dark.css",
to: "@goauthentik/common/styles/theme-dark.css", to: "@goauthentik/common/styles/theme-dark.css",
}, },
{
from: "../src/common/styles/one-dark.css",
to: "@goauthentik/common/styles/one-dark.css",
},
], ],
framework: { framework: {
name: "@storybook/web-components-vite", name: "@storybook/web-components-vite",

View File

@ -71,7 +71,7 @@ export default [
...globals.node, ...globals.node,
}, },
}, },
files: ["scripts/*.mjs", "*.ts", "*.mjs"], files: ["scripts/**/*.mjs", "*.ts", "*.mjs"],
rules: { rules: {
"no-unused-vars": "off", "no-unused-vars": "off",
// We WANT our scripts to output to the console! // We WANT our scripts to output to the console!

4397
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,12 +11,14 @@
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7", "@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.1-1740858273", "@goauthentik/api": "^2025.2.2-1742395408",
"@lit-labs/ssr": "^3.2.2", "@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",
"@lit/task": "^1.0.1", "@lit/task": "^1.0.1",
"@mdx-js/esbuild": "^3.1.0",
"@mdx-js/mdx": "^3.1.0",
"@open-wc/lit-helpers": "^0.7.0", "@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^4.0.2", "@patternfly/elements": "^4.0.2",
"@patternfly/patternfly": "^4.224.2", "@patternfly/patternfly": "^4.224.2",
@ -36,9 +38,11 @@
"guacamole-common-js": "^1.5.0", "guacamole-common-js": "^1.5.0",
"lit": "^3.2.0", "lit": "^3.2.0",
"md-front-matter": "^1.0.4", "md-front-matter": "^1.0.4",
"mdx-mermaid": "^2.0.3",
"mermaid": "^11.4.1", "mermaid": "^11.4.1",
"rapidoc": "^9.3.7", "rapidoc": "^9.3.7",
"showdown": "^2.1.0", "react": "^18.3.1",
"react-dom": "^18.3.1",
"style-mod": "^4.1.2", "style-mod": "^4.1.2",
"ts-pattern": "^5.4.0", "ts-pattern": "^5.4.0",
"webcomponent-qr-code": "^1.2.0", "webcomponent-qr-code": "^1.2.0",
@ -66,13 +70,13 @@
"@types/guacamole-common-js": "^1.5.2", "@types/guacamole-common-js": "^1.5.2",
"@types/mocha": "^10.0.8", "@types/mocha": "^10.0.8",
"@types/node": "^22.7.4", "@types/node": "^22.7.4",
"@types/showdown": "^2.0.6", "@types/react": "^18.3.13",
"@typescript-eslint/eslint-plugin": "^8.8.0", "@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0", "@typescript-eslint/parser": "^8.8.0",
"@wdio/browser-runner": "9.4", "@wdio/browser-runner": "9.4",
"@wdio/cli": "9.4", "@wdio/cli": "9.4",
"@wdio/spec-reporter": "^9.1.2", "@wdio/spec-reporter": "^9.1.2",
"chokidar": "^4.0.1", "change-case": "^5.4.4",
"chromedriver": "^131.0.1", "chromedriver": "^131.0.1",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"eslint": "^9.11.1", "eslint": "^9.11.1",
@ -87,6 +91,13 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"pseudolocale": "^2.1.0", "pseudolocale": "^2.1.0",
"rehype-highlight": "^7.0.2",
"rehype-parse": "^9.0.1",
"rehype-stringify": "^10.0.1",
"remark-directive": "^4.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0",
"rollup-plugin-modify": "^3.0.0", "rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0", "rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.3.4", "storybook": "^8.3.4",
@ -118,7 +129,7 @@
"build-locales:build": "wireit", "build-locales:build": "wireit",
"build-proxy": "wireit", "build-proxy": "wireit",
"build:sfe": "wireit", "build:sfe": "wireit",
"esbuild:watch": "node build.mjs --watch", "esbuild:watch": "node scripts/build-web.mjs --watch",
"extract-locales": "wireit", "extract-locales": "wireit",
"format": "wireit", "format": "wireit",
"lint": "wireit", "lint": "wireit",
@ -153,7 +164,7 @@
"instead of `npm run watch`. The former is more comprehensive, but ", "instead of `npm run watch`. The former is more comprehensive, but ",
"the latter is faster." "the latter is faster."
], ],
"command": "${NODE_RUNNER} build.mjs", "command": "${NODE_RUNNER} scripts/build-web.mjs",
"files": [ "files": [
"src/**/*.{css,jpg,png,ts,js,json}", "src/**/*.{css,jpg,png,ts,js,json}",
"!src/**/*.stories.ts", "!src/**/*.stories.ts",
@ -173,6 +184,7 @@
"./dist/poly-*.js.map", "./dist/poly-*.js.map",
"./dist/custom.css", "./dist/custom.css",
"./dist/theme-dark.css", "./dist/theme-dark.css",
"./dist/one-dark.css",
"./dist/patternfly.min.css" "./dist/patternfly.min.css"
], ],
"dependencies": [ "dependencies": [
@ -195,7 +207,7 @@
] ]
}, },
"build-proxy": { "build-proxy": {
"command": "node build.mjs --proxy", "command": "node scripts/build-web.mjs --proxy",
"dependencies": [ "dependencies": [
"build-locales" "build-locales"
] ]

View File

@ -3,15 +3,16 @@ import esbuild from "esbuild";
import findFreePorts from "find-free-ports"; import findFreePorts from "find-free-ports";
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs"; import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
import { globSync } from "glob"; import { globSync } from "glob";
import path from "path"; import * as path from "path";
import { cwd } from "process"; import { cwd } from "process";
import process from "process"; import process from "process";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { buildObserverPlugin } from "./build-observer-plugin.mjs"; import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
import { buildObserverPlugin } from "./esbuild/build-observer-plugin.mjs";
const __dirname = fileURLToPath(new URL(".", import.meta.url)); const __dirname = fileURLToPath(new URL(".", import.meta.url));
let authentikProjectRoot = __dirname + "../"; let authentikProjectRoot = path.join(__dirname, "..", "..");
try { try {
// Use the package.json file in the root folder, as it has the current version information. // Use the package.json file in the root folder, as it has the current version information.
@ -122,11 +123,10 @@ const BASE_ESBUILD_OPTIONS = {
loader: { loader: {
".css": "text", ".css": "text",
".md": "text", ".md": "text",
".mdx": "text",
}, },
define: definitions, define: definitions,
format: "esm", format: "esm",
plugins: [], plugins: [mdxPlugin()],
logOverride: { logOverride: {
/** /**
* HACK: Silences issue originating in ESBuild. * HACK: Silences issue originating in ESBuild.
@ -161,7 +161,7 @@ function composeVersionID() {
* @throws {Error} on build failure * @throws {Error} on build failure
*/ */
function createEntryPointOptions([source, dest], overrides = {}) { function createEntryPointOptions([source, dest], overrides = {}) {
const outdir = path.join(__dirname, "./dist", dest); const outdir = path.join(__dirname, "..", "dist", dest);
return { return {
...BASE_ESBUILD_OPTIONS, ...BASE_ESBUILD_OPTIONS,
@ -214,7 +214,7 @@ async function doWatch() {
buildObserverPlugin({ buildObserverPlugin({
serverURL, serverURL,
logPrefix: entryPoint[1], logPrefix: entryPoint[1],
relativeRoot: __dirname, relativeRoot: path.join(__dirname, ".."),
}), }),
], ],
define: { define: {

View File

@ -0,0 +1,299 @@
/**
* @import {Options as HighlightOptions} from 'rehype-highlight'
* @import {CompileOptions} from '@mdx-js/mdx'
* @import {mdxmermaid} from 'mdx-mermaid'
* @import {Message,
OnLoadArgs,
OnLoadResult,
Plugin,
PluginBuild
* } from 'esbuild'
*/
import { run as runMDX } from "@mdx-js/mdx";
import { createFormatAwareProcessors } from "@mdx-js/mdx/internal-create-format-aware-processors";
import { extnamesToRegex } from "@mdx-js/mdx/internal-extnames-to-regex";
import apacheGrammar from "highlight.js/lib/languages/apache";
import diffGrammar from "highlight.js/lib/languages/diff";
import confGrammar from "highlight.js/lib/languages/ini";
import nginxGrammar from "highlight.js/lib/languages/nginx";
import { common } from "lowlight";
import mdxMermaid from "mdx-mermaid";
import { Mermaid } from "mdx-mermaid/lib/Mermaid";
import assert from "node:assert";
import fs from "node:fs/promises";
import path from "node:path";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import * as runtime from "react/jsx-runtime";
import rehypeHighlight from "rehype-highlight";
import remarkDirective from "remark-directive";
import remarkFrontmatter from "remark-frontmatter";
import remarkGFM from "remark-gfm";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import remarkParse from "remark-parse";
import { SourceMapGenerator } from "source-map";
import { VFile } from "vfile";
import { VFileMessage } from "vfile-message";
import { remarkAdmonition } from "./remark/remark-admonition.mjs";
import { remarkHeadings } from "./remark/remark-headings.mjs";
import { remarkLinks } from "./remark/remark-links.mjs";
import { remarkLists } from "./remark/remark-lists.mjs";
/**
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData
* Data passed to `onload`.
*
* @typedef LoadDataFields
* Extra fields given in `data` to `onload`.
* @property {PluginData | null | undefined} [pluginData]
* Plugin data.
*
* @typedef {CompileOptions} Options
* Configuration.
*
* Options are the same as `compile` from `@mdx-js/mdx`.
*
* @typedef PluginData
* Extra data passed.
* @property {Buffer | string | null | undefined} [contents]
* File contents.
*
* @typedef State
* Info passed around.
* @property {string} doc
* File value.
* @property {string} name
* Plugin name.
* @property {string} path
* File path.
*/
const eol = /\r\n|\r|\n|\u2028|\u2029/g;
const name = "@mdx-js/esbuild";
/**
* Compile MDX to HTML.
* *
* @param {Readonly<Options> | null | undefined} [mdxOptions]
* Configuration (optional).
* @return {Plugin}
* Plugin.
*/
export function mdxPlugin(mdxOptions) {
/** @type {mdxmermaid.Config} */
const mermaidConfig = {
output: "svg",
};
/**
* @type {HighlightOptions}
*/
const highlightThemeOptions = {
languages: {
...common,
nginx: nginxGrammar,
apache: apacheGrammar,
conf: confGrammar,
diff: diffGrammar,
},
};
const { extnames, process } = createFormatAwareProcessors({
...mdxOptions,
SourceMapGenerator,
outputFormat: "function-body",
remarkPlugins: [
remarkParse,
remarkDirective,
remarkAdmonition,
remarkGFM,
remarkFrontmatter,
remarkMdxFrontmatter,
remarkHeadings,
remarkLinks,
remarkLists,
[mdxMermaid, mermaidConfig],
],
rehypePlugins: [[rehypeHighlight, highlightThemeOptions]],
});
return { name, setup };
/**
* @param {PluginBuild} build
* Build.
* @returns {undefined}
* Nothing.
*/
function setup(build) {
build.onLoad({ filter: extnamesToRegex(extnames) }, onload);
/**
* @param {LoadData} data
* Data.
* @returns {Promise<OnLoadResult>}
* Result.
*/
async function onload(data) {
const document = String(
data.pluginData &&
data.pluginData.contents !== null &&
data.pluginData.contents !== undefined
? data.pluginData.contents
: await fs.readFile(data.path),
);
/** @type {State} */
const state = {
doc: document,
name,
path: data.path,
};
let file = new VFile({
path: data.path,
value: document,
});
/** @type {string | undefined} */
let value;
/** @type {Array<VFileMessage>} */
let messages = [];
/** @type {Array<Message>} */
const errors = [];
/** @type {Array<Message>} */
const warnings = [];
/**
* @type {React.ComponentType<{children: React.ReactNode, frontmatter: Record<string, string>}>}
*/
const wrapper = ({ children, frontmatter }) => {
const title = frontmatter.title;
const nextChildren = React.Children.toArray(children);
if (title) {
nextChildren.unshift(React.createElement("h1", { key: "title" }, title));
}
return React.createElement(React.Fragment, null, nextChildren);
};
try {
file = await process(file);
const { default: Content, ...mdxExports } = await runMDX(file, {
...runtime,
useMDXComponents: () => {
return {
mermaid: Mermaid,
Mermaid,
};
},
baseUrl: import.meta.url,
});
const { frontmatter = {} } = mdxExports;
const result = renderToStaticMarkup(
Content({
frontmatter,
components: {
wrapper,
},
}),
);
value = result;
messages = file.messages;
} catch (error_) {
const cause = /** @type {VFileMessage | Error} */ (error_);
console.error(cause);
const message =
"reason" in cause
? cause
: new VFileMessage("Cannot process MDX file with esbuild", {
cause,
ruleId: "process-error",
source: "@mdx-js/esbuild",
});
message.fatal = true;
messages.push(message);
}
for (const message of messages) {
const list = message.fatal ? errors : warnings;
list.push(vfileMessageToEsbuild(state, message));
}
// Safety check: the file has a path, so there has to be a `dirname`.
assert(file.dirname, "expected `dirname` to be defined");
return {
contents: value || "",
loader: "text",
errors,
resolveDir: path.resolve(file.cwd, file.dirname),
warnings,
};
}
}
}
/**
* @param {Readonly<State>} state
* Info passed around.
* @param {Readonly<VFileMessage>} message
* VFile message or error.
* @returns {Message}
* ESBuild message.
*/
function vfileMessageToEsbuild(state, message) {
const place = message.place;
const start = place ? ("start" in place ? place.start : place) : undefined;
const end = place && "end" in place ? place.end : undefined;
let length = 0;
let lineStart = 0;
let line = 0;
let column = 0;
if (start && start.offset !== undefined) {
line = start.line;
column = start.column - 1;
lineStart = start.offset - column;
length = 1;
if (end && end.offset !== undefined) {
length = end.offset - start.offset;
}
}
eol.lastIndex = lineStart;
const match = eol.exec(state.doc);
const lineEnd = match ? match.index : state.doc.length;
return {
detail: message,
id: "",
location: {
column,
file: state.path,
length: Math.min(length, lineEnd),
line,
lineText: state.doc.slice(lineStart, lineEnd),
namespace: "file",
suggestion: "",
},
notes: [],
pluginName: state.name,
text: message.reason,
};
}

View File

@ -8,7 +8,7 @@ import path from "path";
* @returns {string} * @returns {string}
*/ */
export function serializeCustomEventToStream(event) { export function serializeCustomEventToStream(event) {
// @ts-ignore // @ts-expect-error - TS doesn't know about the detail property
const data = event.detail ?? {}; const data = event.detail ?? {};
const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`]; const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`];

View File

@ -0,0 +1,46 @@
/**
* @import {Plugin} from 'unified'
* @import {Directives} from 'mdast-util-directive'
* @import {} from 'mdast-util-to-hast'
* @import {Root} from 'mdast'
* @import {VFile} from 'vfile'
*/
import { h } from "hastscript";
import { visit } from "unist-util-visit";
const ADMONITION_TYPES = new Set(["info", "warning", "danger", "note"]);
/**
* Remark plugin to process links
* @type {Plugin<[unknown], Root, VFile>}
*/
export function remarkAdmonition() {
return function transformer(tree) {
/**
* @param {Directives} node
*/
const visitor = (node) => {
if (
node.type === "containerDirective" ||
node.type === "leafDirective" ||
node.type === "textDirective"
) {
if (!ADMONITION_TYPES.has(node.name)) return;
const data = node.data || (node.data = {});
const tagName = node.type === "textDirective" ? "span" : "ak-alert";
data.hName = tagName;
const element = h(tagName, node.attributes || {});
data.hProperties = element.properties || {};
data.hProperties.level = `pf-m-${node.name}`;
}
};
// @ts-ignore - visit cannot infer the type of the visitor.
visit(tree, visitor);
};
}

View File

@ -0,0 +1,33 @@
/**
* @import {Plugin} from 'unified'
* @import {Root, Heading} from 'mdast'
* @import {VFile} from 'vfile'
*/
import { kebabCase } from "change-case";
import { toString } from "mdast-util-to-string";
import { visit } from "unist-util-visit";
/**
* Remark plugin to process links
* @type {Plugin<[unknown], Root, VFile>}
*/
export const remarkHeadings = () => {
return function transformer(tree) {
/**
* @param {Heading} node
*/
const visitor = (node) => {
const textContent = toString(node);
const id = kebabCase(textContent);
node.data = node.data || {};
node.data.hProperties = {
...node.data.hProperties,
id,
};
};
// @ts-ignore - visit cannot infer the type of the visitor.
visit(tree, "heading", visitor);
};
};

View File

@ -0,0 +1,59 @@
/**
* @import {Plugin} from 'unified'
* @import {} from 'mdast-util-directive'
* @import {} from 'mdast-util-to-hast'
* @import {Root, Link} from 'mdast'
* @import {VFile} from 'vfile'
*/
import * as path from "node:path";
import { visit } from "unist-util-visit";
const DOCS_DOMAIN = "https://goauthentik.io";
/**
* Remark plugin to process links
* @type {Plugin<[unknown], Root, VFile>}
*/
export const remarkLinks = () => {
return function transformer(tree, file) {
const docsRoot = path.resolve(file.cwd, "..", "website");
/**
* @param {Link} node
*/
const visitor = (node) => {
node.data = node.data || {};
if (node.url.startsWith("#")) {
node.data.hProperties = {
className: "markdown-heading",
};
return;
}
node.data.hProperties = {
...node.data.hProperties,
rel: "noopener noreferrer",
target: "_blank",
};
if (node.url.startsWith(".") && file.dirname) {
const nextPathname = path.resolve(
"/",
path.relative(docsRoot, file.dirname),
node.url,
);
const nextURL = new URL(nextPathname, DOCS_DOMAIN);
// Remove trailing .md and .mdx, and trailing "index".
nextURL.pathname = nextURL.pathname.replace(/(index)?\.mdx?$/, "");
node.data.hProperties.href = nextURL.toString();
}
};
// @ts-ignore - visit cannot infer the type of the visitor.
visit(tree, "link", visitor);
};
};

View File

@ -0,0 +1,29 @@
/**
* @import {Plugin} from 'unified'
* @import {Root, List} from 'mdast'
* @import {VFile} from 'vfile'
*/
import { visit } from "unist-util-visit";
/**
* Remark plugin to process links
* @type {Plugin<[unknown], Root, VFile>}
*/
export const remarkLists = () => {
return function transformer(tree) {
/**
* @param {List} node
*/
const visitor = (node) => {
node.data = node.data || {};
node.data.hProperties = {
...node.data.hProperties,
className: "pf-c-list",
};
};
// @ts-ignore - visit cannot infer the type of the visitor.
visit(tree, "list", visitor);
};
};

View File

@ -3,8 +3,8 @@ import { PFSize } from "@goauthentik/common/enums.js";
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard"; import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { PropertyValues, TemplateResult, html, nothing } from "lit";
import { until } from "lit/directives/until.js"; import { state } from "lit/decorators.js";
import { ResponseError } from "@goauthentik/api"; import { ResponseError } from "@goauthentik/api";
@ -13,46 +13,143 @@ export interface AdminStatus {
message?: TemplateResult; message?: TemplateResult;
} }
/**
* Abstract base class for admin status cards with robust state management
*
* @template T - Type of the primary data value used in the card
*/
export abstract class AdminStatusCard<T> extends AggregateCard { export abstract class AdminStatusCard<T> extends AggregateCard {
abstract getPrimaryValue(): Promise<T>; // Current data value state
@state()
abstract getStatus(value: T): Promise<AdminStatus>;
value?: T; value?: T;
// Current status state derived from value
@state()
protected status?: AdminStatus;
// Current error state if any request fails
@state()
protected error?: string;
// Abstract methods to be implemented by subclasses
abstract getPrimaryValue(): Promise<T>;
abstract getStatus(value: T): Promise<AdminStatus>;
constructor() { constructor() {
super(); super();
this.addEventListener(EVENT_REFRESH, () => { // Proper binding for event handler
this.requestUpdate(); this.fetchData = this.fetchData.bind(this);
}); // Register refresh event listener
this.addEventListener(EVENT_REFRESH, this.fetchData);
} }
renderValue(): TemplateResult { // Lifecycle method: Called when component is added to DOM
connectedCallback(): void {
super.connectedCallback();
// Initial data fetch
this.fetchData();
}
/**
* Fetch primary data and handle errors
*/
private fetchData() {
this.getPrimaryValue()
.then((value: T) => {
this.value = value; // Triggers shouldUpdate
this.error = undefined;
})
.catch((err: ResponseError) => {
this.status = undefined;
this.error = err?.response?.statusText ?? msg("Unknown error");
});
}
/**
* Lit lifecycle method: Determine if component should update
*
* @param changed - Map of changed properties
* @returns boolean indicating if update should proceed
*/
shouldUpdate(changed: PropertyValues<this>) {
if (changed.has("value") && this.value !== undefined) {
// When value changes, fetch new status
this.getStatus(this.value)
.then((status) => {
this.status = status;
this.error = undefined;
})
.catch((err: ResponseError) => {
this.status = undefined;
this.error = err?.response?.statusText ?? msg("Unknown error");
});
// Prevent immediate re-render if only value changed
if (changed.size === 1) return false;
}
return true;
}
/**
* Render the primary value display
*
* @returns TemplateResult displaying the value
*/
protected renderValue(): TemplateResult {
return html`${this.value}`; return html`${this.value}`;
} }
/**
* Render status state
*
* @param status - AdminStatus object containing icon and message
* @returns TemplateResult for status display
*/
private renderStatus(status: AdminStatus): TemplateResult {
return html`
<p><i class="${status.icon}"></i>&nbsp;${this.renderValue()}</p>
${status.message ? html`<p class="subtext">${status.message}</p>` : nothing}
`;
}
/**
* Render error state
*
* @param error - Error message to display
* @returns TemplateResult for error display
*/
private renderError(error: string): TemplateResult {
return html`
<p><i class="fa fa-times"></i>&nbsp;${error}</p>
<p class="subtext">${msg("Failed to fetch")}</p>
`;
}
/**
* Render loading state
*
* @returns TemplateResult for loading spinner
*/
private renderLoading(): TemplateResult {
return html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`;
}
/**
* Main render method that selects appropriate state display
*
* @returns TemplateResult for current component state
*/
renderInner(): TemplateResult { renderInner(): TemplateResult {
return html`<p class="center-value"> return html`
${until( <p class="center-value">
this.getPrimaryValue() ${
.then((v) => { this.status
this.value = v; ? this.renderStatus(this.status) // Status available
return this.getStatus(v); : this.error
}) ? this.renderError(this.error) // Error state
.then((status) => { : this.renderLoading() // Loading state
return html`<p><i class="${status.icon}"></i>&nbsp;${this.renderValue()}</p> }
${status.message </p>
? html`<p class="subtext">${status.message}</p>` `;
: html``}`;
})
.catch((exc: ResponseError) => {
return html` <p>
<i class="fa fa-times"></i>&nbsp;${exc.response.statusText}
</p>
<p class="subtext">${msg("Failed to fetch")}</p>`;
}),
html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`,
)}
</p>`;
} }
} }

View File

@ -89,7 +89,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
return html`<div class="pf-c-sidebar__panel pf-m-width-25"> return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<ak-markdown .md=${MDApplication} meta="applications/index.md"></ak-markdown> <ak-markdown .content=${MDApplication}></ak-markdown>
</div> </div>
</div> </div>
</div>`; </div>`;

View File

@ -58,7 +58,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
get bindingsAsColumns() { get bindingsAsColumns() {
return this.wizard.bindings.map((binding, index) => { return this.wizard.bindings.map((binding, index) => {
const { order, enabled, timeout } = binding; const { order, enabled, timeout } = binding;
const isSet = P.string.minLength(1); const isSet = P.union(P.string.minLength(1), P.number);
const policy = match(binding) const policy = match(binding)
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`)) .with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
.with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`)) .with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`))

View File

@ -221,7 +221,7 @@ export class OAuth2ProviderViewPage extends AKElement {
> >
</dt> </dt>
<dd class="pf-c-description-list__description"> <dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text"> <div class="pf-c-description-list__text pf-m-monospace">
${this.provider.clientId} ${this.provider.clientId}
</div> </div>
</dd> </dd>
@ -236,7 +236,9 @@ export class OAuth2ProviderViewPage extends AKElement {
<div class="pf-c-description-list__text"> <div class="pf-c-description-list__text">
<ul> <ul>
${this.provider.redirectUris.map((ru) => { ${this.provider.redirectUris.map((ru) => {
return html`<li>${ru.matchingMode}: ${ru.url}</li>`; return html`<li class="pf-m-monospace">
${ru.matchingMode}: ${ru.url}
</li>`;
})} })}
</ul> </ul>
</div> </div>
@ -356,6 +358,7 @@ export class OAuth2ProviderViewPage extends AKElement {
> >
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<ak-markdown <ak-markdown
.content=${MDProviderOAuth2}
.replacers=${[ .replacers=${[
(input: string) => { (input: string) => {
if (!this.provider) { if (!this.provider) {
@ -367,8 +370,6 @@ export class OAuth2ProviderViewPage extends AKElement {
); );
}, },
]} ]}
.md=${MDProviderOAuth2}
meta="providers/oauth2/index.md"
></ak-markdown> ></ak-markdown>
</div> </div>
</div> </div>

View File

@ -196,8 +196,8 @@ export class ProxyProviderViewPage extends AKElement {
class="pf-c-page__main-section pf-m-no-padding-mobile ak-markdown-section" class="pf-c-page__main-section pf-m-no-padding-mobile ak-markdown-section"
> >
<ak-markdown <ak-markdown
.content=${server.md}
.replacers=${replacers} .replacers=${replacers}
.md=${server.md}
meta=${server.meta} meta=${server.meta}
></ak-markdown> ></ak-markdown>
</section>`; </section>`;
@ -266,7 +266,7 @@ export class ProxyProviderViewPage extends AKElement {
<div class="pf-c-card pf-l-grid__item pf-m-12-col"> <div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<ak-markdown <ak-markdown
.md=${MDHeaderAuthentication} .content=${MDHeaderAuthentication}
meta="proxy/header_authentication.md" meta="proxy/header_authentication.md"
></ak-markdown> ></ak-markdown>
</div> </div>

View File

@ -245,6 +245,41 @@ export function renderForm(
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("AuthnContextClassRef Property Mapping")}
name="authnContextClassRefMapping"
>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<SAMLPropertyMapping[]> => {
const args: PropertymappingsProviderSamlListRequest = {
ordering: "saml_name",
};
if (query !== undefined) {
args.search = query;
}
const items = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderSamlList(args);
return items.results;
}}
.renderElement=${(item: SAMLPropertyMapping): string => {
return item.name;
}}
.value=${(item: SAMLPropertyMapping | undefined): string | undefined => {
return item?.pk;
}}
.selected=${(item: SAMLPropertyMapping): boolean => {
return provider?.authnContextClassRefMapping === item.pk;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.",
)}
</p>
</ak-form-element-horizontal>
<ak-text-input <ak-text-input
name="assertionValidNotBefore" name="assertionValidNotBefore"

View File

@ -11,6 +11,7 @@ import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { import {
CompatibilityModeEnum,
CoreApi, CoreApi,
CoreGroupsListRequest, CoreGroupsListRequest,
Group, Group,
@ -61,6 +62,35 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
)} )}
inputHint="code" inputHint="code"
></ak-text-input> ></ak-text-input>
<ak-radio-input
name="compatibilityMode"
label=${msg("Compatibility Mode")}
.value=${provider?.compatibilityMode}
required
.options=${[
{
label: msg("Default"),
value: CompatibilityModeEnum.Default,
default: true,
description: html`${msg("Default behavior.")}`,
},
{
label: msg("AWS"),
value: CompatibilityModeEnum.Aws,
description: html`${msg(
"Altered behavior for usage with Amazon Web Services.",
)}`,
},
{
label: msg("Slack"),
value: CompatibilityModeEnum.Slack,
description: html`${msg("Altered behavior for usage with Slack.")}`,
},
]}
help=${msg(
"Alter authentik's behavior for vendor-specific SCIM implementations.",
)}
></ak-radio-input>
<ak-form-element-horizontal name="dryRun"> <ak-form-element-horizontal name="dryRun">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input

View File

@ -24,6 +24,7 @@ export class SCIMProviderGroupList extends Table<SCIMProviderGroup> {
return true; return true;
} }
expandable = true;
checkbox = true; checkbox = true;
clearOnRefresh = true; clearOnRefresh = true;
@ -81,6 +82,13 @@ export class SCIMProviderGroupList extends Table<SCIMProviderGroup> {
html`${item.id}`, html`${item.id}`,
]; ];
} }
renderExpanded(item: SCIMProviderGroup): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</td>`;
}
} }
declare global { declare global {

View File

@ -24,6 +24,7 @@ export class SCIMProviderUserList extends Table<SCIMProviderUser> {
return true; return true;
} }
expandable = true;
checkbox = true; checkbox = true;
clearOnRefresh = true; clearOnRefresh = true;
@ -82,6 +83,13 @@ export class SCIMProviderUserList extends Table<SCIMProviderUser> {
html`${item.id}`, html`${item.id}`,
]; ];
} }
renderExpanded(item: SCIMProviderUser): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</td>`;
}
} }
declare global { declare global {

View File

@ -244,7 +244,7 @@ export class SCIMProviderViewPage extends AKElement {
<div class="pf-c-card pf-l-grid__item pf-m-5-col"> <div class="pf-c-card pf-l-grid__item pf-m-5-col">
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<ak-markdown <ak-markdown
.md=${MDSCIMProvider} .content=${MDSCIMProvider}
meta="providers/scim/index.md" meta="providers/scim/index.md"
></ak-markdown> ></ak-markdown>
</div> </div>

View File

@ -62,7 +62,11 @@ export class SSFProviderFormPage extends BaseProviderForm<SSFProvider> {
<ak-form-group expanded> <ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span> <span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey"> <ak-form-element-horizontal
label=${msg("Signing Key")}
name="signingKey"
required
>
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements --> <!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search <ak-crypto-certificate-search
certificate=${ifDefined(provider?.signingKey ?? undefined)} certificate=${ifDefined(provider?.signingKey ?? undefined)}

View File

@ -137,10 +137,13 @@ export class SSFProviderViewPage extends AKElement {
<dd class="pf-c-description-list__description"> <dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text"> <div class="pf-c-description-list__text">
<input <input
class="pf-c-form-control" class="pf-c-form-control pf-m-monospace"
readonly readonly
type="text" type="text"
value=${this.provider.ssfUrl || ""} value=${this.provider.ssfUrl || ""}
placeholder=${this.provider.ssfUrl
? msg("SSF URL")
: msg("No assigned application")}
/> />
</div> </div>
</dd> </dd>

View File

@ -57,10 +57,13 @@ export class SourceListPage extends TablePage<Source> {
} }
renderToolbarSelected(): TemplateResult { renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1; const disabled =
this.selectedElements.length < 1 ||
this.selectedElements.some((item) => item.component === "");
const nonBuiltInSources = this.selectedElements.filter((item) => item.component !== "");
return html`<ak-forms-delete-bulk return html`<ak-forms-delete-bulk
objectLabel=${msg("Source(s)")} objectLabel=${msg("Source(s)")}
.objects=${this.selectedElements} .objects=${nonBuiltInSources}
.usedBy=${(item: Source) => { .usedBy=${(item: Source) => {
return new SourcesApi(DEFAULT_CONFIG).sourcesAllUsedByList({ return new SourcesApi(DEFAULT_CONFIG).sourcesAllUsedByList({
slug: item.slug, slug: item.slug,

View File

@ -187,7 +187,7 @@ export class KerberosSourceViewPage extends AKElement {
<div class="pf-c-card pf-l-grid__item pf-m-12-col"> <div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<ak-markdown <ak-markdown
.md=${MDSourceKerberosBrowser} .content=${MDSourceKerberosBrowser}
meta="users-sources/protocols/kerberos/browser.md" meta="users-sources/protocols/kerberos/browser.md"
; ;
></ak-markdown> ></ak-markdown>

View File

@ -3,6 +3,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
@ -202,19 +203,20 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Token expiry")} label=${msg("Token expiration")}
?required=${true} ?required=${true}
name="tokenExpiry" name="tokenExpiry"
> >
<input <input
type="number" type="text"
value="${first(this.instance?.tokenExpiry, 30)}" value="${first(this.instance?.tokenExpiry, "minutes=30")}"
class="pf-c-form-control" class="pf-c-form-control"
required required
/> />
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg("Time in minutes the token sent is valid.")} ${msg("Time the token sent is valid.")}
</p> </p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Subject")} label=${msg("Subject")}

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger"; export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress"; export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current"; export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2025.2.1"; export const VERSION = "2025.2.2";
export const TITLE_DEFAULT = "authentik"; export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";"; export const ROUTE_SEPARATOR = ";";

View File

@ -1,3 +1,5 @@
/* #region Global */
:root { :root {
--ak-accent: #fd4b2d; --ak-accent: #fd4b2d;
@ -45,7 +47,44 @@ html > form > input {
left: -2000px; left: -2000px;
} }
/*#region Icons*/ /* #endregion */
/* #region Anchors */
a {
--pf-global--link--Color: var(--pf-global--link--Color--light);
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover);
--pf-global--link--Color--visited: var(--pf-global--link--Color);
}
/*
Note that order of anchor pseudo-selectors must follow:
1. link
2. visited
3. hover
4. active
*/
a:link {
color: var(--pf-global--link--Color);
}
a:visited {
color: var(--pf-global--link--Color--visited);
}
a:hover {
color: var(--pf-global--link--Color--hover);
}
a:active {
color: var(--pf-global--link--Color);
}
/* #endregion */
/* #region Icons */
.pf-icon { .pf-icon {
display: inline-block; display: inline-block;
@ -66,7 +105,7 @@ html > form > input {
); );
} }
/*#endregion*/ /* #endregion */
.pf-c-page__header { .pf-c-page__header {
z-index: 0; z-index: 0;
@ -74,15 +113,15 @@ html > form > input {
box-shadow: var(--pf-global--BoxShadow--lg-bottom); box-shadow: var(--pf-global--BoxShadow--lg-bottom);
} }
/***************************** /* #region Login adjustments */
* Login adjustments
*****************************/
/* Ensure card is displayed on small screens */ /* Ensure card is displayed on small screens */
.pf-c-login__main { .pf-c-login__main {
display: block; display: block;
position: relative; position: relative;
width: 100%; width: 100%;
} }
.ak-login-container { .ak-login-container {
height: calc(100vh - var(--pf-global--spacer--lg) - var(--pf-global--spacer--lg)); height: calc(100vh - var(--pf-global--spacer--lg) - var(--pf-global--spacer--lg));
width: 35rem; width: 35rem;
@ -90,30 +129,34 @@ html > form > input {
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
} }
.pf-c-login__header { .pf-c-login__header {
flex-grow: 1; flex-grow: 1;
} }
.pf-c-login__footer { .pf-c-login__footer {
flex-grow: 2; flex-grow: 2;
display: flex; display: flex;
justify-content: end; justify-content: end;
flex-direction: column; flex-direction: column;
} }
.pf-c-login__footer ul.pf-c-list.pf-m-inline { .pf-c-login__footer ul.pf-c-list.pf-m-inline {
justify-content: center; justify-content: center;
padding: 2rem 0; padding: 2rem 0;
} }
/*****************************
* End Login adjustments /* #endregion */
*****************************/
.pf-c-content h1 { .pf-c-content h1 {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
} }
.pf-c-content h1 i { .pf-c-content h1 i {
font-style: normal; font-style: normal;
} }
.pf-c-content h1 :first-child { .pf-c-content h1 :first-child {
margin-right: var(--pf-global--spacer--sm); margin-right: var(--pf-global--spacer--sm);
} }
@ -127,9 +170,11 @@ html > form > input {
.pf-m-success { .pf-m-success {
color: var(--pf-global--success-color--100) !important; color: var(--pf-global--success-color--100) !important;
} }
.pf-m-warning { .pf-m-warning {
color: var(--pf-global--warning-color--100); color: var(--pf-global--warning-color--100);
} }
.pf-m-danger { .pf-m-danger {
color: var(--pf-global--danger-color--100); color: var(--pf-global--danger-color--100);
} }
@ -167,6 +212,7 @@ html > form > input {
justify-content: center; justify-content: center;
width: 100%; width: 100%;
} }
.ak-brand img { .ak-brand img {
padding: 0 2rem; padding: 0 2rem;
max-height: inherit; max-height: inherit;
@ -181,3 +227,48 @@ html > form > input {
.pf-c-data-list { .pf-c-data-list {
padding-inline-start: 0; padding-inline-start: 0;
} }
/* #region Mermaid */
svg[id^="mermaid-svg-"] {
.rect {
fill: var(
--ak-mermaid-box-background-color,
var(--pf-global--BackgroundColor--light-300)
) !important;
}
.messageText {
stroke-width: 4;
fill: var(--ak-mermaid-message-text) !important;
paint-order: stroke;
}
}
/* #endregion */
/* #region Tables */
table thead,
table tr:nth-child(2n) {
background-color: var(
--ak-table-stripe-background,
var(--pf-global--BackgroundColor--light-200)
);
}
table td,
table th {
border: var(--pf-table-border-width) solid var(--ifm-table-border-color);
padding: var(--pf-global--spacer--md);
}
/* #endregion */
/* #region Code blocks */
pre:has(.hljs) {
padding: var(--pf-global--spacer--md);
}
/* #endregion */

View File

@ -0,0 +1,98 @@
/*
Atom One Dark by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
base: #282c34
mono-1: #abb2bf
mono-2: #818896
mono-3: #5c6370
hue-1: #56b6c2
hue-2: #61aeee
hue-3: #c678dd
hue-4: #98c379
hue-5: #e06c75
hue-5-2: #be5046
hue-6: #d19a66
hue-6-2: #e6c07b
*/
.hljs {
color: #abb2bf;
background: #282c34;
}
pre:has(.hljs) {
background: #282c34;
}
.hljs-comment,
.hljs-quote {
color: #5c6370;
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #c678dd;
}
.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string {
color: #98c379;
}
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #d19a66;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #61aeee;
}
.hljs-built_in,
.hljs-title.class_,
.hljs-class .hljs-title {
color: #e6c07b;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}

View File

@ -1,59 +1,84 @@
/* #region Global */
:root { :root {
--pf-global--Color--100: var(--ak-dark-foreground) !important; --pf-global--Color--100: var(--ak-dark-foreground) !important;
--ak-global--Color--100: var(--ak-dark-foreground) !important; --ak-global--Color--100: var(--ak-dark-foreground) !important;
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker); --pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
--pf-global--link--Color: var(--ak-dark-foreground-link) !important;
--pf-global--BorderColor--100: var(--ak-dark-background-lighter) !important; --pf-global--BorderColor--100: var(--ak-dark-background-lighter) !important;
--ak-mermaid-message-text: var(--ak-dark-foreground) !important;
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter) !important;
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
} }
body { body {
background-color: var(--ak-dark-background) !important; background-color: var(--ak-dark-background) !important;
} }
.pf-c-radio { .pf-c-radio {
--pf-c-radio__label--Color: var(--ak-dark-foreground); --pf-c-radio__label--Color: var(--ak-dark-foreground);
} }
/* Global page background colour */ /* Global page background colour */
.pf-c-page { .pf-c-page {
--pf-c-page--BackgroundColor: var(--ak-dark-background); --pf-c-page--BackgroundColor: var(--ak-dark-background);
} }
.pf-c-drawer__content { .pf-c-drawer__content {
--pf-c-drawer__content--BackgroundColor: var(--ak-dark-background); --pf-c-drawer__content--BackgroundColor: var(--ak-dark-background);
} }
.pf-c-title { .pf-c-title {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-u-mb-xl { .pf-u-mb-xl {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
/* #endregion */
/* Header sections */ /* Header sections */
.pf-c-page__main-section { .pf-c-page__main-section {
--pf-c-page__main-section--BackgroundColor: var(--ak-dark-background); --pf-c-page__main-section--BackgroundColor: var(--ak-dark-background);
} }
.sidebar-trigger, .sidebar-trigger,
.notification-trigger { .notification-trigger {
background-color: transparent !important; background-color: transparent !important;
} }
.pf-c-content { .pf-c-content {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
/* Card */
/* #region Card */
.pf-c-card { .pf-c-card {
--pf-c-card--BackgroundColor: var(--ak-dark-background-light); --pf-c-card--BackgroundColor: var(--ak-dark-background-light);
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-card.pf-m-non-selectable-raised { .pf-c-card.pf-m-non-selectable-raised {
--pf-c-card--BackgroundColor: var(--ak-dark-background-lighter); --pf-c-card--BackgroundColor: var(--ak-dark-background-lighter);
} }
.pf-c-card__title, .pf-c-card__title,
.pf-c-card__body { .pf-c-card__body {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
/* #endregion */
.pf-c-toolbar { .pf-c-toolbar {
--pf-c-toolbar--BackgroundColor: var(--ak-dark-background-light); --pf-c-toolbar--BackgroundColor: var(--ak-dark-background-light);
} }
.pf-c-pagination.pf-m-bottom { .pf-c-pagination.pf-m-bottom {
background-color: var(--ak-dark-background-light); background-color: var(--ak-dark-background-light);
} }
/* table */
/* #region Tables */
.pf-c-table { .pf-c-table {
--pf-c-table--BackgroundColor: var(--ak-dark-background-light); --pf-c-table--BackgroundColor: var(--ak-dark-background-light);
--pf-c-table--BorderColor: var(--ak-dark-background-lighter); --pf-c-table--BorderColor: var(--ak-dark-background-lighter);
@ -61,41 +86,58 @@ body {
--pf-c-table--tr--m-hoverable--hover--BackgroundColor: var(--ak-dark-background-light-ish); --pf-c-table--tr--m-hoverable--hover--BackgroundColor: var(--ak-dark-background-light-ish);
--pf-c-table--tr--m-hoverable--active--BackgroundColor: var(--ak-dark-background-lighter); --pf-c-table--tr--m-hoverable--active--BackgroundColor: var(--ak-dark-background-lighter);
} }
.pf-c-table__text { .pf-c-table__text {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-table__sort:not(.pf-m-selected) .pf-c-table__button .pf-c-table__text { .pf-c-table__sort:not(.pf-m-selected) .pf-c-table__button .pf-c-table__text {
color: var(--ak-dark-foreground) !important; color: var(--ak-dark-foreground) !important;
} }
.pf-c-table__sort-indicator i { .pf-c-table__sort-indicator i {
color: var(--ak-dark-foreground) !important; color: var(--ak-dark-foreground) !important;
} }
.pf-c-table__expandable-row.pf-m-expanded { .pf-c-table__expandable-row.pf-m-expanded {
--pf-c-table__expandable-row--m-expanded--BorderBottomColor: var(--ak-dark-background-lighter); --pf-c-table__expandable-row--m-expanded--BorderBottomColor: var(--ak-dark-background-lighter);
} }
/* tabs */
/* #endregion */
/* #region Tabs */
.pf-c-tabs { .pf-c-tabs {
background-color: transparent; background-color: transparent;
} }
.pf-c-tabs.pf-m-box.pf-m-vertical .pf-c-tabs__list::before { .pf-c-tabs.pf-m-box.pf-m-vertical .pf-c-tabs__list::before {
border-color: transparent; border-color: transparent;
} }
.pf-c-tabs.pf-m-box .pf-c-tabs__item.pf-m-current:first-child .pf-c-tabs__link::before { .pf-c-tabs.pf-m-box .pf-c-tabs__item.pf-m-current:first-child .pf-c-tabs__link::before {
border-color: transparent; border-color: transparent;
} }
.pf-c-tabs__link::before { .pf-c-tabs__link::before {
border-color: transparent; border-color: transparent;
} }
.pf-c-tabs__item.pf-m-current { .pf-c-tabs__item.pf-m-current {
--pf-c-tabs__link--after--BorderColor: var(--ak-accent); --pf-c-tabs__link--after--BorderColor: var(--ak-accent);
} }
.pf-c-tabs__link { .pf-c-tabs__link {
--pf-c-tabs__link--Color: var(--ak-dark-foreground); --pf-c-tabs__link--Color: var(--ak-dark-foreground);
} }
.pf-c-tabs.pf-m-vertical .pf-c-tabs__link { .pf-c-tabs.pf-m-vertical .pf-c-tabs__link {
background-color: transparent; background-color: transparent;
} }
/* table, on mobile */
/* #endregion */
/* #Region Mobile Tables */
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
.pf-m-grid-xl.pf-c-table tbody:first-of-type { .pf-m-grid-xl.pf-c-table tbody:first-of-type {
border-top-color: var(--ak-dark-background); border-top-color: var(--ak-dark-background);
@ -104,32 +146,42 @@ body {
border-bottom-color: var(--ak-dark-background); border-bottom-color: var(--ak-dark-background);
} }
} }
/* #endregion */
/* class for pagination text */ /* class for pagination text */
.pf-c-options-menu__toggle { .pf-c-options-menu__toggle {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
/* table icon used for expanding rows */ /* table icon used for expanding rows */
.pf-c-table__toggle-icon { .pf-c-table__toggle-icon {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
/* expandable elements */ /* expandable elements */
.pf-c-expandable-section__toggle-text { .pf-c-expandable-section__toggle-text {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-expandable-section__toggle-icon { .pf-c-expandable-section__toggle-icon {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-expandable-section.pf-m-display-lg { .pf-c-expandable-section.pf-m-display-lg {
background-color: var(--ak-dark-background-light-ish); background-color: var(--ak-dark-background-light-ish);
} }
/* header for form group */ /* header for form group */
.pf-c-form__field-group-header-title-text { .pf-c-form__field-group-header-title-text {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-form__field-group { .pf-c-form__field-group {
border-bottom: 0; border-bottom: 0;
} }
/* inputs */
/* #region Inputs */
optgroup, optgroup,
option { option {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
@ -138,9 +190,11 @@ select[multiple] optgroup:checked,
select[multiple] option:checked { select[multiple] option:checked {
color: var(--ak-dark-background); color: var(--ak-dark-background);
} }
.pf-c-input-group { .pf-c-input-group {
--pf-c-input-group--BackgroundColor: transparent; --pf-c-input-group--BackgroundColor: transparent;
} }
.pf-c-form-control { .pf-c-form-control {
--pf-c-form-control--BorderTopColor: transparent !important; --pf-c-form-control--BorderTopColor: transparent !important;
--pf-c-form-control--BorderRightColor: transparent !important; --pf-c-form-control--BorderRightColor: transparent !important;
@ -149,12 +203,15 @@ select[multiple] option:checked {
--pf-c-form-control--BackgroundColor: var(--ak-dark-background-light); --pf-c-form-control--BackgroundColor: var(--ak-dark-background-light);
--pf-c-form-control--Color: var(--ak-dark-foreground) !important; --pf-c-form-control--Color: var(--ak-dark-foreground) !important;
} }
.pf-c-form-control:disabled { .pf-c-form-control:disabled {
background-color: var(--ak-dark-background-light); background-color: var(--ak-dark-background-light);
} }
.pf-c-form-control[readonly] { .pf-c-form-control[readonly] {
background-color: var(--ak-dark-background-light) !important; background-color: var(--ak-dark-background-light) !important;
} }
.pf-c-switch__input:checked ~ .pf-c-switch__label { .pf-c-switch__input:checked ~ .pf-c-switch__label {
--pf-c-switch__input--checked__label--Color: var(--ak-dark-foreground); --pf-c-switch__input--checked__label--Color: var(--ak-dark-foreground);
} }
@ -162,38 +219,47 @@ input[type="datetime-local"]::-webkit-calendar-picker-indicator,
input[type="date"]::-webkit-calendar-picker-indicator { input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1); filter: invert(1);
} }
/* select toggle */ /* select toggle */
.pf-c-select__toggle::before { .pf-c-select__toggle::before {
--pf-c-select__toggle--before--BorderTopColor: var(--ak-dark-background-lighter); --pf-c-select__toggle--before--BorderTopColor: var(--ak-dark-background-lighter);
--pf-c-select__toggle--before--BorderRightColor: var(--ak-dark-background-lighter); --pf-c-select__toggle--before--BorderRightColor: var(--ak-dark-background-lighter);
--pf-c-select__toggle--before--BorderLeftColor: var(--ak-dark-background-lighter); --pf-c-select__toggle--before--BorderLeftColor: var(--ak-dark-background-lighter);
} }
.pf-c-select__toggle.pf-m-typeahead { .pf-c-select__toggle.pf-m-typeahead {
--pf-c-select__toggle--BackgroundColor: var(--ak-dark-background-light); --pf-c-select__toggle--BackgroundColor: var(--ak-dark-background-light);
} }
.pf-c-select__menu { .pf-c-select__menu {
--pf-c-select__menu--BackgroundColor: var(--ak-dark-background-light-ish); --pf-c-select__menu--BackgroundColor: var(--ak-dark-background-light-ish);
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-select__menu-item { .pf-c-select__menu-item {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-select__menu-wrapper:hover, .pf-c-select__menu-wrapper:hover,
.pf-c-select__menu-item:hover { .pf-c-select__menu-item:hover {
--pf-c-select__menu-item--hover--BackgroundColor: var(--ak-dark-background-lighter); --pf-c-select__menu-item--hover--BackgroundColor: var(--ak-dark-background-lighter);
} }
.pf-c-select__menu-wrapper:focus-within, .pf-c-select__menu-wrapper:focus-within,
.pf-c-select__menu-wrapper.pf-m-focus, .pf-c-select__menu-wrapper.pf-m-focus,
.pf-c-select__menu-item:focus, .pf-c-select__menu-item:focus,
.pf-c-select__menu-item.pf-m-focus { .pf-c-select__menu-item.pf-m-focus {
--pf-c-select__menu-item--focus--BackgroundColor: var(--ak-dark-background-light-ish); --pf-c-select__menu-item--focus--BackgroundColor: var(--ak-dark-background-light-ish);
} }
.pf-c-button:disabled { .pf-c-button:disabled {
color: var(--ak-dark-background-lighter); color: var(--ak-dark-background-lighter);
} }
.pf-c-button.pf-m-plain:hover { .pf-c-button.pf-m-plain:hover {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-button.pf-m-control { .pf-c-button.pf-m-control {
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter) --pf-c-button--after--BorderColor: var(--ak-dark-background-lighter)
var(--ak-dark-background-lighter) var(--pf-c-button--m-control--after--BorderBottomColor) var(--ak-dark-background-lighter) var(--pf-c-button--m-control--after--BorderBottomColor)
@ -201,92 +267,127 @@ input[type="date"]::-webkit-calendar-picker-indicator {
background-color: var(--ak-dark-background-light); background-color: var(--ak-dark-background-light);
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-m-tertiary, .pf-m-tertiary,
.pf-c-button.pf-m-tertiary { .pf-c-button.pf-m-tertiary {
--pf-c-button--after--BorderColor: var(--ak-dark-foreground-darker); --pf-c-button--after--BorderColor: var(--ak-dark-foreground-darker);
color: var(--ak-dark-foreground-darker); color: var(--ak-dark-foreground-darker);
} }
.pf-m-tertiary:hover, .pf-m-tertiary:hover,
.pf-c-button.pf-m-tertiary:hover { .pf-c-button.pf-m-tertiary:hover {
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter); --pf-c-button--after--BorderColor: var(--ak-dark-background-lighter);
} }
.pf-c-form__label-text { .pf-c-form__label-text {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-check__label { .pf-c-check__label {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-dropdown__toggle::before { .pf-c-dropdown__toggle::before {
border-color: transparent; border-color: transparent;
} }
.pf-c-dropdown__menu { .pf-c-dropdown__menu {
--pf-c-dropdown__menu--BackgroundColor: var(--ak-dark-background); --pf-c-dropdown__menu--BackgroundColor: var(--ak-dark-background);
} }
.pf-c-dropdown__menu-item { .pf-c-dropdown__menu-item {
--pf-c-dropdown__menu-item--BackgroundColor: var(--ak-dark-background); --pf-c-dropdown__menu-item--BackgroundColor: var(--ak-dark-background);
--pf-c-dropdown__menu-item--Color: var(--ak-dark-foreground); --pf-c-dropdown__menu-item--Color: var(--ak-dark-foreground);
} }
.pf-c-dropdown__menu-item:hover, .pf-c-dropdown__menu-item:hover,
.pf-c-dropdown__menu-item:focus { .pf-c-dropdown__menu-item:focus {
--pf-c-dropdown__menu-item--BackgroundColor: var(--ak-dark-background-light-ish); --pf-c-dropdown__menu-item--BackgroundColor: var(--ak-dark-background-light-ish);
--pf-c-dropdown__menu-item--Color: var(--ak-dark-foreground); --pf-c-dropdown__menu-item--Color: var(--ak-dark-foreground);
} }
.pf-c-toggle-group__button { .pf-c-toggle-group__button {
color: var(--ak-dark-foreground) !important; color: var(--ak-dark-foreground) !important;
} }
.pf-c-toggle-group__button:not(.pf-m-selected) { .pf-c-toggle-group__button:not(.pf-m-selected) {
background-color: var(--ak-dark-background-light) !important; background-color: var(--ak-dark-background-light) !important;
} }
.pf-c-toggle-group__button.pf-m-selected { .pf-c-toggle-group__button.pf-m-selected {
color: var(--ak-dark-foreground) !important; color: var(--ak-dark-foreground) !important;
background-color: var(--pf-global--primary-color--100) !important; background-color: var(--pf-global--primary-color--100) !important;
} }
/* inputs help text */ /* inputs help text */
.pf-c-form__helper-text:not(.pf-m-error) { .pf-c-form__helper-text:not(.pf-m-error) {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
/* modal */
/* #endregion */
/* #region Modal */
.pf-c-modal-box, .pf-c-modal-box,
.pf-c-modal-box__header, .pf-c-modal-box__header,
.pf-c-modal-box__footer, .pf-c-modal-box__footer,
.pf-c-modal-box__body { .pf-c-modal-box__body {
background-color: var(--ak-dark-background); background-color: var(--ak-dark-background);
} }
/* sidebar */
/* #endregion */
/* #region Sidebar */
.pf-c-nav { .pf-c-nav {
background-color: var(--ak-dark-background-light); background-color: var(--ak-dark-background-light);
} }
/* flows */
/* #endregion */
/* #region Flows */
.pf-c-login__main { .pf-c-login__main {
--pf-c-login__main--BackgroundColor: var(--ak-dark-background); --pf-c-login__main--BackgroundColor: var(--ak-dark-background);
} }
.pf-c-login__main-body, .pf-c-login__main-body,
.pf-c-login__main-header, .pf-c-login__main-header,
.pf-c-login__main-header-desc { .pf-c-login__main-header-desc {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-login__main-footer-links-item img, .pf-c-login__main-footer-links-item img,
.pf-c-login__main-footer-links-item .fas { .pf-c-login__main-footer-links-item .fas {
filter: invert(1); filter: invert(1);
} }
.pf-c-login__main-footer-band { .pf-c-login__main-footer-band {
--pf-c-login__main-footer-band--BackgroundColor: var(--ak-dark-background-lighter); --pf-c-login__main-footer-band--BackgroundColor: var(--ak-dark-background-lighter);
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.form-control-static { .form-control-static {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
/* notifications */
/* #endregion */
/* #region Notifications */
.pf-c-drawer__panel { .pf-c-drawer__panel {
background-color: var(--ak-dark-background); background-color: var(--ak-dark-background);
} }
.pf-c-notification-drawer { .pf-c-notification-drawer {
--pf-c-notification-drawer--BackgroundColor: var(--ak-dark-background); --pf-c-notification-drawer--BackgroundColor: var(--ak-dark-background);
} }
.pf-c-notification-drawer__header { .pf-c-notification-drawer__header {
background-color: var(--ak-dark-background-lighter); background-color: var(--ak-dark-background-lighter);
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-notification-drawer__list-item { .pf-c-notification-drawer__list-item {
background-color: var(--ak-dark-background-light-ish); background-color: var(--ak-dark-background-light-ish);
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
@ -294,46 +395,84 @@ input[type="date"]::-webkit-calendar-picker-indicator {
--ak-dark-background-lighter --ak-dark-background-lighter
) !important; ) !important;
} }
/* data list */
/* #endregion */
/* #region Data List */
.pf-c-data-list { .pf-c-data-list {
padding-inline-start: 0; padding-inline-start: 0;
border-top-color: var(--ak-dark-background-lighter); border-top-color: var(--ak-dark-background-lighter);
} }
.pf-c-data-list__item { .pf-c-data-list__item {
--pf-c-data-list__item--BackgroundColor: transparent; --pf-c-data-list__item--BackgroundColor: transparent;
--pf-c-data-list__item--BorderBottomColor: var(--ak-dark-background-lighter); --pf-c-data-list__item--BorderBottomColor: var(--ak-dark-background-lighter);
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
/* wizards */
/* #endregion */
/* #region Wizards */
.pf-c-wizard__nav { .pf-c-wizard__nav {
--pf-c-wizard__nav--BackgroundColor: var(--ak-dark-background-lighter); --pf-c-wizard__nav--BackgroundColor: var(--ak-dark-background-lighter);
--pf-c-wizard__nav--lg--BorderRightColor: transparent; --pf-c-wizard__nav--lg--BorderRightColor: transparent;
} }
.pf-c-wizard__main { .pf-c-wizard__main {
background-color: var(--ak-dark-background-light-ish); background-color: var(--ak-dark-background-light-ish);
} }
.pf-c-wizard__footer { .pf-c-wizard__footer {
--pf-c-wizard__footer--BackgroundColor: var(--ak-dark-background-light-ish); --pf-c-wizard__footer--BackgroundColor: var(--ak-dark-background-light-ish);
} }
.pf-c-wizard__toggle-num, .pf-c-wizard__toggle-num,
.pf-c-wizard__nav-link::before { .pf-c-wizard__nav-link::before {
--pf-c-wizard__nav-link--before--BackgroundColor: transparent; --pf-c-wizard__nav-link--before--BackgroundColor: transparent;
} }
/* tree view */
/* #endregion */
/* #region Tree view */
.pf-c-tree-view__node { .pf-c-tree-view__node {
--pf-c-tree-view__node--Color: var(--ak-dark-foreground); --pf-c-tree-view__node--Color: var(--ak-dark-foreground);
} }
.pf-c-tree-view__node-toggle { .pf-c-tree-view__node-toggle {
--pf-c-tree-view__node-toggle--Color: var(--ak-dark-foreground); --pf-c-tree-view__node-toggle--Color: var(--ak-dark-foreground);
} }
.pf-c-tree-view__node:focus { .pf-c-tree-view__node:focus {
--pf-c-tree-view__node--focus--BackgroundColor: var(--ak-dark-background-light-ish); --pf-c-tree-view__node--focus--BackgroundColor: var(--ak-dark-background-light-ish);
} }
.pf-c-tree-view__content:hover, .pf-c-tree-view__content:hover,
.pf-c-tree-view__content:focus-within { .pf-c-tree-view__content:focus-within {
--pf-c-tree-view__node--hover--BackgroundColor: var(--ak-dark-background-light-ish); --pf-c-tree-view__node--hover--BackgroundColor: var(--ak-dark-background-light-ish);
} }
/* stepper */
/* #endregion */
/* #region Stepper */
.pf-c-progress-stepper__step-title { .pf-c-progress-stepper__step-title {
--pf-c-progress-stepper__step-title--Color: var(--ak-dark-foreground); --pf-c-progress-stepper__step-title--Color: var(--ak-dark-foreground);
} }
/* #endregion */
/* #region Mermaid */
svg[id^="mermaid-svg-"] {
line[class^="messageLine"] {
/*
Mermaid's support for dynamic palette changes leaves a lot to be desired.
This is a workaround to keep content readable while not breaking the rest of the theme.
*/
filter: invert(1) !important;
}
}
/* #endregion */

Some files were not shown because too many files have changed in this diff Show More