Compare commits
100 Commits
permission
...
fix/issue_
Author | SHA1 | Date | |
---|---|---|---|
13c8cbf03a | |||
1776981f29 | |||
5a4df95011 | |||
f2927e5725 | |||
8ee90826fc | |||
8c7d4d2f5e | |||
d72def0368 | |||
5bcf501842 | |||
13fc216c68 | |||
27aed4b315 | |||
84b5992e55 | |||
7eb985f636 | |||
d3172ae904 | |||
88662b54c1 | |||
b38bc8c1c4 | |||
a9b648842a | |||
5fda531e2b | |||
921a3e6eb8 | |||
fd898bea66 | |||
cbf9ee55ae | |||
590ee7d9d4 | |||
b8cd1d1ae2 | |||
9f9524fbcb | |||
1df87cdf77 | |||
6383550914 | |||
10771b4779 | |||
fcaf1193ed | |||
b9f6093e6f | |||
47f6d59758 | |||
59d20e3bc0 | |||
ae347cd1c5 | |||
7653a35caa | |||
dc9b12fd37 | |||
b7dac0674a | |||
5a17dea765 | |||
044547c316 | |||
6a84e7e6b0 | |||
6d4bb77960 | |||
1b588b98bc | |||
3eccef88aa | |||
8f50dfa0c5 | |||
8417d8508f | |||
b2c2fc001b | |||
f60312cbbc | |||
7614b17a05 | |||
8947376edb | |||
ce23209ae8 | |||
0b806b7130 | |||
9538cf4690 | |||
63da458fb3 | |||
873dab29a9 | |||
1e96c80593 | |||
ee4a922234 | |||
37a2eff716 | |||
50e2f1c474 | |||
ab7338b50e | |||
bcdc6fcd36 | |||
98c3e0d68b | |||
a2b82b6448 | |||
0456ace646 | |||
d3a11ce810 | |||
bfd1445c69 | |||
c2b3e9b05c | |||
2c7d841e4a | |||
c5d13c4a15 | |||
079ef6e114 | |||
98bfca0b4d | |||
a247bd5b9f | |||
27856ec301 | |||
e4a8c05d25 | |||
cb2e0c6d54 | |||
f37e1ca642 | |||
70b1f05a84 | |||
192ed8f494 | |||
b69d77d270 | |||
35b6801ba0 | |||
f9e6f57aad | |||
868261c883 | |||
b6442c233d | |||
74292e6c23 | |||
3e2cf4fd30 | |||
05cbb4ce0c | |||
c93d85731c | |||
d163afe87c | |||
eac2c9a12b | |||
c10e4a9063 | |||
4e4adcc672 | |||
bb20576d84 | |||
5f315bddbd | |||
9e0404646b | |||
45883ff86b | |||
915f5689c6 | |||
ce1ea926f8 | |||
2e3624ea82 | |||
4e52fb7e52 | |||
7e36fb2153 | |||
2b00754324 | |||
12a73ef306 | |||
4469db9b23 | |||
b7beac6795 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.2.1
|
||||
current_version = 2025.2.2
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
|
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Documentation issue
|
||||
about: Suggest an improvement or report a problem
|
||||
title: ""
|
||||
labels: documentation
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.**
|
||||
A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...]
|
||||
|
||||
**Provide the URL or link to the exact page in the documentation to which you are referring.**
|
||||
If there are multiple pages, list them all, and be sure to state the header or section where the content is.
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the documentation issue here.
|
||||
|
||||
**Consider opening a PR!**
|
||||
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).
|
@ -44,7 +44,6 @@ if is_release:
|
||||
]
|
||||
if not prerelease:
|
||||
image_tags += [
|
||||
f"{name}:latest",
|
||||
f"{name}:{version_family}",
|
||||
]
|
||||
else:
|
||||
|
16
.github/actions/setup/action.yml
vendored
16
.github/actions/setup/action.yml
vendored
@ -9,17 +9,22 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install poetry & deps
|
||||
- name: Install apt deps
|
||||
shell: bash
|
||||
run: |
|
||||
pipx install poetry || true
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
|
||||
- name: 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
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
cache: "poetry"
|
||||
- name: Install Python deps
|
||||
shell: bash
|
||||
run: uv sync --all-extras --dev --frozen
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@ -39,10 +44,9 @@ runs:
|
||||
run: |
|
||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||
docker compose -f .github/actions/setup/docker-compose.yml up -d
|
||||
poetry sync
|
||||
cd web && npm ci
|
||||
- name: Generate config
|
||||
shell: poetry run python {0}
|
||||
shell: uv run python {0}
|
||||
run: |
|
||||
from authentik.lib.generators import generate_id
|
||||
from yaml import safe_dump
|
||||
|
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -98,7 +98,7 @@ updates:
|
||||
prefix: "lifecycle/aws:"
|
||||
labels:
|
||||
- dependencies
|
||||
- package-ecosystem: pip
|
||||
- package-ecosystem: uv
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
|
2
.github/workflows/ci-aws-cfn.yml
vendored
2
.github/workflows/ci-aws-cfn.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
npm ci
|
||||
- name: Check changes have been applied
|
||||
run: |
|
||||
poetry run make aws-cfn
|
||||
uv run make aws-cfn
|
||||
git diff --exit-code
|
||||
ci-aws-cfn-mark:
|
||||
if: always()
|
||||
|
32
.github/workflows/ci-main.yml
vendored
32
.github/workflows/ci-main.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run job
|
||||
run: poetry run make ci-${{ matrix.job }}
|
||||
run: uv run make ci-${{ matrix.job }}
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -42,7 +42,7 @@ jobs:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run migrations
|
||||
run: poetry run python -m lifecycle.migrate
|
||||
run: uv run python -m lifecycle.migrate
|
||||
test-make-seed:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -69,19 +69,21 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: checkout stable
|
||||
run: |
|
||||
# Delete all poetry envs
|
||||
rm -rf /home/runner/.cache/pypoetry
|
||||
# Copy current, latest config to local
|
||||
# Temporarly comment the .github backup while migrating to uv
|
||||
cp authentik/lib/default.yml local.env.yml
|
||||
cp -R .github ..
|
||||
# cp -R .github ..
|
||||
cp -R scripts ..
|
||||
git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
|
||||
rm -rf .github/ scripts/
|
||||
mv ../.github ../scripts .
|
||||
# rm -rf .github/ scripts/
|
||||
# mv ../.github ../scripts .
|
||||
rm -rf scripts/
|
||||
mv ../scripts .
|
||||
- name: Setup authentik env (stable)
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
postgresql_version: ${{ matrix.psql }}
|
||||
continue-on-error: true
|
||||
- name: run migrations to stable
|
||||
run: poetry run python -m lifecycle.migrate
|
||||
- name: checkout current code
|
||||
@ -91,15 +93,13 @@ jobs:
|
||||
git reset --hard HEAD
|
||||
git clean -d -fx .
|
||||
git checkout $GITHUB_SHA
|
||||
# Delete previous poetry env
|
||||
rm -rf /home/runner/.cache/pypoetry/virtualenvs/*
|
||||
- name: Setup authentik env (ensure latest deps are installed)
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
postgresql_version: ${{ matrix.psql }}
|
||||
- name: migrate to latest
|
||||
run: |
|
||||
poetry run python -m lifecycle.migrate
|
||||
uv run python -m lifecycle.migrate
|
||||
- name: run tests
|
||||
env:
|
||||
# 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_TOTAL_RUNS: "5"
|
||||
run: |
|
||||
poetry run make ci-test
|
||||
uv run make ci-test
|
||||
test-unittest:
|
||||
name: test-unittest - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
|
||||
runs-on: ubuntu-latest
|
||||
@ -133,7 +133,7 @@ jobs:
|
||||
CI_RUN_ID: ${{ matrix.run_id }}
|
||||
CI_TOTAL_RUNS: "5"
|
||||
run: |
|
||||
poetry run make ci-test
|
||||
uv run make ci-test
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
@ -156,8 +156,8 @@ jobs:
|
||||
uses: helm/kind-action@v1.12.0
|
||||
- name: run integration
|
||||
run: |
|
||||
poetry run coverage run manage.py test tests/integration
|
||||
poetry run coverage xml
|
||||
uv run coverage run manage.py test tests/integration
|
||||
uv run coverage xml
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
@ -214,8 +214,8 @@ jobs:
|
||||
npm run build
|
||||
- name: run e2e
|
||||
run: |
|
||||
poetry run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
poetry run coverage xml
|
||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
uv run coverage xml
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: golangci/golangci-lint-action@v7
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout 5000s --verbose
|
||||
|
@ -2,7 +2,7 @@ name: authentik-gen-update-webauthn-mds
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 1 1,15 * *'
|
||||
- cron: "30 1 1,15 * *"
|
||||
|
||||
env:
|
||||
POSTGRES_DB: authentik
|
||||
@ -24,7 +24,7 @@ jobs:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Setup authentik env
|
||||
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
|
||||
id: cpr
|
||||
with:
|
||||
|
4
.github/workflows/publish-source-docs.yml
vendored
4
.github/workflows/publish-source-docs.yml
vendored
@ -21,8 +21,8 @@ jobs:
|
||||
uses: ./.github/actions/setup
|
||||
- name: generate docs
|
||||
run: |
|
||||
poetry run make migrate
|
||||
poetry run ak build_source_docs
|
||||
uv run make migrate
|
||||
uv run ak build_source_docs
|
||||
- name: Publish
|
||||
uses: netlify/actions/cli@master
|
||||
with:
|
||||
|
27
.github/workflows/semgrep.yml
vendored
Normal file
27
.github/workflows/semgrep.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: authentik-semgrep
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
pull_request: {}
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/semgrep.yml
|
||||
schedule:
|
||||
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
|
||||
- cron: '12 15 * * *'
|
||||
jobs:
|
||||
semgrep:
|
||||
name: semgrep/ci
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
||||
container:
|
||||
image: semgrep/semgrep
|
||||
if: (github.actor != 'dependabot[bot]')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: semgrep ci
|
@ -36,10 +36,10 @@ jobs:
|
||||
run: make gen-client-ts
|
||||
- name: run extract
|
||||
run: |
|
||||
poetry run make i18n-extract
|
||||
uv run make i18n-extract
|
||||
- name: run compile
|
||||
run: |
|
||||
poetry run ak compilemessages
|
||||
uv run ak compilemessages
|
||||
make web-check-compile
|
||||
- name: Create Pull Request
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
46
.vscode/tasks.json
vendored
46
.vscode/tasks.json
vendored
@ -3,8 +3,13 @@
|
||||
"tasks": [
|
||||
{
|
||||
"label": "authentik/core: make",
|
||||
"command": "poetry",
|
||||
"args": ["run", "make", "lint-fix", "lint"],
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"make",
|
||||
"lint-fix",
|
||||
"lint"
|
||||
],
|
||||
"presentation": {
|
||||
"panel": "new"
|
||||
},
|
||||
@ -12,8 +17,12 @@
|
||||
},
|
||||
{
|
||||
"label": "authentik/core: run",
|
||||
"command": "poetry",
|
||||
"args": ["run", "ak", "server"],
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"ak",
|
||||
"server"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
@ -23,13 +32,17 @@
|
||||
{
|
||||
"label": "authentik/web: make",
|
||||
"command": "make",
|
||||
"args": ["web"],
|
||||
"args": [
|
||||
"web"
|
||||
],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "authentik/web: watch",
|
||||
"command": "make",
|
||||
"args": ["web-watch"],
|
||||
"args": [
|
||||
"web-watch"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
@ -39,19 +52,26 @@
|
||||
{
|
||||
"label": "authentik: install",
|
||||
"command": "make",
|
||||
"args": ["install", "-j4"],
|
||||
"args": [
|
||||
"install",
|
||||
"-j4"
|
||||
],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "authentik/website: make",
|
||||
"command": "make",
|
||||
"args": ["website"],
|
||||
"args": [
|
||||
"website"
|
||||
],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "authentik/website: watch",
|
||||
"command": "make",
|
||||
"args": ["website-watch"],
|
||||
"args": [
|
||||
"website-watch"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
@ -60,8 +80,12 @@
|
||||
},
|
||||
{
|
||||
"label": "authentik/api: generate",
|
||||
"command": "poetry",
|
||||
"args": ["run", "make", "gen"],
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"make",
|
||||
"gen"
|
||||
],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
|
@ -10,7 +10,7 @@ schemas/ @goauthentik/backend
|
||||
scripts/ @goauthentik/backend
|
||||
tests/ @goauthentik/backend
|
||||
pyproject.toml @goauthentik/backend
|
||||
poetry.lock @goauthentik/backend
|
||||
uv.lock @goauthentik/backend
|
||||
go.mod @goauthentik/backend
|
||||
go.sum @goauthentik/backend
|
||||
# Infrastructure
|
||||
|
88
Dockerfile
88
Dockerfile
@ -3,8 +3,7 @@
|
||||
# Stage 1: Build website
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
GIT_UNAVAILABLE=true
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /work/website
|
||||
|
||||
@ -94,53 +93,59 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
mkdir -p /usr/share/GeoIP && \
|
||||
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 5: Python dependencies
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-deps
|
||||
# Stage 5: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.6.10 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 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
|
||||
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
|
||||
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
||||
apt-get update && \
|
||||
# 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 \
|
||||
build-essential libffi-dev \
|
||||
# Required for cryptography
|
||||
curl pkg-config \
|
||||
# Required for lxml
|
||||
libxslt-dev zlib1g-dev \
|
||||
# Required for xmlsec
|
||||
libltdl-dev \
|
||||
# Required for kadmin
|
||||
sccache clang && \
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y && \
|
||||
. "$HOME/.cargo/env" && \
|
||||
python -m venv /ak-root/venv/ && \
|
||||
bash -c "source ${VENV_PATH}/bin/activate && \
|
||||
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"
|
||||
# Build essentials
|
||||
build-essential pkg-config libffi-dev git \
|
||||
# cryptography
|
||||
curl \
|
||||
# libxml
|
||||
libxslt-dev zlib1g-dev \
|
||||
# postgresql
|
||||
libpq-dev \
|
||||
# python-kadmin-rs
|
||||
clang libkrb5-dev sccache \
|
||||
# xmlsec
|
||||
libltdl-dev && \
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
|
||||
# Stage 6: Run
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS final-image
|
||||
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec"
|
||||
|
||||
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 GIT_BUILD_HASH
|
||||
@ -172,7 +177,7 @@ RUN apt-get update && \
|
||||
|
||||
COPY ./authentik/ /authentik
|
||||
COPY ./pyproject.toml /
|
||||
COPY ./poetry.lock /
|
||||
COPY ./uv.lock /
|
||||
COPY ./schemas /schemas
|
||||
COPY ./locale /locale
|
||||
COPY ./tests /tests
|
||||
@ -181,7 +186,7 @@ COPY ./blueprints /blueprints
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
|
||||
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/authentik/ /web/authentik/
|
||||
COPY --from=website-builder /work/website/build/ /website/help/
|
||||
@ -192,9 +197,6 @@ USER 1000
|
||||
ENV TMPDIR=/dev/shm/ \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/ak-root/venv/bin:/lifecycle:$PATH" \
|
||||
VENV_PATH="/ak-root/venv" \
|
||||
POETRY_VIRTUALENVS_CREATE=false \
|
||||
GOFIPS=1
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
|
||||
|
51
Makefile
51
Makefile
@ -12,9 +12,9 @@ GEN_API_TS = "gen-ts-api"
|
||||
GEN_API_PY = "gen-py-api"
|
||||
GEN_API_GO = "gen-go-api"
|
||||
|
||||
pg_user := $(shell poetry 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_name := $(shell poetry run python -m authentik.lib.config postgresql.name 2>/dev/null)
|
||||
pg_user := $(shell uv run python -m authentik.lib.config postgresql.user 2>/dev/null)
|
||||
pg_host := $(shell uv run python -m authentik.lib.config postgresql.host 2>/dev/null)
|
||||
pg_name := $(shell uv run python -m authentik.lib.config postgresql.name 2>/dev/null)
|
||||
|
||||
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 ./...
|
||||
|
||||
test: ## Run the server tests and produce a coverage report (locally)
|
||||
poetry run coverage run manage.py test --keepdb authentik
|
||||
poetry run coverage html
|
||||
poetry run coverage report
|
||||
uv run coverage run manage.py test --keepdb authentik
|
||||
uv run coverage html
|
||||
uv run coverage report
|
||||
|
||||
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||
poetry run black $(PY_SOURCES)
|
||||
poetry run ruff check --fix $(PY_SOURCES)
|
||||
uv run black $(PY_SOURCES)
|
||||
uv run ruff check --fix $(PY_SOURCES)
|
||||
|
||||
lint-codespell: ## Reports spelling errors.
|
||||
poetry run codespell -w
|
||||
uv run codespell -w
|
||||
|
||||
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
|
||||
|
||||
core-install:
|
||||
poetry install
|
||||
uv sync --frozen
|
||||
|
||||
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
|
||||
|
||||
aws-cfn:
|
||||
cd lifecycle/aws && npm run aws-cfn
|
||||
|
||||
run: ## Run the main authentik server process
|
||||
uv run ak server
|
||||
|
||||
core-i18n-extract:
|
||||
poetry run ak makemessages \
|
||||
uv run ak makemessages \
|
||||
--add-location file \
|
||||
--no-obsolete \
|
||||
--ignore web \
|
||||
@ -90,11 +93,11 @@ gen-build: ## Extract the schema from the database
|
||||
AUTHENTIK_DEBUG=true \
|
||||
AUTHENTIK_TENANTS__ENABLED=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_TENANTS__ENABLED=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
|
||||
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
|
||||
@ -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/
|
||||
|
||||
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
|
||||
|
||||
@ -254,21 +257,21 @@ ci--meta-debug:
|
||||
node --version
|
||||
|
||||
ci-black: ci--meta-debug
|
||||
poetry run black --check $(PY_SOURCES)
|
||||
uv run black --check $(PY_SOURCES)
|
||||
|
||||
ci-ruff: ci--meta-debug
|
||||
poetry run ruff check $(PY_SOURCES)
|
||||
uv run ruff check $(PY_SOURCES)
|
||||
|
||||
ci-codespell: ci--meta-debug
|
||||
poetry run codespell -s
|
||||
uv run codespell -s
|
||||
|
||||
ci-bandit: ci--meta-debug
|
||||
poetry run bandit -r $(PY_SOURCES)
|
||||
uv run bandit -r $(PY_SOURCES)
|
||||
|
||||
ci-pending-migrations: ci--meta-debug
|
||||
poetry run ak makemigrations --check
|
||||
uv run ak makemigrations --check
|
||||
|
||||
ci-test: ci--meta-debug
|
||||
poetry run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
||||
poetry run coverage report
|
||||
poetry run coverage xml
|
||||
uv run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
||||
uv run coverage report
|
||||
uv run coverage xml
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.2.1"
|
||||
__version__ = "2025.2.2"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -59,7 +59,7 @@ class SystemInfoSerializer(PassiveSerializer):
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
actual_value = value
|
||||
if raw_session in actual_value:
|
||||
if raw_session is not None and raw_session in actual_value:
|
||||
actual_value = actual_value.replace(
|
||||
raw_session, SafeExceptionReporterFilter.cleansed_substitute
|
||||
)
|
||||
|
@ -49,6 +49,8 @@ class BrandSerializer(ModelSerializer):
|
||||
"branding_title",
|
||||
"branding_logo",
|
||||
"branding_favicon",
|
||||
"branding_custom_css",
|
||||
"branding_default_flow_background",
|
||||
"flow_authentication",
|
||||
"flow_invalidation",
|
||||
"flow_recovery",
|
||||
@ -86,6 +88,7 @@ class CurrentBrandSerializer(PassiveSerializer):
|
||||
branding_title = CharField()
|
||||
branding_logo = CharField(source="branding_logo_url")
|
||||
branding_favicon = CharField(source="branding_favicon_url")
|
||||
branding_custom_css = CharField()
|
||||
ui_footer_links = ListField(
|
||||
child=FooterLinkSerializer(),
|
||||
read_only=True,
|
||||
@ -125,6 +128,7 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
|
||||
"branding_title",
|
||||
"branding_logo",
|
||||
"branding_favicon",
|
||||
"branding_default_flow_background",
|
||||
"flow_authentication",
|
||||
"flow_invalidation",
|
||||
"flow_recovery",
|
||||
|
@ -0,0 +1,35 @@
|
||||
# Generated by Django 5.0.12 on 2025-02-22 01:51
|
||||
|
||||
from pathlib import Path
|
||||
from django.db import migrations, models
|
||||
from django.apps.registry import Apps
|
||||
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def migrate_custom_css(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
Brand = apps.get_model("authentik_brands", "brand")
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
path = Path("/web/dist/custom.css")
|
||||
if not path.exists():
|
||||
return
|
||||
css = path.read_text()
|
||||
Brand.objects.using(db_alias).update(branding_custom_css=css)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_brands", "0007_brand_default_application"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="brand",
|
||||
name="branding_custom_css",
|
||||
field=models.TextField(blank=True, default=""),
|
||||
),
|
||||
migrations.RunPython(migrate_custom_css),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.13 on 2025-03-19 22:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_brands", "0008_brand_branding_custom_css"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="brand",
|
||||
name="branding_default_flow_background",
|
||||
field=models.TextField(default="/static/dist/assets/images/flow_background.jpg"),
|
||||
),
|
||||
]
|
@ -33,6 +33,10 @@ class Brand(SerializerModel):
|
||||
|
||||
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
|
||||
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
|
||||
branding_custom_css = models.TextField(default="", blank=True)
|
||||
branding_default_flow_background = models.TextField(
|
||||
default="/static/dist/assets/images/flow_background.jpg"
|
||||
)
|
||||
|
||||
flow_authentication = models.ForeignKey(
|
||||
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
|
||||
@ -84,6 +88,12 @@ class Brand(SerializerModel):
|
||||
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
|
||||
return self.branding_favicon
|
||||
|
||||
def branding_default_flow_background_url(self) -> str:
|
||||
"""Get branding_default_flow_background with the correct prefix"""
|
||||
if self.branding_default_flow_background.startswith("/static"):
|
||||
return CONFIG.get("web.path", "/")[:-1] + self.branding_default_flow_background
|
||||
return self.branding_default_flow_background
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
from authentik.brands.api import BrandSerializer
|
||||
|
@ -24,6 +24,7 @@ class TestBrands(APITestCase):
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_title": "authentik",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": brand.domain,
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
@ -43,6 +44,7 @@ class TestBrands(APITestCase):
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_title": "custom",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "bar.baz",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
@ -59,6 +61,7 @@ class TestBrands(APITestCase):
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_title": "authentik",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "fallback",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
@ -121,3 +124,27 @@ class TestBrands(APITestCase):
|
||||
"subject": None,
|
||||
},
|
||||
)
|
||||
|
||||
def test_branding_url(self):
|
||||
"""Test branding attributes return correct values"""
|
||||
brand = create_test_brand()
|
||||
brand.branding_default_flow_background = "https://goauthentik.io/img/icon.png"
|
||||
brand.branding_favicon = "https://goauthentik.io/img/icon.png"
|
||||
brand.branding_logo = "https://goauthentik.io/img/icon.png"
|
||||
brand.save()
|
||||
self.assertEqual(
|
||||
brand.branding_default_flow_background_url(), "https://goauthentik.io/img/icon.png"
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
|
||||
{
|
||||
"branding_logo": "https://goauthentik.io/img/icon.png",
|
||||
"branding_favicon": "https://goauthentik.io/img/icon.png",
|
||||
"branding_title": "authentik",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": brand.domain,
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
},
|
||||
)
|
||||
|
@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django_filters.filters import BooleanFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from rest_framework import mixins
|
||||
from rest_framework.fields import ReadOnlyField, SerializerMethodField
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.core.api.object_types import TypesMixin
|
||||
@ -18,10 +18,10 @@ from authentik.core.models import Provider
|
||||
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Provider Serializer"""
|
||||
|
||||
assigned_application_slug = ReadOnlyField(source="application.slug")
|
||||
assigned_application_name = ReadOnlyField(source="application.name")
|
||||
assigned_backchannel_application_slug = ReadOnlyField(source="backchannel_application.slug")
|
||||
assigned_backchannel_application_name = ReadOnlyField(source="backchannel_application.name")
|
||||
assigned_application_slug = SerializerMethodField()
|
||||
assigned_application_name = SerializerMethodField()
|
||||
assigned_backchannel_application_slug = SerializerMethodField()
|
||||
assigned_backchannel_application_name = SerializerMethodField()
|
||||
|
||||
component = SerializerMethodField()
|
||||
|
||||
@ -31,6 +31,38 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
return ""
|
||||
return obj.component
|
||||
|
||||
def get_assigned_application_slug(self, obj: Provider) -> str:
|
||||
"""Get application slug, return empty string if no application exists"""
|
||||
try:
|
||||
return obj.application.slug
|
||||
except Provider.application.RelatedObjectDoesNotExist:
|
||||
return ""
|
||||
|
||||
def get_assigned_application_name(self, obj: Provider) -> str:
|
||||
"""Get application name, return empty string if no application exists"""
|
||||
try:
|
||||
return obj.application.name
|
||||
except Provider.application.RelatedObjectDoesNotExist:
|
||||
return ""
|
||||
|
||||
def get_assigned_backchannel_application_slug(self, obj: Provider) -> str:
|
||||
"""Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
"""
|
||||
if not obj.backchannel_application:
|
||||
return ""
|
||||
return obj.backchannel_application.slug or ""
|
||||
|
||||
def get_assigned_backchannel_application_name(self, obj: Provider) -> str:
|
||||
"""Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
"""
|
||||
if not obj.backchannel_application:
|
||||
return ""
|
||||
return obj.backchannel_application.name or ""
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
|
@ -48,6 +48,7 @@ LOGGER = get_logger()
|
||||
|
||||
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
||||
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
|
||||
SESSION_KEY_SOURCE_FLOW_CONTEXT = "authentik/flows/source_flow_context"
|
||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
||||
|
||||
|
||||
@ -261,6 +262,7 @@ class SourceFlowManager:
|
||||
plan.append_stage(stage)
|
||||
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
|
||||
plan.append_stage(stage)
|
||||
plan.context.update(self.request.session.get(SESSION_KEY_SOURCE_FLOW_CONTEXT, {}))
|
||||
return plan.to_redirect(self.request, flow)
|
||||
|
||||
def handle_auth(
|
||||
|
@ -16,7 +16,7 @@
|
||||
{% block head_before %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
||||
<style>{{ brand.branding_custom_css }}</style>
|
||||
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||
{% block head %}
|
||||
|
@ -4,7 +4,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_before %}
|
||||
<link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" />
|
||||
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
||||
{% include "base/header_js.html" %}
|
||||
@ -13,7 +13,7 @@
|
||||
{% block head %}
|
||||
<style>
|
||||
:root {
|
||||
--ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}");
|
||||
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
|
||||
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
|
||||
|
@ -133,6 +133,8 @@ class TestApplicationsAPI(APITestCase):
|
||||
"provider_obj": {
|
||||
"assigned_application_name": "allowed",
|
||||
"assigned_application_slug": "allowed",
|
||||
"assigned_backchannel_application_name": "",
|
||||
"assigned_backchannel_application_slug": "",
|
||||
"authentication_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||
@ -186,6 +188,8 @@ class TestApplicationsAPI(APITestCase):
|
||||
"provider_obj": {
|
||||
"assigned_application_name": "allowed",
|
||||
"assigned_application_slug": "allowed",
|
||||
"assigned_backchannel_application_name": "",
|
||||
"assigned_backchannel_application_slug": "",
|
||||
"authentication_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||
|
@ -3,7 +3,8 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import PropertyMapping
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.models import Application, PropertyMapping, Provider
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
@ -24,3 +25,51 @@ class TestProvidersAPI(APITestCase):
|
||||
reverse("authentik_api:provider-types"),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_provider_serializer_without_application(self):
|
||||
"""Test that Provider serializer handles missing application gracefully"""
|
||||
# Create a provider without an application
|
||||
provider = Provider.objects.create(name="test-provider")
|
||||
|
||||
serializer = ProviderSerializer(instance=provider)
|
||||
serialized_data = serializer.data
|
||||
|
||||
# Check that fields return empty strings when no application exists
|
||||
self.assertEqual(serialized_data["assigned_application_slug"], "")
|
||||
self.assertEqual(serialized_data["assigned_application_name"], "")
|
||||
self.assertEqual(serialized_data["assigned_backchannel_application_slug"], "")
|
||||
self.assertEqual(serialized_data["assigned_backchannel_application_name"], "")
|
||||
|
||||
def test_provider_serializer_with_application(self):
|
||||
"""Test that Provider serializer correctly includes application data"""
|
||||
# Create an application
|
||||
app = Application.objects.create(name="Test App", slug="test-app")
|
||||
|
||||
# Create a provider with an application
|
||||
provider = Provider.objects.create(name="test-provider-with-app")
|
||||
app.provider = provider
|
||||
app.save()
|
||||
|
||||
serializer = ProviderSerializer(instance=provider)
|
||||
serialized_data = serializer.data
|
||||
|
||||
# Check that fields return correct values when application exists
|
||||
self.assertEqual(serialized_data["assigned_application_slug"], "test-app")
|
||||
self.assertEqual(serialized_data["assigned_application_name"], "Test App")
|
||||
self.assertEqual(serialized_data["assigned_backchannel_application_slug"], "")
|
||||
self.assertEqual(serialized_data["assigned_backchannel_application_name"], "")
|
||||
|
||||
def test_provider_api_response(self):
|
||||
"""Test that the API response includes empty strings for missing applications"""
|
||||
# Create a provider without an application
|
||||
provider = Provider.objects.create(name="test-provider-api")
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:provider-detail", kwargs={"pk": provider.pk}),
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["assigned_application_slug"], "")
|
||||
self.assertEqual(response.data["assigned_application_name"], "")
|
||||
self.assertEqual(response.data["assigned_backchannel_application_slug"], "")
|
||||
self.assertEqual(response.data["assigned_backchannel_application_name"], "")
|
||||
|
@ -11,13 +11,14 @@ from guardian.shortcuts import get_anonymous_user
|
||||
from authentik.core.models import Source, User
|
||||
from authentik.core.sources.flow_manager import (
|
||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
|
||||
SESSION_KEY_SOURCE_FLOW_CONTEXT,
|
||||
SESSION_KEY_SOURCE_FLOW_STAGES,
|
||||
)
|
||||
from authentik.core.types import UILoginButton
|
||||
from authentik.enterprise.stages.source.models import SourceStage
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse
|
||||
from authentik.flows.models import FlowToken, in_memory_stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_REDIRECTED, PLAN_CONTEXT_IS_RESTORED
|
||||
from authentik.flows.stage import ChallengeStageView, StageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
|
||||
@ -53,6 +54,9 @@ class SourceStageView(ChallengeStageView):
|
||||
resume_token = self.create_flow_token()
|
||||
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
|
||||
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
|
||||
self.request.session[SESSION_KEY_SOURCE_FLOW_CONTEXT] = {
|
||||
PLAN_CONTEXT_IS_REDIRECTED: self.executor.flow,
|
||||
}
|
||||
return self.login_button.challenge
|
||||
|
||||
def create_flow_token(self) -> FlowToken:
|
||||
|
@ -50,7 +50,8 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||
"mode",
|
||||
"mode_verbose",
|
||||
"webhook_url",
|
||||
"webhook_mapping",
|
||||
"webhook_mapping_body",
|
||||
"webhook_mapping_headers",
|
||||
"send_once",
|
||||
]
|
||||
|
||||
|
@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.0.13 on 2025-03-20 19:54
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0008_event_authentik_e_expires_8c73a8_idx_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="notificationtransport",
|
||||
old_name="webhook_mapping",
|
||||
new_name="webhook_mapping_body",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="notificationtransport",
|
||||
name="webhook_mapping_body",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
help_text="Customize the body of the request. Mapping should return data that is JSON-serializable.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
related_name="+",
|
||||
to="authentik_events.notificationwebhookmapping",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="notificationtransport",
|
||||
name="webhook_mapping_headers",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
help_text="Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
related_name="+",
|
||||
to="authentik_events.notificationwebhookmapping",
|
||||
),
|
||||
),
|
||||
]
|
@ -336,8 +336,27 @@ class NotificationTransport(SerializerModel):
|
||||
mode = models.TextField(choices=TransportMode.choices, default=TransportMode.LOCAL)
|
||||
|
||||
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
|
||||
webhook_mapping = models.ForeignKey(
|
||||
"NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
|
||||
webhook_mapping_body = models.ForeignKey(
|
||||
"NotificationWebhookMapping",
|
||||
on_delete=models.SET_DEFAULT,
|
||||
null=True,
|
||||
default=None,
|
||||
related_name="+",
|
||||
help_text=_(
|
||||
"Customize the body of the request. "
|
||||
"Mapping should return data that is JSON-serializable."
|
||||
),
|
||||
)
|
||||
webhook_mapping_headers = models.ForeignKey(
|
||||
"NotificationWebhookMapping",
|
||||
on_delete=models.SET_DEFAULT,
|
||||
null=True,
|
||||
default=None,
|
||||
related_name="+",
|
||||
help_text=_(
|
||||
"Configure additional headers to be sent. "
|
||||
"Mapping should return a dictionary of key-value pairs"
|
||||
),
|
||||
)
|
||||
send_once = models.BooleanField(
|
||||
default=False,
|
||||
@ -360,8 +379,8 @@ class NotificationTransport(SerializerModel):
|
||||
|
||||
def send_local(self, notification: "Notification") -> list[str]:
|
||||
"""Local notification delivery"""
|
||||
if self.webhook_mapping:
|
||||
self.webhook_mapping.evaluate(
|
||||
if self.webhook_mapping_body:
|
||||
self.webhook_mapping_body.evaluate(
|
||||
user=notification.user,
|
||||
request=None,
|
||||
notification=notification,
|
||||
@ -380,9 +399,18 @@ class NotificationTransport(SerializerModel):
|
||||
if notification.event and notification.event.user:
|
||||
default_body["event_user_email"] = notification.event.user.get("email", None)
|
||||
default_body["event_user_username"] = notification.event.user.get("username", None)
|
||||
if self.webhook_mapping:
|
||||
headers = {}
|
||||
if self.webhook_mapping_body:
|
||||
default_body = sanitize_item(
|
||||
self.webhook_mapping.evaluate(
|
||||
self.webhook_mapping_body.evaluate(
|
||||
user=notification.user,
|
||||
request=None,
|
||||
notification=notification,
|
||||
)
|
||||
)
|
||||
if self.webhook_mapping_headers:
|
||||
headers = sanitize_item(
|
||||
self.webhook_mapping_headers.evaluate(
|
||||
user=notification.user,
|
||||
request=None,
|
||||
notification=notification,
|
||||
@ -392,6 +420,7 @@ class NotificationTransport(SerializerModel):
|
||||
response = get_http_session().post(
|
||||
self.webhook_url,
|
||||
json=default_body,
|
||||
headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
|
@ -120,7 +120,7 @@ class TestEventsNotifications(APITestCase):
|
||||
)
|
||||
|
||||
transport = NotificationTransport.objects.create(
|
||||
name=generate_id(), webhook_mapping=mapping, mode=TransportMode.LOCAL
|
||||
name=generate_id(), webhook_mapping_body=mapping, mode=TransportMode.LOCAL
|
||||
)
|
||||
NotificationRule.objects.filter(name__startswith="default").delete()
|
||||
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)
|
||||
|
@ -60,20 +60,25 @@ class TestEventTransports(TestCase):
|
||||
|
||||
def test_transport_webhook_mapping(self):
|
||||
"""Test webhook transport with custom mapping"""
|
||||
mapping = NotificationWebhookMapping.objects.create(
|
||||
mapping_body = NotificationWebhookMapping.objects.create(
|
||||
name=generate_id(), expression="return request.user"
|
||||
)
|
||||
mapping_headers = NotificationWebhookMapping.objects.create(
|
||||
name=generate_id(), expression="""return {"foo": "bar"}"""
|
||||
)
|
||||
transport: NotificationTransport = NotificationTransport.objects.create(
|
||||
name=generate_id(),
|
||||
mode=TransportMode.WEBHOOK,
|
||||
webhook_url="http://localhost:1234/test",
|
||||
webhook_mapping=mapping,
|
||||
webhook_mapping_body=mapping_body,
|
||||
webhook_mapping_headers=mapping_headers,
|
||||
)
|
||||
with Mocker() as mocker:
|
||||
mocker.post("http://localhost:1234/test")
|
||||
transport.send(self.notification)
|
||||
self.assertEqual(mocker.call_count, 1)
|
||||
self.assertEqual(mocker.request_history[0].method, "POST")
|
||||
self.assertEqual(mocker.request_history[0].headers["foo"], "bar")
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[0].body.decode(),
|
||||
{"email": self.user.email, "pk": self.user.pk, "username": self.user.username},
|
||||
|
@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from model_utils.managers import InheritanceManager
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
@ -178,11 +179,12 @@ class Flow(SerializerModel, PolicyBindingModel):
|
||||
help_text=_("Required level of authentication and authorization to access a flow."),
|
||||
)
|
||||
|
||||
@property
|
||||
def background_url(self) -> str:
|
||||
def background_url(self, request: HttpRequest | None = None) -> str:
|
||||
"""Get the URL to the background image. If the name is /static or starts with http
|
||||
it is returned as-is"""
|
||||
if not self.background:
|
||||
if request:
|
||||
return request.brand.branding_default_flow_background_url()
|
||||
return (
|
||||
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg"
|
||||
)
|
||||
|
@ -184,7 +184,7 @@ class ChallengeStageView(StageView):
|
||||
flow_info = ContextualFlowInfo(
|
||||
data={
|
||||
"title": self.format_title(),
|
||||
"background": self.executor.flow.background_url,
|
||||
"background": self.executor.flow.background_url(self.request),
|
||||
"cancel_url": reverse("authentik_flows:cancel"),
|
||||
"layout": self.executor.flow.layout,
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ class FlowTestCase(APITestCase):
|
||||
self.assertIsNotNone(raw_response["component"])
|
||||
if flow:
|
||||
self.assertIn("flow_info", raw_response)
|
||||
self.assertEqual(raw_response["flow_info"]["background"], flow.background_url)
|
||||
self.assertEqual(
|
||||
raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel")
|
||||
)
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""API flow tests"""
|
||||
|
||||
from json import loads
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.flows.api.stages import StageSerializer, StageViewSet
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage
|
||||
from authentik.lib.generators import generate_id
|
||||
@ -77,6 +79,22 @@ class TestFlowsAPI(APITestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(response.content, {"diagram": DIAGRAM_EXPECTED})
|
||||
|
||||
def test_api_background(self):
|
||||
"""Test custom background"""
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
flow = create_test_flow()
|
||||
response = self.client.get(reverse("authentik_api:flow-detail", kwargs={"slug": flow.slug}))
|
||||
body = loads(response.content.decode())
|
||||
self.assertEqual(body["background"], "/static/dist/assets/images/flow_background.jpg")
|
||||
|
||||
flow.background = "https://goauthentik.io/img/icon.png"
|
||||
flow.save()
|
||||
response = self.client.get(reverse("authentik_api:flow-detail", kwargs={"slug": flow.slug}))
|
||||
body = loads(response.content.decode())
|
||||
self.assertEqual(body["background"], "https://goauthentik.io/img/icon.png")
|
||||
|
||||
def test_api_diagram_no_stages(self):
|
||||
"""Test flow diagram with no stages."""
|
||||
user = create_test_admin_user()
|
||||
|
@ -49,7 +49,7 @@ class TestFlowInspector(APITestCase):
|
||||
"captcha_stage": None,
|
||||
"component": "ak-stage-identification",
|
||||
"flow_info": {
|
||||
"background": flow.background_url,
|
||||
"background": "/static/dist/assets/images/flow_background.jpg",
|
||||
"cancel_url": reverse("authentik_flows:cancel"),
|
||||
"title": flow.title,
|
||||
"layout": "stacked",
|
||||
|
@ -1,5 +1,20 @@
|
||||
# update website/docs/install-config/configuration/configuration.mdx
|
||||
# This is the default configuration file
|
||||
# authentik configuration
|
||||
#
|
||||
# https://docs.goauthentik.io/docs/install-config/configuration/
|
||||
#
|
||||
# To override the settings in this file, run the following command from the repository root:
|
||||
#
|
||||
# ```shell
|
||||
# make gen-dev-config
|
||||
# ```
|
||||
#
|
||||
# You may edit the generated file to override the configuration below.
|
||||
#
|
||||
# When making modifying the default configuration file,
|
||||
# ensure that the corresponding documentation is updated to match.
|
||||
#
|
||||
# @see {@link ../../website/docs/install-config/configuration/configuration.mdx Configuration documentation} for more information.
|
||||
|
||||
postgresql:
|
||||
host: localhost
|
||||
name: authentik
|
||||
@ -45,6 +60,8 @@ redis:
|
||||
# url: ""
|
||||
# transport_options: ""
|
||||
|
||||
http_timeout: 30
|
||||
|
||||
cache:
|
||||
# url: ""
|
||||
timeout: 300
|
||||
|
@ -16,7 +16,40 @@ def authentik_user_agent() -> str:
|
||||
return f"authentik@{get_full_version()}"
|
||||
|
||||
|
||||
class DebugSession(Session):
|
||||
class TimeoutSession(Session):
|
||||
"""Always set a default HTTP request timeout"""
|
||||
|
||||
def __init__(self, default_timeout=None):
|
||||
super().__init__()
|
||||
self.timeout = default_timeout
|
||||
|
||||
def send(
|
||||
self,
|
||||
request,
|
||||
*,
|
||||
stream=...,
|
||||
verify=...,
|
||||
proxies=...,
|
||||
cert=...,
|
||||
timeout=...,
|
||||
allow_redirects=...,
|
||||
**kwargs,
|
||||
):
|
||||
if not timeout and self.timeout:
|
||||
timeout = self.timeout
|
||||
return super().send(
|
||||
request,
|
||||
stream=stream,
|
||||
verify=verify,
|
||||
proxies=proxies,
|
||||
cert=cert,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class DebugSession(TimeoutSession):
|
||||
"""requests session which logs http requests and responses"""
|
||||
|
||||
def send(self, req: PreparedRequest, *args, **kwargs):
|
||||
@ -42,8 +75,9 @@ class DebugSession(Session):
|
||||
|
||||
def get_http_session() -> Session:
|
||||
"""Get a requests session with common headers"""
|
||||
session = Session()
|
||||
session = TimeoutSession()
|
||||
if CONFIG.get_bool("debug") or CONFIG.get("log_level") == "trace":
|
||||
session = DebugSession()
|
||||
session.headers["User-Agent"] = authentik_user_agent()
|
||||
session.timeout = CONFIG.get_optional_int("http_timeout")
|
||||
return session
|
||||
|
@ -13,6 +13,7 @@ from paramiko.ssh_exception import SSHException
|
||||
from structlog.stdlib import get_logger
|
||||
from yaml import safe_dump
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
||||
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
|
||||
@ -184,7 +185,7 @@ class DockerController(BaseController):
|
||||
try:
|
||||
self.client.images.pull(image)
|
||||
except DockerException: # pragma: no cover
|
||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:latest"
|
||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}"
|
||||
self.client.images.pull(image)
|
||||
return image
|
||||
|
||||
|
@ -74,6 +74,8 @@ class TestEndpointsAPI(APITestCase):
|
||||
"component": "ak-provider-rac-form",
|
||||
"assigned_application_slug": self.app.slug,
|
||||
"assigned_application_name": self.app.name,
|
||||
"assigned_backchannel_application_slug": "",
|
||||
"assigned_backchannel_application_name": "",
|
||||
"verbose_name": "RAC Provider",
|
||||
"verbose_name_plural": "RAC Providers",
|
||||
"meta_model_name": "authentik_providers_rac.racprovider",
|
||||
@ -124,6 +126,8 @@ class TestEndpointsAPI(APITestCase):
|
||||
"component": "ak-provider-rac-form",
|
||||
"assigned_application_slug": self.app.slug,
|
||||
"assigned_application_name": self.app.name,
|
||||
"assigned_backchannel_application_slug": "",
|
||||
"assigned_backchannel_application_name": "",
|
||||
"connection_expiry": "hours=8",
|
||||
"delete_token_on_disconnect": False,
|
||||
"verbose_name": "RAC Provider",
|
||||
@ -153,6 +157,8 @@ class TestEndpointsAPI(APITestCase):
|
||||
"component": "ak-provider-rac-form",
|
||||
"assigned_application_slug": self.app.slug,
|
||||
"assigned_application_name": self.app.name,
|
||||
"assigned_backchannel_application_slug": "",
|
||||
"assigned_backchannel_application_name": "",
|
||||
"connection_expiry": "hours=8",
|
||||
"delete_token_on_disconnect": False,
|
||||
"verbose_name": "RAC Provider",
|
||||
|
@ -180,6 +180,7 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
"session_valid_not_on_or_after",
|
||||
"property_mappings",
|
||||
"name_id_mapping",
|
||||
"authn_context_class_ref_mapping",
|
||||
"digest_algorithm",
|
||||
"signature_algorithm",
|
||||
"signing_kp",
|
||||
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
@ -71,6 +71,20 @@ class SAMLProvider(Provider):
|
||||
"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(
|
||||
default="minutes=-5",
|
||||
@ -170,7 +184,6 @@ class SAMLProvider(Provider):
|
||||
def launch_url(self) -> str | None:
|
||||
"""Use IDP-Initiated SAML flow as launch URL"""
|
||||
try:
|
||||
|
||||
return reverse(
|
||||
"authentik_providers_saml:sso-init",
|
||||
kwargs={"application_slug": self.application.slug},
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""SAML Assertion generator"""
|
||||
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
from types import GeneratorType
|
||||
|
||||
@ -52,6 +53,7 @@ class AssertionProcessor:
|
||||
_assertion_id: str
|
||||
_response_id: str
|
||||
|
||||
_auth_instant: str
|
||||
_valid_not_before: str
|
||||
_session_not_on_or_after: str
|
||||
_valid_not_on_or_after: str
|
||||
@ -65,6 +67,11 @@ class AssertionProcessor:
|
||||
self._assertion_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(
|
||||
timedelta_from_string(self.provider.assertion_valid_not_before)
|
||||
)
|
||||
@ -131,7 +138,7 @@ class AssertionProcessor:
|
||||
def get_assertion_auth_n_statement(self) -> Element:
|
||||
"""Generate AuthnStatement with AuthnContext and ContextClassRef Elements."""
|
||||
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(
|
||||
self.http_request.session.session_key.encode("ascii")
|
||||
).hexdigest()
|
||||
@ -158,6 +165,28 @@ class AssertionProcessor:
|
||||
auth_n_context_class_ref.text = (
|
||||
"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
|
||||
|
||||
def get_assertion_conditions(self) -> Element:
|
||||
|
@ -294,6 +294,61 @@ class TestAuthNRequest(TestCase):
|
||||
self.assertEqual(parsed_request.id, "aws_LDxLGeubpc5lx12gxCgS6uPbix1yd5re")
|
||||
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):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
user = create_test_admin_user()
|
||||
@ -321,8 +376,10 @@ class TestAuthNRequest(TestCase):
|
||||
request = request_proc.build_auth_n()
|
||||
|
||||
# Create invalid PropertyMapping
|
||||
scope = SAMLPropertyMapping.objects.create(name="test", saml_name="test", expression="q")
|
||||
self.provider.property_mappings.add(scope)
|
||||
mapping = SAMLPropertyMapping.objects.create(
|
||||
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)
|
||||
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||
@ -338,7 +395,7 @@ class TestAuthNRequest(TestCase):
|
||||
self.assertTrue(events.exists())
|
||||
self.assertEqual(
|
||||
events.first().context["message"],
|
||||
"Failed to evaluate property-mapping: 'test'",
|
||||
f"Failed to evaluate property-mapping: '{mapping.name}'",
|
||||
)
|
||||
|
||||
def test_idp_initiated(self):
|
||||
|
@ -1,12 +1,16 @@
|
||||
"""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"""
|
||||
if delta is None:
|
||||
delta = datetime.timedelta()
|
||||
now = datetime.datetime.now()
|
||||
final = now + delta
|
||||
delta = timedelta()
|
||||
if isinstance(delta, timedelta):
|
||||
final = now() + delta
|
||||
else:
|
||||
final = delta
|
||||
return final.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
@ -24,7 +24,9 @@ class SCIMProviderGroupSerializer(ModelSerializer):
|
||||
"group",
|
||||
"group_obj",
|
||||
"provider",
|
||||
"attributes",
|
||||
]
|
||||
extra_kwargs = {"attributes": {"read_only": True}}
|
||||
|
||||
|
||||
class SCIMProviderGroupViewSet(
|
||||
|
@ -24,7 +24,9 @@ class SCIMProviderUserSerializer(ModelSerializer):
|
||||
"user",
|
||||
"user_obj",
|
||||
"provider",
|
||||
"attributes",
|
||||
]
|
||||
extra_kwargs = {"attributes": {"read_only": True}}
|
||||
|
||||
|
||||
class SCIMProviderUserViewSet(
|
||||
|
@ -102,7 +102,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
if not scim_id or scim_id == "":
|
||||
raise StopSync("SCIM Response with missing or invalid `id`")
|
||||
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))
|
||||
self._patch_add_users(connection, users)
|
||||
@ -243,9 +243,10 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
if user.value not in users_should:
|
||||
users_to_remove.append(user.value)
|
||||
# Check users that should be in the group and add them
|
||||
for user in users_should:
|
||||
if len([x for x in current_group.members if x.value == user]) < 1:
|
||||
users_to_add.append(user)
|
||||
if current_group.members is not None:
|
||||
for user in users_should:
|
||||
if len([x for x in current_group.members if x.value == user]) < 1:
|
||||
users_to_add.append(user)
|
||||
# Only send request if we need to make changes
|
||||
if len(users_to_add) < 1 and len(users_to_remove) < 1:
|
||||
return
|
||||
|
@ -77,21 +77,24 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
|
||||
if len(users_res) < 1:
|
||||
raise exc
|
||||
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:
|
||||
scim_id = response.get("id")
|
||||
if not scim_id or scim_id == "":
|
||||
raise StopSync("SCIM Response with missing or invalid `id`")
|
||||
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):
|
||||
"""Update existing user"""
|
||||
scim_user = self.to_schema(user, connection)
|
||||
scim_user.id = connection.scim_id
|
||||
self._request(
|
||||
response = self._request(
|
||||
"PUT",
|
||||
f"/Users/{connection.scim_id}",
|
||||
json=scim_user.model_dump(
|
||||
@ -99,3 +102,5 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
|
||||
exclude_unset=True,
|
||||
),
|
||||
)
|
||||
connection.attributes = response
|
||||
connection.save()
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
@ -22,6 +22,7 @@ class SCIMProviderUser(SerializerModel):
|
||||
scim_id = models.TextField()
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
provider = models.ForeignKey("SCIMProvider", on_delete=models.CASCADE)
|
||||
attributes = models.JSONField(default=dict)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
@ -43,6 +44,7 @@ class SCIMProviderGroup(SerializerModel):
|
||||
scim_id = models.TextField()
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
provider = models.ForeignKey("SCIMProvider", on_delete=models.CASCADE)
|
||||
attributes = models.JSONField(default=dict)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
|
File diff suppressed because one or more lines are too long
@ -8,7 +8,7 @@ from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
@ -67,6 +67,36 @@ class TestEmailStageSending(FlowTestCase):
|
||||
self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"])
|
||||
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
||||
|
||||
def test_newlines_long_name(self):
|
||||
"""Test with pending user"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
long_user = create_test_user()
|
||||
long_user.name = "Test User\r\n Many Words\r\n"
|
||||
long_user.save()
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = long_user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
Event.objects.filter(action=EventAction.EMAIL_SENT).delete()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
with patch(
|
||||
"authentik.stages.email.models.EmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
response_errors={
|
||||
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
||||
self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"])
|
||||
|
||||
def test_pending_fake_user(self):
|
||||
"""Test with pending (fake) user"""
|
||||
self.flow.designation = FlowDesignation.RECOVERY
|
||||
|
@ -32,7 +32,14 @@ class TemplateEmailMessage(EmailMultiAlternatives):
|
||||
sanitized_to = []
|
||||
# Ensure that all recipients are valid
|
||||
for recipient_name, recipient_email in to:
|
||||
sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8"))
|
||||
# Remove any newline characters from name and email before sanitizing
|
||||
clean_name = (
|
||||
recipient_name.replace("\n", " ").replace("\r", " ") if recipient_name else ""
|
||||
)
|
||||
clean_email = (
|
||||
recipient_email.replace("\n", "").replace("\r", "") if recipient_email else ""
|
||||
)
|
||||
sanitized_to.append(sanitize_address((clean_name, clean_email), "utf-8"))
|
||||
super().__init__(to=sanitized_to, **kwargs)
|
||||
if not template_name:
|
||||
return
|
||||
|
@ -142,38 +142,35 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
raise ValidationError("Failed to authenticate.")
|
||||
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
|
||||
if captcha_stage := current_stage.captcha_stage:
|
||||
captcha_token = attrs.get("captcha_token", None)
|
||||
if not captcha_token:
|
||||
self.stage.logger.warning("Token not set for captcha attempt")
|
||||
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
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2025.2.1 Blueprint schema",
|
||||
"title": "authentik 2025.2.2 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@ -6462,6 +6462,11 @@
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@ -13011,6 +13016,15 @@
|
||||
"minLength": 1,
|
||||
"title": "Branding favicon"
|
||||
},
|
||||
"branding_custom_css": {
|
||||
"type": "string",
|
||||
"title": "Branding custom css"
|
||||
},
|
||||
"branding_default_flow_background": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Branding default flow background"
|
||||
},
|
||||
"flow_authentication": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
@ -14892,9 +14906,15 @@
|
||||
"type": "string",
|
||||
"title": "Webhook url"
|
||||
},
|
||||
"webhook_mapping": {
|
||||
"webhook_mapping_body": {
|
||||
"type": "integer",
|
||||
"title": "Webhook mapping"
|
||||
"title": "Webhook mapping body",
|
||||
"description": "Customize the body of the request. Mapping should return data that is JSON-serializable."
|
||||
},
|
||||
"webhook_mapping_headers": {
|
||||
"type": "integer",
|
||||
"title": "Webhook mapping headers",
|
||||
"description": "Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs"
|
||||
},
|
||||
"send_once": {
|
||||
"type": "boolean",
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
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
|
||||
command: server
|
||||
environment:
|
||||
@ -54,7 +54,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
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
|
||||
command: worker
|
||||
environment:
|
||||
|
6
go.mod
6
go.mod
@ -11,7 +11,7 @@ require (
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
github.com/go-openapi/runtime v0.28.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
@ -23,13 +23,13 @@ require (
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
github.com/pires/go-proxyproto v0.8.0
|
||||
github.com/prometheus/client_golang v1.21.1
|
||||
github.com/redis/go-redis/v9 v9.7.1
|
||||
github.com/redis/go-redis/v9 v9.7.3
|
||||
github.com/sethvargo/go-envconfig v1.1.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025021.4
|
||||
goauthentik.io/api/v3 v3.2025022.6
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/sync v0.12.0
|
||||
|
12
go.sum
12
go.sum
@ -113,8 +113,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@ -248,8 +248,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
|
||||
github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
|
||||
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
@ -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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025021.4 h1:KFap2KW+8CwhOxjBkRnRB4flvuHEMw24+fZei9dOhzw=
|
||||
goauthentik.io/api/v3 v3.2025021.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025022.6 h1:M5M8Cd/1N7E8KLkvYYh7VdcdKz5nfzjKPFLK+YOtOVg=
|
||||
goauthentik.io/api/v3 v3.2025022.6/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -162,13 +162,14 @@ func (c *Config) parseScheme(rawVal string) string {
|
||||
if err != nil {
|
||||
return rawVal
|
||||
}
|
||||
if u.Scheme == "env" {
|
||||
switch u.Scheme {
|
||||
case "env":
|
||||
e, ok := os.LookupEnv(u.Host)
|
||||
if ok {
|
||||
return e
|
||||
}
|
||||
return u.RawQuery
|
||||
} else if u.Scheme == "file" {
|
||||
case "file":
|
||||
d, err := os.ReadFile(u.Path)
|
||||
if err != nil {
|
||||
return u.RawQuery
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigEnv(t *testing.T) {
|
||||
os.Setenv("AUTHENTIK_SECRET_KEY", "bar")
|
||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "bar"))
|
||||
cfg = nil
|
||||
if err := Get().fromEnv(); err != nil {
|
||||
panic(err)
|
||||
@ -19,8 +19,8 @@ func TestConfigEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigEnv_Scheme(t *testing.T) {
|
||||
os.Setenv("foo", "bar")
|
||||
os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo")
|
||||
assert.NoError(t, os.Setenv("foo", "bar"))
|
||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo"))
|
||||
cfg = nil
|
||||
if err := Get().fromEnv(); err != nil {
|
||||
panic(err)
|
||||
@ -33,13 +33,15 @@ func TestConfigEnv_File(t *testing.T) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
defer func() {
|
||||
assert.NoError(t, os.Remove(file.Name()))
|
||||
}()
|
||||
_, err = file.Write([]byte("bar"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name()))
|
||||
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name())))
|
||||
cfg = nil
|
||||
if err := Get().fromEnv(); err != nil {
|
||||
panic(err)
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2025.2.1"
|
||||
const VERSION = "2025.2.2"
|
||||
|
@ -35,7 +35,7 @@ func EnableDebugServer() {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, err = w.Write([]byte(fmt.Sprintf("<a href='%[1]s'>%[1]s</a><br>", tpl)))
|
||||
_, err = fmt.Fprintf(w, "<a href='%[1]s'>%[1]s</a><br>", tpl)
|
||||
if err != nil {
|
||||
l.WithError(err).Warning("failed to write index")
|
||||
return nil
|
||||
|
@ -44,10 +44,11 @@ func New(healthcheck func() bool) *GoUnicorn {
|
||||
signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
if sig == syscall.SIGHUP {
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
g.log.Info("SIGHUP received, forwarding to gunicorn")
|
||||
g.Reload()
|
||||
} else if sig == syscall.SIGUSR2 {
|
||||
case syscall.SIGUSR2:
|
||||
g.log.Info("SIGUSR2 received, restarting gunicorn")
|
||||
g.Restart()
|
||||
}
|
||||
|
@ -35,13 +35,19 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
|
||||
req PaginatorRequest[Treq, Tres],
|
||||
opts PaginatorOptions,
|
||||
) ([]Tobj, error) {
|
||||
if opts.Logger == nil {
|
||||
opts.Logger = log.NewEntry(log.StandardLogger())
|
||||
}
|
||||
var bfreq, cfreq interface{}
|
||||
fetchOffset := func(page int32) (Tres, error) {
|
||||
bfreq = req.Page(page)
|
||||
cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
|
||||
res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
||||
res, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
||||
if err != nil {
|
||||
opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
|
||||
if hres != nil && hres.StatusCode >= 400 && hres.StatusCode < 500 {
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
@ -51,6 +57,9 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
|
||||
for {
|
||||
apiObjects, err := fetchOffset(page)
|
||||
if err != nil {
|
||||
if page == 1 {
|
||||
return objects, err
|
||||
}
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
@ -1,5 +1,64 @@
|
||||
package ak
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/api/v3"
|
||||
)
|
||||
|
||||
type fakeAPIType struct{}
|
||||
|
||||
type fakeAPIResponse struct {
|
||||
results []fakeAPIType
|
||||
pagination api.Pagination
|
||||
}
|
||||
|
||||
func (fapi *fakeAPIResponse) GetResults() []fakeAPIType { return fapi.results }
|
||||
func (fapi *fakeAPIResponse) GetPagination() api.Pagination { return fapi.pagination }
|
||||
|
||||
type fakeAPIRequest struct {
|
||||
res *fakeAPIResponse
|
||||
http *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
func (fapi *fakeAPIRequest) Page(page int32) *fakeAPIRequest { return fapi }
|
||||
func (fapi *fakeAPIRequest) PageSize(size int32) *fakeAPIRequest { return fapi }
|
||||
func (fapi *fakeAPIRequest) Execute() (*fakeAPIResponse, *http.Response, error) {
|
||||
return fapi.res, fapi.http, fapi.err
|
||||
}
|
||||
|
||||
func Test_Simple(t *testing.T) {
|
||||
req := &fakeAPIRequest{
|
||||
res: &fakeAPIResponse{
|
||||
results: []fakeAPIType{
|
||||
{},
|
||||
},
|
||||
pagination: api.Pagination{
|
||||
TotalPages: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := Paginator(req, PaginatorOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 1)
|
||||
}
|
||||
|
||||
func Test_BadRequest(t *testing.T) {
|
||||
req := &fakeAPIRequest{
|
||||
http: &http.Response{
|
||||
StatusCode: 400,
|
||||
},
|
||||
err: errors.New("foo"),
|
||||
}
|
||||
res, err := Paginator(req, PaginatorOptions{})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, []fakeAPIType{}, res)
|
||||
}
|
||||
|
||||
// func Test_PaginatorCompile(t *testing.T) {
|
||||
// req := api.ApiCoreUsersListRequest{}
|
||||
// Paginator(req, PaginatorOptions{
|
||||
|
@ -148,7 +148,8 @@ func (ac *APIController) startWSHandler() {
|
||||
"outpost_type": ac.Server.Type(),
|
||||
"uuid": ac.instanceUUID.String(),
|
||||
}).Set(1)
|
||||
if wsMsg.Instruction == WebsocketInstructionTriggerUpdate {
|
||||
switch wsMsg.Instruction {
|
||||
case WebsocketInstructionTriggerUpdate:
|
||||
time.Sleep(ac.reloadOffset)
|
||||
logger.Debug("Got update trigger...")
|
||||
err := ac.OnRefresh()
|
||||
@ -163,7 +164,7 @@ func (ac *APIController) startWSHandler() {
|
||||
"build": constants.BUILD(""),
|
||||
}).SetToCurrentTime()
|
||||
}
|
||||
} else if wsMsg.Instruction == WebsocketInstructionProviderSpecific {
|
||||
case WebsocketInstructionProviderSpecific:
|
||||
for _, h := range ac.wsHandlers {
|
||||
h(context.Background(), wsMsg.Args)
|
||||
}
|
||||
|
@ -66,7 +66,12 @@ func (ls *LDAPServer) StartLDAPServer() error {
|
||||
return err
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ls.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
|
||||
ls.log.WithField("listen", listen).Info("Starting LDAP server")
|
||||
err = ls.s.Serve(proxyListener)
|
||||
|
@ -49,7 +49,12 @@ func (ls *LDAPServer) StartLDAPTLSServer() error {
|
||||
}
|
||||
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ls.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
|
||||
tln := tls.NewListener(proxyListener, tlsConfig)
|
||||
|
||||
|
@ -98,7 +98,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
|
||||
entries := make([]*ldap.Entry, 0)
|
||||
|
||||
scope := req.SearchRequest.Scope
|
||||
scope := req.Scope
|
||||
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
|
||||
|
||||
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
||||
|
@ -56,7 +56,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo
|
||||
if !embedded && hostBrowser == "" {
|
||||
return ep
|
||||
}
|
||||
var newHost *url.URL = aku
|
||||
var newHost = aku
|
||||
var newBrowserHost *url.URL
|
||||
if embedded {
|
||||
if authentikHost == "" {
|
||||
|
@ -130,7 +130,12 @@ func (ps *ProxyServer) ServeHTTP() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ps.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
|
||||
ps.log.WithField("listen", listenAddress).Info("Starting HTTP server")
|
||||
ps.serve(proxyListener)
|
||||
@ -149,7 +154,12 @@ func (ps *ProxyServer) ServeHTTPS() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ps.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
|
||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
||||
ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server")
|
||||
|
@ -72,11 +72,13 @@ func (s *RedisStore) New(r *http.Request, name string) (*sessions.Session, error
|
||||
session.ID = c.Value
|
||||
|
||||
err = s.load(r.Context(), session)
|
||||
if err == nil {
|
||||
session.IsNew = false
|
||||
} else if err == redis.Nil {
|
||||
err = nil // no data stored
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return session, nil
|
||||
}
|
||||
return session, err
|
||||
}
|
||||
session.IsNew = false
|
||||
return session, err
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@
|
||||
<link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/patternfly.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/authentik.css">
|
||||
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/custom.css">
|
||||
<link rel="prefetch" href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg" />
|
||||
<style>
|
||||
.pf-c-background-image::before {
|
||||
|
@ -156,7 +156,12 @@ func (ws *WebServer) listenPlain() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
|
||||
ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server")
|
||||
ws.serve(proxyListener)
|
||||
|
@ -46,7 +46,12 @@ func (ws *WebServer) listenTLS() {
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
defer func() {
|
||||
err := proxyListener.Close()
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to close proxy listener")
|
||||
}
|
||||
}()
|
||||
|
||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
||||
ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Starting HTTPS server")
|
||||
|
@ -62,12 +62,12 @@ function prepare_debug {
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
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
|
||||
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
|
||||
fi
|
||||
|
||||
|
@ -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 pathlib import Path
|
||||
|
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1004.0",
|
||||
"aws-cdk": "^2.1006.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,9 +17,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1004.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1004.0.tgz",
|
||||
"integrity": "sha512-3E5ICmSc7ZCZCwLX7NY+HFmmdUYgRaL+67h/BDoDQmkhx9StC8wG4xgzHFY9k8WQS0+ib/MP28f2d9yzHtQLlQ==",
|
||||
"version": "2.1006.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1006.0.tgz",
|
||||
"integrity": "sha512-6qYnCt4mBN+3i/5F+FC2yMETkDHY/IL7gt3EuqKVPcaAO4jU7oXfVSlR60CYRkZWL4fnAurUV14RkJuJyVG/IA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1004.0",
|
||||
"aws-cdk": "^2.1006.0",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2025.2.1
|
||||
Default: 2025.2.2
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -616,6 +616,18 @@ msgstr ""
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
||||
"serializable."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Configure additional headers to be sent. Mapping should return a dictionary "
|
||||
"of key-value pairs"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Only send notification once, for example when sending a webhook into a chat "
|
||||
@ -1756,6 +1768,17 @@ msgid ""
|
||||
"NameIDPolicy of the incoming request will be considered"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "AuthnContextClassRef Property Mapping"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Assertion valid not before current time + this value (Format: hours=-1;"
|
||||
|
@ -19,7 +19,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Marc Schmitt, 2025\n"
|
||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
||||
@ -676,6 +676,22 @@ msgstr "Webhook Slack (ou Discord)"
|
||||
msgid "Email"
|
||||
msgstr "Courriel"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
||||
"serializable."
|
||||
msgstr ""
|
||||
"Personnalise le corps de la requête. Le mappage doit renvoyer des données "
|
||||
"sérialisables en JSON."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Configure additional headers to be sent. Mapping should return a dictionary "
|
||||
"of key-value pairs"
|
||||
msgstr ""
|
||||
"Configure les en-têtes supplémentaires à envoyer. Le mappage doit renvoyer "
|
||||
"un dictionnaire de paires clé-valeur."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Only send notification once, for example when sending a webhook into a chat "
|
||||
@ -1956,6 +1972,20 @@ msgstr ""
|
||||
"Configure la manière dont la valeur NameID sera créée. Si laissé vide, la "
|
||||
"NameIDPolicy de la requête entrante sera prise en compte"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "AuthnContextClassRef Property Mapping"
|
||||
msgstr "Mappage de propriété AuthnContextClassRef"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"Configure comment la valeur AuthnContextClassRef sera créée. Lorsque non "
|
||||
"sélectionné, AuthnContextClassRef sera défini en fonction de quelle méthode "
|
||||
"d'authentification l'utilisateur a utilisé pour s'authentifier."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Assertion valid not before current time + this value (Format: "
|
||||
|
Binary file not shown.
@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||
@ -627,6 +627,18 @@ msgstr "Slack Webhook(Slack/Discord)"
|
||||
msgid "Email"
|
||||
msgstr "电子邮箱"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
||||
"serializable."
|
||||
msgstr "自定义请求体。映射应该返回 JSON 序列化的数据。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Configure additional headers to be sent. Mapping should return a dictionary "
|
||||
"of key-value pairs"
|
||||
msgstr "配置要发送的额外标头。映射应该返回键值对字典。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Only send notification once, for example when sending a webhook into a chat "
|
||||
@ -1782,6 +1794,18 @@ msgid ""
|
||||
"NameIDPolicy of the incoming request will be considered"
|
||||
msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "AuthnContextClassRef Property Mapping"
|
||||
msgstr "AuthnContextClassRef 属性映射"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"配置如何创建 AuthnContextClassRef 值。留空时,AuthnContextClassRef 会基于用户使用的身份验证方式设置。"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Assertion valid not before current time + this value (Format: "
|
||||
|
Binary file not shown.
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
@ -626,6 +626,18 @@ msgstr "Slack Webhook(Slack/Discord)"
|
||||
msgid "Email"
|
||||
msgstr "电子邮箱"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
||||
"serializable."
|
||||
msgstr "自定义请求体。映射应该返回 JSON 序列化的数据。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Configure additional headers to be sent. Mapping should return a dictionary "
|
||||
"of key-value pairs"
|
||||
msgstr "配置要发送的额外标头。映射应该返回键值对字典。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Only send notification once, for example when sending a webhook into a chat "
|
||||
@ -1781,6 +1793,18 @@ msgid ""
|
||||
"NameIDPolicy of the incoming request will be considered"
|
||||
msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "AuthnContextClassRef Property Mapping"
|
||||
msgstr "AuthnContextClassRef 属性映射"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"配置如何创建 AuthnContextClassRef 值。留空时,AuthnContextClassRef 会基于用户使用的身份验证方式设置。"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Assertion valid not before current time + this value (Format: "
|
||||
|
12
package-lock.json
generated
Normal file
12
package-lock.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.2.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.2.1"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.2.1",
|
||||
"version": "2025.2.2",
|
||||
"private": true
|
||||
}
|
||||
|
6120
poetry.lock
generated
6120
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
222
pyproject.toml
222
pyproject.toml
@ -1,8 +1,116 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "authentik"
|
||||
version = "2025.2.1"
|
||||
version = "2025.2.2"
|
||||
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-prometheus",
|
||||
"django-redis",
|
||||
"django-storages[s3]",
|
||||
"django-tenants",
|
||||
"djangorestframework ==3.14.0",
|
||||
"djangorestframework-guardian",
|
||||
"docker",
|
||||
"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",
|
||||
"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/BeryJu/oci-python", rev = "c791b19056769cd67957322806809ab70f5bead8" }
|
||||
|
||||
[project.scripts]
|
||||
ak = "lifecycle.ak:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.bandit]
|
||||
exclude_dirs = ["**/node_modules/**"]
|
||||
@ -30,6 +138,7 @@ skip = [
|
||||
]
|
||||
dictionary = ".github/codespell-dictionary.txt,-"
|
||||
ignore-words = ".github/codespell-words.txt"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py312']
|
||||
@ -60,6 +169,7 @@ select = [
|
||||
ignore = [
|
||||
"DJ001", # Avoid using `null=True` on string-based fields,
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pylint]
|
||||
max-args = 7
|
||||
max-branches = 18
|
||||
@ -107,109 +217,3 @@ filterwarnings = [
|
||||
"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",
|
||||
]
|
||||
|
||||
[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-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 = "*"
|
||||
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 = "*"
|
||||
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"
|
||||
|
232
schema.yml
232
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2025.2.1
|
||||
version: 2025.2.2
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@ -4447,6 +4447,10 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: branding_default_flow_background
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: branding_favicon
|
||||
schema:
|
||||
@ -22191,6 +22195,11 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: authn_context_class_ref_mapping
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: authorization_flow
|
||||
schema:
|
||||
@ -25745,7 +25754,7 @@ paths:
|
||||
description: ''
|
||||
delete:
|
||||
operationId: sources_all_destroy
|
||||
description: Source Viewset
|
||||
description: Prevent deletion of built-in sources
|
||||
parameters:
|
||||
- in: path
|
||||
name: slug
|
||||
@ -41140,6 +41149,10 @@ components:
|
||||
type: string
|
||||
branding_favicon:
|
||||
type: string
|
||||
branding_custom_css:
|
||||
type: string
|
||||
branding_default_flow_background:
|
||||
type: string
|
||||
flow_authentication:
|
||||
type: string
|
||||
format: uuid
|
||||
@ -41199,6 +41212,11 @@ components:
|
||||
branding_favicon:
|
||||
type: string
|
||||
minLength: 1
|
||||
branding_custom_css:
|
||||
type: string
|
||||
branding_default_flow_background:
|
||||
type: string
|
||||
minLength: 1
|
||||
flow_authentication:
|
||||
type: string
|
||||
format: uuid
|
||||
@ -42091,6 +42109,8 @@ components:
|
||||
type: string
|
||||
branding_favicon:
|
||||
type: string
|
||||
branding_custom_css:
|
||||
type: string
|
||||
ui_footer_links:
|
||||
type: array
|
||||
items:
|
||||
@ -42117,6 +42137,7 @@ components:
|
||||
type: string
|
||||
readOnly: true
|
||||
required:
|
||||
- branding_custom_css
|
||||
- branding_favicon
|
||||
- branding_logo
|
||||
- branding_title
|
||||
@ -44120,11 +44141,17 @@ components:
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: |-
|
||||
Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: |-
|
||||
Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
@ -45654,19 +45681,27 @@ components:
|
||||
readOnly: true
|
||||
assigned_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: Get application slug, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: Get application name, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: |-
|
||||
Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: |-
|
||||
Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
@ -46374,11 +46409,17 @@ components:
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: |-
|
||||
Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: |-
|
||||
Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
@ -46869,10 +46910,18 @@ components:
|
||||
webhook_url:
|
||||
type: string
|
||||
format: uri
|
||||
webhook_mapping:
|
||||
webhook_mapping_body:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Customize the body of the request. Mapping should return data
|
||||
that is JSON-serializable.
|
||||
webhook_mapping_headers:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Configure additional headers to be sent. Mapping should return
|
||||
a dictionary of key-value pairs
|
||||
send_once:
|
||||
type: boolean
|
||||
description: Only send notification once, for example when sending a webhook
|
||||
@ -46900,10 +46949,18 @@ components:
|
||||
webhook_url:
|
||||
type: string
|
||||
format: uri
|
||||
webhook_mapping:
|
||||
webhook_mapping_body:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Customize the body of the request. Mapping should return data
|
||||
that is JSON-serializable.
|
||||
webhook_mapping_headers:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Configure additional headers to be sent. Mapping should return
|
||||
a dictionary of key-value pairs
|
||||
send_once:
|
||||
type: boolean
|
||||
description: Only send notification once, for example when sending a webhook
|
||||
@ -46985,19 +47042,27 @@ components:
|
||||
readOnly: true
|
||||
assigned_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: Get application slug, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: Get application name, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: |-
|
||||
Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: |-
|
||||
Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
@ -50120,6 +50185,11 @@ components:
|
||||
branding_favicon:
|
||||
type: string
|
||||
minLength: 1
|
||||
branding_custom_css:
|
||||
type: string
|
||||
branding_default_flow_background:
|
||||
type: string
|
||||
minLength: 1
|
||||
flow_authentication:
|
||||
type: string
|
||||
format: uuid
|
||||
@ -51332,10 +51402,18 @@ components:
|
||||
webhook_url:
|
||||
type: string
|
||||
format: uri
|
||||
webhook_mapping:
|
||||
webhook_mapping_body:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Customize the body of the request. Mapping should return data
|
||||
that is JSON-serializable.
|
||||
webhook_mapping_headers:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Configure additional headers to be sent. Mapping should return
|
||||
a dictionary of key-value pairs
|
||||
send_once:
|
||||
type: boolean
|
||||
description: Only send notification once, for example when sending a webhook
|
||||
@ -52228,6 +52306,14 @@ components:
|
||||
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
|
||||
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:
|
||||
$ref: '#/components/schemas/DigestAlgorithmEnum'
|
||||
signature_algorithm:
|
||||
@ -53790,19 +53876,27 @@ components:
|
||||
readOnly: true
|
||||
assigned_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: Get application slug, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: Get application name, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: |-
|
||||
Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: |-
|
||||
Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
@ -54031,19 +54125,27 @@ components:
|
||||
readOnly: true
|
||||
assigned_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: Get application slug, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: Get application name, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: |-
|
||||
Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: |-
|
||||
Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
@ -54350,19 +54452,27 @@ components:
|
||||
readOnly: true
|
||||
assigned_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: Get application slug, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: Get application name, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: |-
|
||||
Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: |-
|
||||
Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
@ -54515,19 +54625,27 @@ components:
|
||||
readOnly: true
|
||||
assigned_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: Get application slug, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: Get application name, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: |-
|
||||
Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: |-
|
||||
Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
@ -55127,19 +55245,27 @@ components:
|
||||
readOnly: true
|
||||
assigned_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: Get application slug, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: Get application name, return empty string if no application
|
||||
exists
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: |-
|
||||
Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: |-
|
||||
Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
@ -55183,6 +55309,14 @@ components:
|
||||
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
|
||||
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:
|
||||
$ref: '#/components/schemas/DigestAlgorithmEnum'
|
||||
signature_algorithm:
|
||||
@ -55348,6 +55482,14 @@ components:
|
||||
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
|
||||
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:
|
||||
$ref: '#/components/schemas/DigestAlgorithmEnum'
|
||||
signature_algorithm:
|
||||
@ -55826,11 +55968,17 @@ components:
|
||||
readOnly: true
|
||||
assigned_backchannel_application_slug:
|
||||
type: string
|
||||
description: Internal application name, used in URLs.
|
||||
description: |-
|
||||
Get backchannel application slug.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
assigned_backchannel_application_name:
|
||||
type: string
|
||||
description: Application's display Name.
|
||||
description: |-
|
||||
Get backchannel application name.
|
||||
|
||||
Returns an empty string if no backchannel application exists.
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
@ -55897,7 +56045,10 @@ components:
|
||||
readOnly: true
|
||||
provider:
|
||||
type: integer
|
||||
attributes:
|
||||
readOnly: true
|
||||
required:
|
||||
- attributes
|
||||
- group
|
||||
- group_obj
|
||||
- id
|
||||
@ -55984,7 +56135,10 @@ components:
|
||||
readOnly: true
|
||||
provider:
|
||||
type: integer
|
||||
attributes:
|
||||
readOnly: true
|
||||
required:
|
||||
- attributes
|
||||
- id
|
||||
- provider
|
||||
- scim_id
|
||||
|
@ -7,6 +7,8 @@ services:
|
||||
environment:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
POSTGRES_DB: authentik
|
||||
command:
|
||||
["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"]
|
||||
ports:
|
||||
- 127.0.0.1:5432:5432
|
||||
restart: always
|
||||
|
@ -5,45 +5,85 @@ from yaml import safe_dump
|
||||
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
with open("local.env.yml", "w", encoding="utf-8") as _config:
|
||||
safe_dump(
|
||||
{
|
||||
"debug": True,
|
||||
"log_level": "debug",
|
||||
"secret_key": generate_id(),
|
||||
"postgresql": {
|
||||
"user": "postgres",
|
||||
},
|
||||
"outposts": {
|
||||
"container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s",
|
||||
"disable_embedded_outpost": False,
|
||||
},
|
||||
"blueprints_dir": "./blueprints",
|
||||
"cert_discovery_dir": "./certs",
|
||||
"events": {
|
||||
"processors": {
|
||||
"geoip": "tests/GeoLite2-City-Test.mmdb",
|
||||
"asn": "tests/GeoLite2-ASN-Test.mmdb",
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"media": {
|
||||
"backend": "file",
|
||||
"s3": {
|
||||
"endpoint": "http://localhost:8020",
|
||||
"access_key": "accessKey1",
|
||||
"secret_key": "secretKey1",
|
||||
"bucket_name": "authentik-media",
|
||||
"custom_domain": "localhost:8020/authentik-media",
|
||||
"secure_urls": False,
|
||||
},
|
||||
|
||||
def generate_local_config():
|
||||
"""Generate a local development configuration"""
|
||||
# TODO: This should be generated and validated against a schema, such as Pydantic.
|
||||
|
||||
return {
|
||||
"debug": True,
|
||||
"log_level": "debug",
|
||||
"secret_key": generate_id(),
|
||||
"postgresql": {
|
||||
"user": "postgres",
|
||||
},
|
||||
"outposts": {
|
||||
"container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s",
|
||||
"disable_embedded_outpost": False,
|
||||
},
|
||||
"blueprints_dir": "./blueprints",
|
||||
"cert_discovery_dir": "./certs",
|
||||
"events": {
|
||||
"processors": {
|
||||
"geoip": "tests/GeoLite2-City-Test.mmdb",
|
||||
"asn": "tests/GeoLite2-ASN-Test.mmdb",
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"media": {
|
||||
"backend": "file",
|
||||
"s3": {
|
||||
"endpoint": "http://localhost:8020",
|
||||
"access_key": "accessKey1",
|
||||
"secret_key": "secretKey1",
|
||||
"bucket_name": "authentik-media",
|
||||
"custom_domain": "localhost:8020/authentik-media",
|
||||
"secure_urls": False,
|
||||
},
|
||||
},
|
||||
"tenants": {
|
||||
"enabled": False,
|
||||
"api_key": generate_id(),
|
||||
},
|
||||
},
|
||||
_config,
|
||||
default_flow_style=False,
|
||||
"tenants": {
|
||||
"enabled": False,
|
||||
"api_key": generate_id(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config_file_name = "local.env.yml"
|
||||
|
||||
with open(config_file_name, "w", encoding="utf-8") as _config:
|
||||
_config.write(
|
||||
"""
|
||||
# Local authentik configuration overrides
|
||||
#
|
||||
# https://docs.goauthentik.io/docs/install-config/configuration/
|
||||
#
|
||||
# To regenerate this file, run the following command from the repository root:
|
||||
#
|
||||
# ```shell
|
||||
# make gen-dev-config
|
||||
# ```
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
safe_dump(
|
||||
generate_local_config(),
|
||||
_config,
|
||||
default_flow_style=False,
|
||||
)
|
||||
|
||||
print(
|
||||
f"""
|
||||
---
|
||||
|
||||
Generated configuration file: {config_file_name}
|
||||
|
||||
For more information on how to use this configuration, see:
|
||||
|
||||
https://docs.goauthentik.io/docs/install-config/configuration/
|
||||
|
||||
---
|
||||
"""
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user