Merge branch 'main' into celery-2-dramatiq
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
@ -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*))?
|
||||
|
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,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:
|
||||
|
@ -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
|
||||
|
@ -5,7 +5,7 @@
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
identity and expression, level of experience, education, socioeconomic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
|
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.8 AS uv
|
||||
# Stage 6: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_NATIVE_TLS=1 \
|
||||
UV_PYTHON_DOWNLOADS=0
|
||||
|
||||
WORKDIR /ak-root/
|
||||
|
||||
COPY --from=uv /uv /uvx /bin/
|
||||
|
||||
# Stage 7: Python dependencies
|
||||
FROM python-base AS python-deps
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG 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" ]
|
||||
|
55
Makefile
55
Makefile
@ -4,7 +4,7 @@
|
||||
PWD = $(shell pwd)
|
||||
UID = $(shell id -u)
|
||||
GID = $(shell id -g)
|
||||
NPM_VERSION = $(shell poetry run python -m scripts.generate_semver)
|
||||
NPM_VERSION = $(shell python -m scripts.generate_semver)
|
||||
PY_SOURCES = authentik tests scripts lifecycle .github
|
||||
DOCKER_IMAGE ?= "authentik:test"
|
||||
|
||||
@ -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
|
||||
@ -145,7 +148,7 @@ gen-client-py: gen-clean-py ## Build and install the authentik API for Python
|
||||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
docker.io/openapitools/openapi-generator-cli:v7.4.0 generate \
|
||||
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
|
||||
-i /local/schema.yml \
|
||||
-g python \
|
||||
-o /local/${GEN_API_PY} \
|
||||
@ -173,7 +176,7 @@ gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
|
||||
rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/
|
||||
|
||||
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"
|
||||
|
||||
|
||||
|
@ -5,6 +5,7 @@ from collections.abc import Iterable
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
@ -154,6 +155,17 @@ class SourceViewSet(
|
||||
matching_sources.append(source_settings.validated_data)
|
||||
return Response(matching_sources)
|
||||
|
||||
def destroy(self, request: Request, *args, **kwargs):
|
||||
"""Prevent deletion of built-in sources"""
|
||||
instance: Source = self.get_object()
|
||||
|
||||
if instance.managed == Source.MANAGED_INBUILT:
|
||||
raise ValidationError(
|
||||
{"detail": "Built-in sources cannot be deleted"}, code="protected"
|
||||
)
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UserSourceConnectionSerializer(SourceSerializer):
|
||||
"""User source connection"""
|
||||
|
@ -32,5 +32,5 @@ class AuthentikCoreConfig(ManagedAppConfig):
|
||||
"name": "authentik Built-in",
|
||||
"slug": "authentik-built-in",
|
||||
},
|
||||
managed="goauthentik.io/sources/inbuilt",
|
||||
managed=Source.MANAGED_INBUILT,
|
||||
)
|
||||
|
@ -678,6 +678,8 @@ class SourceGroupMatchingModes(models.TextChoices):
|
||||
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||
|
||||
MANAGED_INBUILT = "goauthentik.io/sources/inbuilt"
|
||||
|
||||
name = models.TextField(help_text=_("Source's display Name."))
|
||||
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Base Kubernetes Reconciler"""
|
||||
|
||||
import re
|
||||
from dataclasses import asdict
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
@ -67,7 +68,8 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get the name of the object this reconciler manages"""
|
||||
return (
|
||||
|
||||
base_name = (
|
||||
self.controller.outpost.config.object_naming_template
|
||||
% {
|
||||
"name": slugify(self.controller.outpost.name),
|
||||
@ -75,6 +77,16 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
}
|
||||
).lower()
|
||||
|
||||
formatted = slugify(base_name)
|
||||
formatted = re.sub(r"[^a-z0-9-]", "-", formatted)
|
||||
formatted = re.sub(r"-+", "-", formatted)
|
||||
formatted = formatted[:63]
|
||||
|
||||
if not formatted:
|
||||
formatted = f"outpost-{self.controller.outpost.uuid.hex}"[:63]
|
||||
|
||||
return formatted
|
||||
|
||||
def get_patched_reference_object(self) -> T:
|
||||
"""Get patched reference object"""
|
||||
reference = self.get_reference_object()
|
||||
@ -112,7 +124,6 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
try:
|
||||
current = self.retrieve()
|
||||
except (OpenApiException, HTTPError) as exc:
|
||||
|
||||
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
|
||||
self.logger.debug("Failed to get current, triggering recreate")
|
||||
raise NeedsRecreate from exc
|
||||
@ -156,7 +167,6 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
self.delete(current)
|
||||
self.logger.debug("Removing")
|
||||
except (OpenApiException, HTTPError) as exc:
|
||||
|
||||
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
|
||||
self.logger.debug("Failed to get current, assuming non-existent")
|
||||
return
|
||||
|
@ -61,9 +61,14 @@ class KubernetesController(BaseController):
|
||||
client: KubernetesClient
|
||||
connection: KubernetesServiceConnection
|
||||
|
||||
def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
outpost: Outpost,
|
||||
connection: KubernetesServiceConnection,
|
||||
client: KubernetesClient | None = None,
|
||||
) -> None:
|
||||
super().__init__(outpost, connection)
|
||||
self.client = KubernetesClient(connection)
|
||||
self.client = client if client else KubernetesClient(connection)
|
||||
self.reconcilers = {
|
||||
SecretReconciler.reconciler_name(): SecretReconciler,
|
||||
DeploymentReconciler.reconciler_name(): DeploymentReconciler,
|
||||
|
44
authentik/outposts/tests/test_controller_k8s.py
Normal file
44
authentik/outposts/tests/test_controller_k8s.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Kubernetes controller tests"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
|
||||
|
||||
|
||||
class KubernetesControllerTests(TestCase):
|
||||
"""Kubernetes controller tests"""
|
||||
|
||||
@reconcile_app("authentik_outposts")
|
||||
def setUp(self) -> None:
|
||||
self.outpost = Outpost.objects.create(
|
||||
name="test",
|
||||
type=OutpostType.PROXY,
|
||||
)
|
||||
self.integration = KubernetesServiceConnection(name="test")
|
||||
|
||||
def test_gen_name(self):
|
||||
"""Ensure the generated name is valid"""
|
||||
controller = KubernetesController(
|
||||
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(),
|
||||
self.integration,
|
||||
# Pass something not-none as client so we don't
|
||||
# attempt to connect to K8s as that's not needed
|
||||
client=self,
|
||||
)
|
||||
rec = DeploymentReconciler(controller)
|
||||
self.assertEqual(rec.name, "ak-outpost-authentik-embedded-outpost")
|
||||
|
||||
controller.outpost.name = generate_id()
|
||||
self.assertLess(len(rec.name), 64)
|
||||
|
||||
# Test custom naming template
|
||||
_cfg = controller.outpost.config
|
||||
_cfg.object_naming_template = ""
|
||||
controller.outpost.config = _cfg
|
||||
self.assertEqual(rec.name, f"outpost-{controller.outpost.uuid.hex}")
|
||||
self.assertLess(len(rec.name), 64)
|
@ -254,10 +254,10 @@ class OAuthAuthorizationParams:
|
||||
raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type, self.state)
|
||||
if SCOPE_OFFLINE_ACCESS in self.scope:
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
if PROMPT_CONSENT not in self.prompt:
|
||||
# Instead of ignoring the `offline_access` scope when `prompt`
|
||||
# isn't set to `consent`, we set override it ourselves
|
||||
self.prompt.add(PROMPT_CONSENT)
|
||||
# Don't explicitly request consent with offline_access, as the spec allows for
|
||||
# "other conditions for processing the request permitting offline access to the
|
||||
# requested resources are in place"
|
||||
# which we interpret as "the admin picks an authorization flow with or without consent"
|
||||
if self.response_type not in [
|
||||
ResponseTypes.CODE,
|
||||
ResponseTypes.CODE_TOKEN,
|
||||
|
@ -1,9 +1,9 @@
|
||||
"""RAC app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikProviderRAC(AppConfig):
|
||||
class AuthentikProviderRAC(ManagedAppConfig):
|
||||
"""authentik rac app config"""
|
||||
|
||||
name = "authentik.providers.rac"
|
||||
|
@ -4,8 +4,7 @@ from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.db.models.signals import post_delete, post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpRequest
|
||||
|
||||
@ -46,12 +45,8 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Endpoint)
|
||||
def post_save_endpoint(sender: type[Model], instance, created: bool, **_):
|
||||
"""Clear user's endpoint cache upon endpoint creation"""
|
||||
if not created: # pragma: no cover
|
||||
return
|
||||
|
||||
# Delete user endpoint cache
|
||||
@receiver([post_save, post_delete], sender=Endpoint)
|
||||
def post_save_post_delete_endpoint(**_):
|
||||
"""Clear user's endpoint cache upon endpoint creation or deletion"""
|
||||
keys = cache.keys(user_endpoint_cache_key("*"))
|
||||
cache.delete_many(keys)
|
||||
|
@ -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(
|
||||
|
@ -28,6 +28,7 @@ class SCIMProviderSerializer(ProviderSerializer):
|
||||
"url",
|
||||
"verify_certificates",
|
||||
"token",
|
||||
"compatibility_mode",
|
||||
"exclude_users_service_account",
|
||||
"filter_group",
|
||||
"dry_run",
|
||||
|
@ -24,7 +24,9 @@ class SCIMProviderUserSerializer(ModelSerializer):
|
||||
"user",
|
||||
"user_obj",
|
||||
"provider",
|
||||
"attributes",
|
||||
]
|
||||
extra_kwargs = {"attributes": {"read_only": True}}
|
||||
|
||||
|
||||
class SCIMProviderUserViewSet(
|
||||
|
@ -22,7 +22,7 @@ from authentik.lib.sync.outgoing.exceptions import (
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.providers.scim.clients.exceptions import SCIMRequestException
|
||||
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
|
||||
from authentik.providers.scim.models import SCIMProvider
|
||||
from authentik.providers.scim.models import SCIMCompatibilityMode, SCIMProvider
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models import Model
|
||||
@ -90,9 +90,14 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
|
||||
"""Get Service provider config"""
|
||||
default_config = ServiceProviderConfiguration.default()
|
||||
try:
|
||||
return ServiceProviderConfiguration.model_validate(
|
||||
config = ServiceProviderConfiguration.model_validate(
|
||||
self._request("GET", "/ServiceProviderConfig")
|
||||
)
|
||||
if self.provider.compatibility_mode == SCIMCompatibilityMode.AWS:
|
||||
config.patch.supported = False
|
||||
if self.provider.compatibility_mode == SCIMCompatibilityMode.SLACK:
|
||||
config.filter.supported = True
|
||||
return config
|
||||
except (ValidationError, SCIMRequestException, NotFoundSyncException) as exc:
|
||||
self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
|
||||
return default_config
|
||||
|
@ -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)
|
||||
|
@ -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,24 @@
|
||||
# Generated by Django 5.0.12 on 2025-03-07 23:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_scim", "0011_scimprovider_dry_run"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scimprovider",
|
||||
name="compatibility_mode",
|
||||
field=models.CharField(
|
||||
choices=[("default", "Default"), ("aws", "AWS"), ("slack", "Slack")],
|
||||
default="default",
|
||||
help_text="Alter authentik behavior for vendor-specific SCIM implementations.",
|
||||
max_length=30,
|
||||
verbose_name="SCIM Compatibility Mode",
|
||||
),
|
||||
),
|
||||
]
|
@ -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]:
|
||||
@ -57,6 +59,14 @@ class SCIMProviderGroup(SerializerModel):
|
||||
return f"SCIM Provider Group {self.group_id} to {self.provider_id}"
|
||||
|
||||
|
||||
class SCIMCompatibilityMode(models.TextChoices):
|
||||
"""SCIM compatibility mode"""
|
||||
|
||||
DEFAULT = "default", _("Default")
|
||||
AWS = "aws", _("AWS")
|
||||
SLACK = "slack", _("Slack")
|
||||
|
||||
|
||||
class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
"""SCIM 2.0 provider to create users and groups in external applications"""
|
||||
|
||||
@ -77,6 +87,14 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
help_text=_("Property mappings used for group creation/updating."),
|
||||
)
|
||||
|
||||
compatibility_mode = models.CharField(
|
||||
max_length=30,
|
||||
choices=SCIMCompatibilityMode.choices,
|
||||
default=SCIMCompatibilityMode.DEFAULT,
|
||||
verbose_name=_("SCIM Compatibility Mode"),
|
||||
help_text=_("Alter authentik behavior for vendor-specific SCIM implementations."),
|
||||
)
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/scim.png")
|
||||
|
@ -68,8 +68,6 @@ class OAuth2Client(BaseOAuthClient):
|
||||
error_desc = self.get_request_arg("error_description", None)
|
||||
return {"error": error_desc or error or _("No token received.")}
|
||||
args = {
|
||||
"client_id": self.get_client_id(),
|
||||
"client_secret": self.get_client_secret(),
|
||||
"redirect_uri": callback,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
|
@ -28,7 +28,7 @@ def update_well_known_jwks(self: SystemTask):
|
||||
LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text)
|
||||
messages.append(f"Failed to update OIDC configuration for {source.slug}")
|
||||
continue
|
||||
config = well_known_config.json()
|
||||
config: dict = well_known_config.json()
|
||||
try:
|
||||
dirty = False
|
||||
source_attr_key = (
|
||||
@ -40,7 +40,9 @@ def update_well_known_jwks(self: SystemTask):
|
||||
for source_attr, config_key in source_attr_key:
|
||||
# Check if we're actually changing anything to only
|
||||
# save when something has changed
|
||||
if getattr(source, source_attr, "") != config[config_key]:
|
||||
if config_key not in config:
|
||||
continue
|
||||
if getattr(source, source_attr, "") != config.get(config_key, ""):
|
||||
dirty = True
|
||||
setattr(source, source_attr, config[config_key])
|
||||
except (IndexError, KeyError) as exc:
|
||||
|
@ -25,8 +25,10 @@ class RedditOAuth2Client(UserprofileHeaderAuthClient):
|
||||
|
||||
def get_access_token(self, **request_kwargs):
|
||||
"Fetch access token from callback request."
|
||||
auth = HTTPBasicAuth(self.source.consumer_key, self.source.consumer_secret)
|
||||
return super().get_access_token(auth=auth)
|
||||
request_kwargs["auth"] = HTTPBasicAuth(
|
||||
self.source.consumer_key, self.source.consumer_secret
|
||||
)
|
||||
return super().get_access_token(**request_kwargs)
|
||||
|
||||
|
||||
class RedditOAuth2Callback(OAuthCallback):
|
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,54 @@
|
||||
# Generated by Django 5.0.12 on 2025-02-27 04:32
|
||||
|
||||
import authentik.lib.utils.time
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def convert_integer_to_string_format(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
EmailStage = apps.get_model("authentik_stages_email", "EmailStage")
|
||||
for stage in EmailStage.objects.using(db_alias).all():
|
||||
stage.token_expiry = f"minutes={stage.token_expiry}"
|
||||
stage.save(using=db_alias)
|
||||
|
||||
|
||||
def convert_string_to_integer_format(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
EmailStage = apps.get_model("authentik_stages_email", "EmailStage")
|
||||
for stage in EmailStage.objects.using(db_alias).all():
|
||||
# Check if token_expiry is a string
|
||||
if isinstance(stage.token_expiry, str):
|
||||
try:
|
||||
# Use the timedelta_from_string utility to convert to timedelta
|
||||
# then convert to minutes by dividing seconds by 60
|
||||
td = timedelta_from_string(stage.token_expiry)
|
||||
minutes_value = int(td.total_seconds() / 60)
|
||||
stage.token_expiry = minutes_value
|
||||
stage.save(using=db_alias)
|
||||
except (ValueError, TypeError):
|
||||
# If the string can't be parsed or converted properly, skip
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_email", "0004_emailstage_activate_user_on_success"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="emailstage",
|
||||
name="token_expiry",
|
||||
field=models.TextField(
|
||||
default="minutes=30",
|
||||
help_text="Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
convert_integer_to_string_format,
|
||||
convert_string_to_integer_format,
|
||||
),
|
||||
]
|
@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@ -74,8 +75,10 @@ class EmailStage(Stage):
|
||||
default=False, help_text=_("Activate users upon completion of stage.")
|
||||
)
|
||||
|
||||
token_expiry = models.IntegerField(
|
||||
default=30, help_text=_("Time in minutes the token sent is valid.")
|
||||
token_expiry = models.TextField(
|
||||
default="minutes=30",
|
||||
validators=[timedelta_string_validator],
|
||||
help_text=_("Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."),
|
||||
)
|
||||
subject = models.TextField(default="authentik")
|
||||
template = models.TextField(default=EmailTemplates.PASSWORD_RESET)
|
||||
|
@ -22,6 +22,7 @@ from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDI
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
@ -73,8 +74,8 @@ class EmailStageView(ChallengeStageView):
|
||||
"""Get token"""
|
||||
pending_user = self.get_pending_user()
|
||||
current_stage: EmailStage = self.executor.current_stage
|
||||
valid_delta = timedelta(
|
||||
minutes=current_stage.token_expiry + 1
|
||||
valid_delta = timedelta_from_string(current_stage.token_expiry) + timedelta(
|
||||
minutes=1
|
||||
) # + 1 because django timesince always rounds down
|
||||
identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
|
||||
# Don't check for validity here, we only care if the token exists
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -57,7 +57,7 @@ entries:
|
||||
use_ssl: false
|
||||
timeout: 10
|
||||
from_address: system@authentik.local
|
||||
token_expiry: 30
|
||||
token_expiry: minutes=30
|
||||
subject: authentik
|
||||
template: email/password_reset.html
|
||||
activate_user_on_success: true
|
||||
|
@ -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": [
|
||||
@ -6661,6 +6666,16 @@
|
||||
"title": "Token",
|
||||
"description": "Authentication token"
|
||||
},
|
||||
"compatibility_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"aws",
|
||||
"slack"
|
||||
],
|
||||
"title": "SCIM Compatibility Mode",
|
||||
"description": "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
},
|
||||
"exclude_users_service_account": {
|
||||
"type": "boolean",
|
||||
"title": "Exclude users service account"
|
||||
@ -11369,11 +11384,10 @@
|
||||
"title": "From address"
|
||||
},
|
||||
"token_expiry": {
|
||||
"type": "integer",
|
||||
"minimum": -2147483648,
|
||||
"maximum": 2147483647,
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Token expiry",
|
||||
"description": "Time in minutes the token sent is valid."
|
||||
"description": "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
|
@ -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:
|
||||
|
10
go.mod
10
go.mod
@ -6,7 +6,7 @@ toolchain go1.24.0
|
||||
|
||||
require (
|
||||
beryju.io/ldap v0.1.0
|
||||
github.com/coreos/go-oidc/v3 v3.12.0
|
||||
github.com/coreos/go-oidc/v3 v3.13.0
|
||||
github.com/getsentry/sentry-go v0.31.1
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
@ -29,7 +29,7 @@ require (
|
||||
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.2
|
||||
goauthentik.io/api/v3 v3.2025022.3
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/sync v0.12.0
|
||||
@ -76,9 +76,9 @@ require (
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
22
go.sum
22
go.sum
@ -55,8 +55,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
|
||||
github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@ -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.2 h1:9y87piH47omtkWxQpKZaKai/+jh+cJdLxj5MC2Y/ZLI=
|
||||
goauthentik.io/api/v3 v3.2025021.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025022.3 h1:cipaxl0il4/s1fU2f6+CD7nzgAktbV0XD7r5qHh0fUc=
|
||||
goauthentik.io/api/v3 v3.2025022.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@ -313,8 +313,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -386,8 +386,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -449,8 +450,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@ -471,8 +472,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2025.2.1"
|
||||
const VERSION = "2025.2.2"
|
||||
|
@ -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.1003.0",
|
||||
"aws-cdk": "^2.1005.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,9 +17,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1003.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1003.0.tgz",
|
||||
"integrity": "sha512-FORPDGW8oUg4tXFlhX+lv/j+152LO9wwi3/CwNr1WY3c3HwJUtc0fZGb2B3+Fzy6NhLWGHJclUsJPEhjEt8Nhg==",
|
||||
"version": "2.1005.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz",
|
||||
"integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1003.0",
|
||||
"aws-cdk": "^2.1005.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-02 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -1883,6 +1883,18 @@ msgstr ""
|
||||
msgid "SAML Providers from Metadata"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "AWS"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Slack"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr ""
|
||||
@ -1891,6 +1903,14 @@ msgstr ""
|
||||
msgid "Authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Compatibility Mode"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
msgstr ""
|
||||
@ -2535,6 +2555,7 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
msgstr ""
|
||||
|
||||
@ -2873,10 +2894,6 @@ msgstr ""
|
||||
msgid "Activate users upon completion of stage."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time in minutes the token sent is valid."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stage"
|
||||
msgstr ""
|
||||
|
@ -9,9 +9,9 @@
|
||||
# Kyllian Delaye-Maillot, 2023
|
||||
# Manuel Viens, 2023
|
||||
# Mordecai, 2023
|
||||
# Charles Leclerc, 2024
|
||||
# nerdinator <florian.dupret@gmail.com>, 2024
|
||||
# Tina, 2024
|
||||
# Charles Leclerc, 2025
|
||||
# Marc Schmitt, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
@ -19,7 +19,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-02 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Marc Schmitt, 2025\n"
|
||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
||||
@ -2097,6 +2097,18 @@ msgstr "Fournisseur SAML depuis métadonnées"
|
||||
msgid "SAML Providers from Metadata"
|
||||
msgstr "Fournisseurs SAML depuis métadonnées"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Default"
|
||||
msgstr "Par défaut"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "AWS"
|
||||
msgstr "AWS"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Slack"
|
||||
msgstr "Slack"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr "URL de base pour les requêtes SCIM, se terminant généralement par /v2"
|
||||
@ -2105,6 +2117,16 @@ msgstr "URL de base pour les requêtes SCIM, se terminant généralement par /v2
|
||||
msgid "Authentication token"
|
||||
msgstr "Jeton d'authentification"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Compatibility Mode"
|
||||
msgstr "Mode de compatibilité SCIM"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr ""
|
||||
"Change le comportement d'authentik en fonction des spécificités "
|
||||
"d'implémentations des fournisseurs SCIM."
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
msgstr "Fournisseur SCIM"
|
||||
@ -2797,6 +2819,7 @@ msgstr ""
|
||||
"les paramètres de connexion ci-dessous seront ignorés."
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
msgstr ""
|
||||
"Durée de validité du jeton envoyé (Format : hours=3,minutes=17,seconds=300)."
|
||||
@ -3168,10 +3191,6 @@ msgstr "Confirmation du Compte"
|
||||
msgid "Activate users upon completion of stage."
|
||||
msgstr "Activer les utilisateurs à la complétion de l'étape."
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time in minutes the token sent is valid."
|
||||
msgstr "Temps en minutes durant lequel le jeton envoyé est valide."
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stage"
|
||||
msgstr "Étape Email"
|
||||
|
Binary file not shown.
@ -7,7 +7,7 @@
|
||||
# Chen Zhikai, 2022
|
||||
# 刘松, 2022
|
||||
# Tianhao Chai <cth451@gmail.com>, 2024
|
||||
# Jens L. <jens@goauthentik.io>, 2024
|
||||
# Jens L. <jens@goauthentik.io>, 2025
|
||||
# deluxghost, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-02 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||
@ -1909,6 +1909,18 @@ msgstr "来自元数据的 SAML 提供程序"
|
||||
msgid "SAML Providers from Metadata"
|
||||
msgstr "来自元数据的 SAML 提供程序"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Default"
|
||||
msgstr "默认"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "AWS"
|
||||
msgstr "AWS"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Slack"
|
||||
msgstr "Slack"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
||||
@ -1917,6 +1929,14 @@ msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
||||
msgid "Authentication token"
|
||||
msgstr "身份验证令牌"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Compatibility Mode"
|
||||
msgstr "SCIM 兼容模式"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr "更改 authentik 的行为,以兼容特定厂商的 SCIM 实现。"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
msgstr "SCIM 提供程序"
|
||||
@ -2571,6 +2591,7 @@ msgid ""
|
||||
msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
msgstr "发出令牌有效的时间(格式:hours=3,minutes=17,seconds=300)。"
|
||||
|
||||
@ -2920,10 +2941,6 @@ msgstr "账户确认"
|
||||
msgid "Activate users upon completion of stage."
|
||||
msgstr "完成阶段后激活用户。"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time in minutes the token sent is valid."
|
||||
msgstr "发出令牌的有效时间(单位为分钟)。"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stage"
|
||||
msgstr "电子邮件阶段"
|
||||
|
Binary file not shown.
@ -6,7 +6,7 @@
|
||||
# Translators:
|
||||
# Chen Zhikai, 2022
|
||||
# 刘松, 2022
|
||||
# Jens L. <jens@goauthentik.io>, 2024
|
||||
# Jens L. <jens@goauthentik.io>, 2025
|
||||
# deluxghost, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-02 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
@ -1908,6 +1908,18 @@ msgstr "来自元数据的 SAML 提供程序"
|
||||
msgid "SAML Providers from Metadata"
|
||||
msgstr "来自元数据的 SAML 提供程序"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Default"
|
||||
msgstr "默认"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "AWS"
|
||||
msgstr "AWS"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Slack"
|
||||
msgstr "Slack"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
||||
@ -1916,6 +1928,14 @@ msgstr "SCIM 请求的基础 URL,通常以 /v2 结尾"
|
||||
msgid "Authentication token"
|
||||
msgstr "身份验证令牌"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Compatibility Mode"
|
||||
msgstr "SCIM 兼容模式"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr "更改 authentik 的行为,以兼容特定厂商的 SCIM 实现。"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
msgstr "SCIM 提供程序"
|
||||
@ -2570,6 +2590,7 @@ msgid ""
|
||||
msgstr "启用后,将使用全局电子邮件连接设置,下面的连接设置将被忽略。"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
msgstr "发出令牌有效的时间(格式:hours=3,minutes=17,seconds=300)。"
|
||||
|
||||
@ -2919,10 +2940,6 @@ msgstr "账户确认"
|
||||
msgid "Activate users upon completion of stage."
|
||||
msgstr "完成阶段后激活用户。"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Time in minutes the token sent is valid."
|
||||
msgstr "发出令牌的有效时间(单位为分钟)。"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stage"
|
||||
msgstr "电子邮件阶段"
|
||||
|
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
|
||||
}
|
||||
|
6359
poetry.lock
generated
6359
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
229
pyproject.toml
229
pyproject.toml
@ -1,8 +1,119 @@
|
||||
[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-pgtrigger",
|
||||
"django-prometheus",
|
||||
"django-redis",
|
||||
"django-storages[s3]",
|
||||
"django-tenants",
|
||||
"djangorestframework ==3.14.0",
|
||||
"djangorestframework-guardian",
|
||||
"docker",
|
||||
"dramatiq[watch]",
|
||||
"drf-orjson-renderer",
|
||||
"drf-spectacular",
|
||||
"dumb-init",
|
||||
"duo-client",
|
||||
"fido2",
|
||||
"flower",
|
||||
"geoip2",
|
||||
"geopy",
|
||||
"google-api-python-client",
|
||||
"gssapi",
|
||||
"gunicorn",
|
||||
"jsonpatch",
|
||||
"jwcrypto",
|
||||
"kubernetes",
|
||||
"ldap3",
|
||||
"lxml",
|
||||
"msgraph-sdk",
|
||||
"opencontainers",
|
||||
"packaging",
|
||||
"paramiko",
|
||||
"psycopg[c]",
|
||||
"pydantic",
|
||||
"pydantic-scim",
|
||||
"pyjwt",
|
||||
"pyrad",
|
||||
"python-kadmin-rs ==0.5.3",
|
||||
"pyyaml",
|
||||
"requests-oauthlib",
|
||||
"scim2-filter-parser",
|
||||
"sentry-sdk",
|
||||
"service_identity",
|
||||
"setproctitle",
|
||||
"structlog",
|
||||
"swagger-spec-validator",
|
||||
"tenacity",
|
||||
"tenant-schemas-celery",
|
||||
"twilio",
|
||||
"ua-parser",
|
||||
"unidecode",
|
||||
"urllib3 <3",
|
||||
"uvicorn[standard]",
|
||||
"watchdog",
|
||||
"webauthn",
|
||||
"wsproto",
|
||||
"xmlsec <= 1.3.14",
|
||||
"zxcvbn",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"aws-cdk-lib",
|
||||
"bandit",
|
||||
"black",
|
||||
"bump2version",
|
||||
"channels[daphne]",
|
||||
"codespell",
|
||||
"colorama",
|
||||
"constructs",
|
||||
"coverage[toml]",
|
||||
"debugpy",
|
||||
"drf-jsonschema-serializer",
|
||||
"freezegun",
|
||||
"importlib-metadata",
|
||||
"k5test",
|
||||
"pdoc",
|
||||
"pytest",
|
||||
"pytest-django",
|
||||
"pytest-github-actions-annotate-failures",
|
||||
"pytest-randomly",
|
||||
"pytest-timeout",
|
||||
"requests-mock",
|
||||
"ruff",
|
||||
"selenium",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" }
|
||||
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf" }
|
||||
|
||||
[project.scripts]
|
||||
ak = "lifecycle.ak:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.bandit]
|
||||
exclude_dirs = ["**/node_modules/**"]
|
||||
@ -20,6 +131,7 @@ skip = [
|
||||
"**/storybook-static",
|
||||
"**/web/src/locales",
|
||||
"**/web/xliff",
|
||||
"./web/storybook-static",
|
||||
"./website/build",
|
||||
"./gen-ts-api",
|
||||
"./gen-py-api",
|
||||
@ -30,6 +142,7 @@ skip = [
|
||||
]
|
||||
dictionary = ".github/codespell-dictionary.txt,-"
|
||||
ignore-words = ".github/codespell-words.txt"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py312']
|
||||
@ -60,6 +173,7 @@ select = [
|
||||
ignore = [
|
||||
"DJ001", # Avoid using `null=True` on string-based fields,
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pylint]
|
||||
max-args = 7
|
||||
max-branches = 18
|
||||
@ -107,112 +221,3 @@ filterwarnings = [
|
||||
"ignore:defusedxml.lxml is no longer supported and will be removed in a future release.:DeprecationWarning",
|
||||
"ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
argon2-cffi = "*"
|
||||
celery = "*"
|
||||
channels = "*"
|
||||
channels-redis = "*"
|
||||
cryptography = "*"
|
||||
dacite = "*"
|
||||
deepmerge = "*"
|
||||
defusedxml = "*"
|
||||
django = "*"
|
||||
django-countries = "*"
|
||||
django-cte = "*"
|
||||
django-filter = "*"
|
||||
django-guardian = "*"
|
||||
django-model-utils = "*"
|
||||
django-pglock = "*"
|
||||
django-pgtrigger = "*"
|
||||
django-prometheus = "*"
|
||||
django-redis = "*"
|
||||
django-storages = { extras = ["s3"], version = "*" }
|
||||
# See https://github.com/django-tenants/django-tenants/pull/997
|
||||
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" }
|
||||
djangorestframework = "3.14.0"
|
||||
djangorestframework-guardian = "*"
|
||||
docker = "*"
|
||||
dramatiq = { version = "*", extras = ["watch"] }
|
||||
drf-orjson-renderer = "*"
|
||||
drf-spectacular = "*"
|
||||
dumb-init = "*"
|
||||
duo-client = "*"
|
||||
fido2 = "*"
|
||||
flower = "*"
|
||||
geoip2 = "*"
|
||||
geopy = "*"
|
||||
google-api-python-client = "*"
|
||||
gunicorn = "*"
|
||||
gssapi = "*"
|
||||
jsonpatch = "*"
|
||||
jwcrypto = "*"
|
||||
kubernetes = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
msgraph-sdk = "*"
|
||||
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = [
|
||||
"reggie",
|
||||
] }
|
||||
packaging = "*"
|
||||
paramiko = "*"
|
||||
psycopg = { extras = ["c"], version = "*" }
|
||||
pydantic = "*"
|
||||
pydantic-scim = "*"
|
||||
pyjwt = "*"
|
||||
pyrad = "*"
|
||||
python = "~3.12"
|
||||
python-kadmin-rs = "0.5.3"
|
||||
pyyaml = "*"
|
||||
requests-oauthlib = "*"
|
||||
scim2-filter-parser = "*"
|
||||
sentry-sdk = "*"
|
||||
service_identity = "*"
|
||||
setproctitle = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
tenacity = "*"
|
||||
tenant-schemas-celery = "*"
|
||||
twilio = "*"
|
||||
ua-parser = "*"
|
||||
unidecode = "*"
|
||||
# Pinned because of botocore https://github.com/orgs/python-poetry/discussions/7937
|
||||
urllib3 = { extras = ["secure"], version = "<3" }
|
||||
uvicorn = { extras = ["standard"], version = "*" }
|
||||
watchdog = "*"
|
||||
webauthn = "*"
|
||||
wsproto = "*"
|
||||
xmlsec = "*"
|
||||
zxcvbn = "*"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
aws-cdk-lib = "*"
|
||||
bandit = "*"
|
||||
black = "*"
|
||||
bump2version = "*"
|
||||
channels = { version = "*", extras = ["daphne"] }
|
||||
codespell = "*"
|
||||
colorama = "*"
|
||||
constructs = "*"
|
||||
coverage = { extras = ["toml"], version = "*" }
|
||||
debugpy = "*"
|
||||
drf-jsonschema-serializer = "*"
|
||||
freezegun = "*"
|
||||
importlib-metadata = "*"
|
||||
k5test = "*"
|
||||
pdoc = "*"
|
||||
pytest = "*"
|
||||
pytest-django = "*"
|
||||
pytest-github-actions-annotate-failures = "*"
|
||||
pytest-randomly = "*"
|
||||
pytest-timeout = "*"
|
||||
requests-mock = "*"
|
||||
ruff = "*"
|
||||
selenium = "*"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
ak = "lifecycle.ak:main"
|
||||
|
82
schema.yml
82
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
|
||||
@ -22191,6 +22191,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 +25750,7 @@ paths:
|
||||
description: ''
|
||||
delete:
|
||||
operationId: sources_all_destroy
|
||||
description: Source Viewset
|
||||
description: Prevent deletion of built-in sources
|
||||
parameters:
|
||||
- in: path
|
||||
name: slug
|
||||
@ -35146,7 +35151,7 @@ paths:
|
||||
- in: query
|
||||
name: token_expiry
|
||||
schema:
|
||||
type: integer
|
||||
type: string
|
||||
- in: query
|
||||
name: use_global_settings
|
||||
schema:
|
||||
@ -41582,6 +41587,12 @@ components:
|
||||
- confidential
|
||||
- public
|
||||
type: string
|
||||
CompatibilityModeEnum:
|
||||
enum:
|
||||
- default
|
||||
- aws
|
||||
- slack
|
||||
type: string
|
||||
Config:
|
||||
type: object
|
||||
description: Serialize authentik Config into DRF Object
|
||||
@ -42774,10 +42785,8 @@ components:
|
||||
format: email
|
||||
maxLength: 254
|
||||
token_expiry:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
description: Time in minutes the token sent is valid.
|
||||
type: string
|
||||
description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
|
||||
subject:
|
||||
type: string
|
||||
template:
|
||||
@ -42833,10 +42842,9 @@ components:
|
||||
minLength: 1
|
||||
maxLength: 254
|
||||
token_expiry:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
description: Time in minutes the token sent is valid.
|
||||
type: string
|
||||
minLength: 1
|
||||
description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
|
||||
subject:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -50389,10 +50397,9 @@ components:
|
||||
minLength: 1
|
||||
maxLength: 254
|
||||
token_expiry:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
description: Time in minutes the token sent is valid.
|
||||
type: string
|
||||
minLength: 1
|
||||
description: 'Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).'
|
||||
subject:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -52226,6 +52233,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:
|
||||
@ -52445,6 +52460,11 @@ components:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Authentication token
|
||||
compatibility_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompatibilityModeEnum'
|
||||
title: SCIM Compatibility Mode
|
||||
description: Alter authentik behavior for vendor-specific SCIM implementations.
|
||||
exclude_users_service_account:
|
||||
type: boolean
|
||||
filter_group:
|
||||
@ -55176,6 +55196,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:
|
||||
@ -55341,6 +55369,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:
|
||||
@ -55845,6 +55881,11 @@ components:
|
||||
token:
|
||||
type: string
|
||||
description: Authentication token
|
||||
compatibility_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompatibilityModeEnum'
|
||||
title: SCIM Compatibility Mode
|
||||
description: Alter authentik behavior for vendor-specific SCIM implementations.
|
||||
exclude_users_service_account:
|
||||
type: boolean
|
||||
filter_group:
|
||||
@ -55885,7 +55926,10 @@ components:
|
||||
readOnly: true
|
||||
provider:
|
||||
type: integer
|
||||
attributes:
|
||||
readOnly: true
|
||||
required:
|
||||
- attributes
|
||||
- group
|
||||
- group_obj
|
||||
- id
|
||||
@ -55935,6 +55979,11 @@ components:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Authentication token
|
||||
compatibility_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompatibilityModeEnum'
|
||||
title: SCIM Compatibility Mode
|
||||
description: Alter authentik behavior for vendor-specific SCIM implementations.
|
||||
exclude_users_service_account:
|
||||
type: boolean
|
||||
filter_group:
|
||||
@ -55967,7 +56016,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
|
||||
|
@ -13,6 +13,7 @@ const importInlinePatterns = [
|
||||
'import AKGlobal from "@goauthentik/common/styles/authentik\\.css',
|
||||
'import PF.+ from "@patternfly/patternfly/\\S+\\.css',
|
||||
'import ThemeDark from "@goauthentik/common/styles/theme-dark\\.css',
|
||||
'import OneDark from "@goauthentik/common/styles/one-dark\\.css',
|
||||
'import styles from "\\./LibraryPageImpl\\.css',
|
||||
];
|
||||
|
||||
@ -39,6 +40,10 @@ const config: StorybookConfig = {
|
||||
from: "../src/common/styles/theme-dark.css",
|
||||
to: "@goauthentik/common/styles/theme-dark.css",
|
||||
},
|
||||
{
|
||||
from: "../src/common/styles/one-dark.css",
|
||||
to: "@goauthentik/common/styles/one-dark.css",
|
||||
},
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/web-components-vite",
|
||||
|
@ -71,7 +71,7 @@ export default [
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
files: ["scripts/*.mjs", "*.ts", "*.mjs"],
|
||||
files: ["scripts/**/*.mjs", "*.ts", "*.mjs"],
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
// We WANT our scripts to output to the console!
|
||||
|
4397
web/package-lock.json
generated
4397
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,12 +11,14 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2025.2.1-1740858273",
|
||||
"@goauthentik/api": "^2025.2.2-1742395408",
|
||||
"@lit-labs/ssr": "^3.2.2",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"@lit/task": "^1.0.1",
|
||||
"@mdx-js/esbuild": "^3.1.0",
|
||||
"@mdx-js/mdx": "^3.1.0",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@patternfly/elements": "^4.0.2",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
@ -36,9 +38,11 @@
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"lit": "^3.2.0",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mdx-mermaid": "^2.0.3",
|
||||
"mermaid": "^11.4.1",
|
||||
"rapidoc": "^9.3.7",
|
||||
"showdown": "^2.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"style-mod": "^4.1.2",
|
||||
"ts-pattern": "^5.4.0",
|
||||
"webcomponent-qr-code": "^1.2.0",
|
||||
@ -66,13 +70,13 @@
|
||||
"@types/guacamole-common-js": "^1.5.2",
|
||||
"@types/mocha": "^10.0.8",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@types/react": "^18.3.13",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.0",
|
||||
"@typescript-eslint/parser": "^8.8.0",
|
||||
"@wdio/browser-runner": "9.4",
|
||||
"@wdio/cli": "9.4",
|
||||
"@wdio/spec-reporter": "^9.1.2",
|
||||
"chokidar": "^4.0.1",
|
||||
"change-case": "^5.4.4",
|
||||
"chromedriver": "^131.0.1",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.11.1",
|
||||
@ -87,6 +91,13 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"pseudolocale": "^2.1.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-directive": "^4.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx-frontmatter": "^5.0.0",
|
||||
"rollup-plugin-modify": "^3.0.0",
|
||||
"rollup-plugin-postcss-lit": "^2.1.0",
|
||||
"storybook": "^8.3.4",
|
||||
@ -118,7 +129,7 @@
|
||||
"build-locales:build": "wireit",
|
||||
"build-proxy": "wireit",
|
||||
"build:sfe": "wireit",
|
||||
"esbuild:watch": "node build.mjs --watch",
|
||||
"esbuild:watch": "node scripts/build-web.mjs --watch",
|
||||
"extract-locales": "wireit",
|
||||
"format": "wireit",
|
||||
"lint": "wireit",
|
||||
@ -153,7 +164,7 @@
|
||||
"instead of `npm run watch`. The former is more comprehensive, but ",
|
||||
"the latter is faster."
|
||||
],
|
||||
"command": "${NODE_RUNNER} build.mjs",
|
||||
"command": "${NODE_RUNNER} scripts/build-web.mjs",
|
||||
"files": [
|
||||
"src/**/*.{css,jpg,png,ts,js,json}",
|
||||
"!src/**/*.stories.ts",
|
||||
@ -173,6 +184,7 @@
|
||||
"./dist/poly-*.js.map",
|
||||
"./dist/custom.css",
|
||||
"./dist/theme-dark.css",
|
||||
"./dist/one-dark.css",
|
||||
"./dist/patternfly.min.css"
|
||||
],
|
||||
"dependencies": [
|
||||
@ -195,7 +207,7 @@
|
||||
]
|
||||
},
|
||||
"build-proxy": {
|
||||
"command": "node build.mjs --proxy",
|
||||
"command": "node scripts/build-web.mjs --proxy",
|
||||
"dependencies": [
|
||||
"build-locales"
|
||||
]
|
||||
|
@ -3,15 +3,16 @@ import esbuild from "esbuild";
|
||||
import findFreePorts from "find-free-ports";
|
||||
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
|
||||
import { globSync } from "glob";
|
||||
import path from "path";
|
||||
import * as path from "path";
|
||||
import { cwd } from "process";
|
||||
import process from "process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import { buildObserverPlugin } from "./build-observer-plugin.mjs";
|
||||
import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
|
||||
import { buildObserverPlugin } from "./esbuild/build-observer-plugin.mjs";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
let authentikProjectRoot = __dirname + "../";
|
||||
let authentikProjectRoot = path.join(__dirname, "..", "..");
|
||||
|
||||
try {
|
||||
// Use the package.json file in the root folder, as it has the current version information.
|
||||
@ -122,11 +123,10 @@ const BASE_ESBUILD_OPTIONS = {
|
||||
loader: {
|
||||
".css": "text",
|
||||
".md": "text",
|
||||
".mdx": "text",
|
||||
},
|
||||
define: definitions,
|
||||
format: "esm",
|
||||
plugins: [],
|
||||
plugins: [mdxPlugin()],
|
||||
logOverride: {
|
||||
/**
|
||||
* HACK: Silences issue originating in ESBuild.
|
||||
@ -161,7 +161,7 @@ function composeVersionID() {
|
||||
* @throws {Error} on build failure
|
||||
*/
|
||||
function createEntryPointOptions([source, dest], overrides = {}) {
|
||||
const outdir = path.join(__dirname, "./dist", dest);
|
||||
const outdir = path.join(__dirname, "..", "dist", dest);
|
||||
|
||||
return {
|
||||
...BASE_ESBUILD_OPTIONS,
|
||||
@ -214,7 +214,7 @@ async function doWatch() {
|
||||
buildObserverPlugin({
|
||||
serverURL,
|
||||
logPrefix: entryPoint[1],
|
||||
relativeRoot: __dirname,
|
||||
relativeRoot: path.join(__dirname, ".."),
|
||||
}),
|
||||
],
|
||||
define: {
|
299
web/scripts/esbuild/build-mdx-plugin.mjs
Normal file
299
web/scripts/esbuild/build-mdx-plugin.mjs
Normal file
@ -0,0 +1,299 @@
|
||||
/**
|
||||
* @import {Options as HighlightOptions} from 'rehype-highlight'
|
||||
* @import {CompileOptions} from '@mdx-js/mdx'
|
||||
* @import {mdxmermaid} from 'mdx-mermaid'
|
||||
* @import {Message,
|
||||
OnLoadArgs,
|
||||
OnLoadResult,
|
||||
Plugin,
|
||||
PluginBuild
|
||||
* } from 'esbuild'
|
||||
*/
|
||||
import { run as runMDX } from "@mdx-js/mdx";
|
||||
import { createFormatAwareProcessors } from "@mdx-js/mdx/internal-create-format-aware-processors";
|
||||
import { extnamesToRegex } from "@mdx-js/mdx/internal-extnames-to-regex";
|
||||
import apacheGrammar from "highlight.js/lib/languages/apache";
|
||||
import diffGrammar from "highlight.js/lib/languages/diff";
|
||||
import confGrammar from "highlight.js/lib/languages/ini";
|
||||
import nginxGrammar from "highlight.js/lib/languages/nginx";
|
||||
import { common } from "lowlight";
|
||||
import mdxMermaid from "mdx-mermaid";
|
||||
import { Mermaid } from "mdx-mermaid/lib/Mermaid";
|
||||
import assert from "node:assert";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import * as runtime from "react/jsx-runtime";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import remarkDirective from "remark-directive";
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkGFM from "remark-gfm";
|
||||
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
|
||||
import remarkParse from "remark-parse";
|
||||
import { SourceMapGenerator } from "source-map";
|
||||
import { VFile } from "vfile";
|
||||
import { VFileMessage } from "vfile-message";
|
||||
|
||||
import { remarkAdmonition } from "./remark/remark-admonition.mjs";
|
||||
import { remarkHeadings } from "./remark/remark-headings.mjs";
|
||||
import { remarkLinks } from "./remark/remark-links.mjs";
|
||||
import { remarkLists } from "./remark/remark-lists.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData
|
||||
* Data passed to `onload`.
|
||||
*
|
||||
* @typedef LoadDataFields
|
||||
* Extra fields given in `data` to `onload`.
|
||||
* @property {PluginData | null | undefined} [pluginData]
|
||||
* Plugin data.
|
||||
*
|
||||
* @typedef {CompileOptions} Options
|
||||
* Configuration.
|
||||
*
|
||||
* Options are the same as `compile` from `@mdx-js/mdx`.
|
||||
*
|
||||
* @typedef PluginData
|
||||
* Extra data passed.
|
||||
* @property {Buffer | string | null | undefined} [contents]
|
||||
* File contents.
|
||||
*
|
||||
* @typedef State
|
||||
* Info passed around.
|
||||
* @property {string} doc
|
||||
* File value.
|
||||
* @property {string} name
|
||||
* Plugin name.
|
||||
* @property {string} path
|
||||
* File path.
|
||||
*/
|
||||
|
||||
const eol = /\r\n|\r|\n|\u2028|\u2029/g;
|
||||
|
||||
const name = "@mdx-js/esbuild";
|
||||
|
||||
/**
|
||||
* Compile MDX to HTML.
|
||||
* *
|
||||
* @param {Readonly<Options> | null | undefined} [mdxOptions]
|
||||
* Configuration (optional).
|
||||
* @return {Plugin}
|
||||
* Plugin.
|
||||
*/
|
||||
export function mdxPlugin(mdxOptions) {
|
||||
/** @type {mdxmermaid.Config} */
|
||||
const mermaidConfig = {
|
||||
output: "svg",
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {HighlightOptions}
|
||||
*/
|
||||
const highlightThemeOptions = {
|
||||
languages: {
|
||||
...common,
|
||||
nginx: nginxGrammar,
|
||||
apache: apacheGrammar,
|
||||
conf: confGrammar,
|
||||
diff: diffGrammar,
|
||||
},
|
||||
};
|
||||
|
||||
const { extnames, process } = createFormatAwareProcessors({
|
||||
...mdxOptions,
|
||||
SourceMapGenerator,
|
||||
outputFormat: "function-body",
|
||||
|
||||
remarkPlugins: [
|
||||
remarkParse,
|
||||
remarkDirective,
|
||||
remarkAdmonition,
|
||||
remarkGFM,
|
||||
remarkFrontmatter,
|
||||
remarkMdxFrontmatter,
|
||||
remarkHeadings,
|
||||
remarkLinks,
|
||||
remarkLists,
|
||||
[mdxMermaid, mermaidConfig],
|
||||
],
|
||||
rehypePlugins: [[rehypeHighlight, highlightThemeOptions]],
|
||||
});
|
||||
|
||||
return { name, setup };
|
||||
|
||||
/**
|
||||
* @param {PluginBuild} build
|
||||
* Build.
|
||||
* @returns {undefined}
|
||||
* Nothing.
|
||||
*/
|
||||
function setup(build) {
|
||||
build.onLoad({ filter: extnamesToRegex(extnames) }, onload);
|
||||
|
||||
/**
|
||||
* @param {LoadData} data
|
||||
* Data.
|
||||
* @returns {Promise<OnLoadResult>}
|
||||
* Result.
|
||||
*/
|
||||
async function onload(data) {
|
||||
const document = String(
|
||||
data.pluginData &&
|
||||
data.pluginData.contents !== null &&
|
||||
data.pluginData.contents !== undefined
|
||||
? data.pluginData.contents
|
||||
: await fs.readFile(data.path),
|
||||
);
|
||||
|
||||
/** @type {State} */
|
||||
const state = {
|
||||
doc: document,
|
||||
name,
|
||||
path: data.path,
|
||||
};
|
||||
|
||||
let file = new VFile({
|
||||
path: data.path,
|
||||
value: document,
|
||||
});
|
||||
|
||||
/** @type {string | undefined} */
|
||||
let value;
|
||||
|
||||
/** @type {Array<VFileMessage>} */
|
||||
let messages = [];
|
||||
|
||||
/** @type {Array<Message>} */
|
||||
const errors = [];
|
||||
|
||||
/** @type {Array<Message>} */
|
||||
const warnings = [];
|
||||
|
||||
/**
|
||||
* @type {React.ComponentType<{children: React.ReactNode, frontmatter: Record<string, string>}>}
|
||||
*/
|
||||
const wrapper = ({ children, frontmatter }) => {
|
||||
const title = frontmatter.title;
|
||||
const nextChildren = React.Children.toArray(children);
|
||||
|
||||
if (title) {
|
||||
nextChildren.unshift(React.createElement("h1", { key: "title" }, title));
|
||||
}
|
||||
|
||||
return React.createElement(React.Fragment, null, nextChildren);
|
||||
};
|
||||
|
||||
try {
|
||||
file = await process(file);
|
||||
const { default: Content, ...mdxExports } = await runMDX(file, {
|
||||
...runtime,
|
||||
useMDXComponents: () => {
|
||||
return {
|
||||
mermaid: Mermaid,
|
||||
Mermaid,
|
||||
};
|
||||
},
|
||||
baseUrl: import.meta.url,
|
||||
});
|
||||
|
||||
const { frontmatter = {} } = mdxExports;
|
||||
const result = renderToStaticMarkup(
|
||||
Content({
|
||||
frontmatter,
|
||||
components: {
|
||||
wrapper,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
value = result;
|
||||
|
||||
messages = file.messages;
|
||||
} catch (error_) {
|
||||
const cause = /** @type {VFileMessage | Error} */ (error_);
|
||||
|
||||
console.error(cause);
|
||||
|
||||
const message =
|
||||
"reason" in cause
|
||||
? cause
|
||||
: new VFileMessage("Cannot process MDX file with esbuild", {
|
||||
cause,
|
||||
ruleId: "process-error",
|
||||
source: "@mdx-js/esbuild",
|
||||
});
|
||||
|
||||
message.fatal = true;
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
const list = message.fatal ? errors : warnings;
|
||||
list.push(vfileMessageToEsbuild(state, message));
|
||||
}
|
||||
|
||||
// Safety check: the file has a path, so there has to be a `dirname`.
|
||||
assert(file.dirname, "expected `dirname` to be defined");
|
||||
|
||||
return {
|
||||
contents: value || "",
|
||||
loader: "text",
|
||||
errors,
|
||||
resolveDir: path.resolve(file.cwd, file.dirname),
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<State>} state
|
||||
* Info passed around.
|
||||
* @param {Readonly<VFileMessage>} message
|
||||
* VFile message or error.
|
||||
* @returns {Message}
|
||||
* ESBuild message.
|
||||
*/
|
||||
function vfileMessageToEsbuild(state, message) {
|
||||
const place = message.place;
|
||||
const start = place ? ("start" in place ? place.start : place) : undefined;
|
||||
const end = place && "end" in place ? place.end : undefined;
|
||||
let length = 0;
|
||||
let lineStart = 0;
|
||||
let line = 0;
|
||||
let column = 0;
|
||||
|
||||
if (start && start.offset !== undefined) {
|
||||
line = start.line;
|
||||
column = start.column - 1;
|
||||
lineStart = start.offset - column;
|
||||
length = 1;
|
||||
|
||||
if (end && end.offset !== undefined) {
|
||||
length = end.offset - start.offset;
|
||||
}
|
||||
}
|
||||
|
||||
eol.lastIndex = lineStart;
|
||||
|
||||
const match = eol.exec(state.doc);
|
||||
const lineEnd = match ? match.index : state.doc.length;
|
||||
|
||||
return {
|
||||
detail: message,
|
||||
id: "",
|
||||
location: {
|
||||
column,
|
||||
file: state.path,
|
||||
length: Math.min(length, lineEnd),
|
||||
line,
|
||||
lineText: state.doc.slice(lineStart, lineEnd),
|
||||
namespace: "file",
|
||||
suggestion: "",
|
||||
},
|
||||
notes: [],
|
||||
pluginName: state.name,
|
||||
text: message.reason,
|
||||
};
|
||||
}
|
@ -8,7 +8,7 @@ import path from "path";
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serializeCustomEventToStream(event) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - TS doesn't know about the detail property
|
||||
const data = event.detail ?? {};
|
||||
|
||||
const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`];
|
46
web/scripts/esbuild/remark/remark-admonition.mjs
Normal file
46
web/scripts/esbuild/remark/remark-admonition.mjs
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @import {Plugin} from 'unified'
|
||||
* @import {Directives} from 'mdast-util-directive'
|
||||
* @import {} from 'mdast-util-to-hast'
|
||||
* @import {Root} from 'mdast'
|
||||
* @import {VFile} from 'vfile'
|
||||
*/
|
||||
import { h } from "hastscript";
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
const ADMONITION_TYPES = new Set(["info", "warning", "danger", "note"]);
|
||||
|
||||
/**
|
||||
* Remark plugin to process links
|
||||
* @type {Plugin<[unknown], Root, VFile>}
|
||||
*/
|
||||
export function remarkAdmonition() {
|
||||
return function transformer(tree) {
|
||||
/**
|
||||
* @param {Directives} node
|
||||
*/
|
||||
const visitor = (node) => {
|
||||
if (
|
||||
node.type === "containerDirective" ||
|
||||
node.type === "leafDirective" ||
|
||||
node.type === "textDirective"
|
||||
) {
|
||||
if (!ADMONITION_TYPES.has(node.name)) return;
|
||||
|
||||
const data = node.data || (node.data = {});
|
||||
|
||||
const tagName = node.type === "textDirective" ? "span" : "ak-alert";
|
||||
|
||||
data.hName = tagName;
|
||||
|
||||
const element = h(tagName, node.attributes || {});
|
||||
|
||||
data.hProperties = element.properties || {};
|
||||
data.hProperties.level = `pf-m-${node.name}`;
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore - visit cannot infer the type of the visitor.
|
||||
visit(tree, visitor);
|
||||
};
|
||||
}
|
33
web/scripts/esbuild/remark/remark-headings.mjs
Normal file
33
web/scripts/esbuild/remark/remark-headings.mjs
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @import {Plugin} from 'unified'
|
||||
* @import {Root, Heading} from 'mdast'
|
||||
* @import {VFile} from 'vfile'
|
||||
*/
|
||||
import { kebabCase } from "change-case";
|
||||
import { toString } from "mdast-util-to-string";
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
/**
|
||||
* Remark plugin to process links
|
||||
* @type {Plugin<[unknown], Root, VFile>}
|
||||
*/
|
||||
export const remarkHeadings = () => {
|
||||
return function transformer(tree) {
|
||||
/**
|
||||
* @param {Heading} node
|
||||
*/
|
||||
const visitor = (node) => {
|
||||
const textContent = toString(node);
|
||||
const id = kebabCase(textContent);
|
||||
|
||||
node.data = node.data || {};
|
||||
node.data.hProperties = {
|
||||
...node.data.hProperties,
|
||||
id,
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-ignore - visit cannot infer the type of the visitor.
|
||||
visit(tree, "heading", visitor);
|
||||
};
|
||||
};
|
59
web/scripts/esbuild/remark/remark-links.mjs
Normal file
59
web/scripts/esbuild/remark/remark-links.mjs
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @import {Plugin} from 'unified'
|
||||
* @import {} from 'mdast-util-directive'
|
||||
* @import {} from 'mdast-util-to-hast'
|
||||
* @import {Root, Link} from 'mdast'
|
||||
* @import {VFile} from 'vfile'
|
||||
*/
|
||||
import * as path from "node:path";
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
const DOCS_DOMAIN = "https://goauthentik.io";
|
||||
|
||||
/**
|
||||
* Remark plugin to process links
|
||||
* @type {Plugin<[unknown], Root, VFile>}
|
||||
*/
|
||||
export const remarkLinks = () => {
|
||||
return function transformer(tree, file) {
|
||||
const docsRoot = path.resolve(file.cwd, "..", "website");
|
||||
|
||||
/**
|
||||
* @param {Link} node
|
||||
*/
|
||||
const visitor = (node) => {
|
||||
node.data = node.data || {};
|
||||
|
||||
if (node.url.startsWith("#")) {
|
||||
node.data.hProperties = {
|
||||
className: "markdown-heading",
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
node.data.hProperties = {
|
||||
...node.data.hProperties,
|
||||
rel: "noopener noreferrer",
|
||||
target: "_blank",
|
||||
};
|
||||
|
||||
if (node.url.startsWith(".") && file.dirname) {
|
||||
const nextPathname = path.resolve(
|
||||
"/",
|
||||
path.relative(docsRoot, file.dirname),
|
||||
node.url,
|
||||
);
|
||||
const nextURL = new URL(nextPathname, DOCS_DOMAIN);
|
||||
|
||||
// Remove trailing .md and .mdx, and trailing "index".
|
||||
nextURL.pathname = nextURL.pathname.replace(/(index)?\.mdx?$/, "");
|
||||
|
||||
node.data.hProperties.href = nextURL.toString();
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore - visit cannot infer the type of the visitor.
|
||||
visit(tree, "link", visitor);
|
||||
};
|
||||
};
|
29
web/scripts/esbuild/remark/remark-lists.mjs
Normal file
29
web/scripts/esbuild/remark/remark-lists.mjs
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @import {Plugin} from 'unified'
|
||||
* @import {Root, List} from 'mdast'
|
||||
* @import {VFile} from 'vfile'
|
||||
*/
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
/**
|
||||
* Remark plugin to process links
|
||||
* @type {Plugin<[unknown], Root, VFile>}
|
||||
*/
|
||||
export const remarkLists = () => {
|
||||
return function transformer(tree) {
|
||||
/**
|
||||
* @param {List} node
|
||||
*/
|
||||
const visitor = (node) => {
|
||||
node.data = node.data || {};
|
||||
|
||||
node.data.hProperties = {
|
||||
...node.data.hProperties,
|
||||
className: "pf-c-list",
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-ignore - visit cannot infer the type of the visitor.
|
||||
visit(tree, "list", visitor);
|
||||
};
|
||||
};
|
@ -3,8 +3,8 @@ import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { until } from "lit/directives/until.js";
|
||||
import { PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
|
||||
import { ResponseError } from "@goauthentik/api";
|
||||
|
||||
@ -13,46 +13,143 @@ export interface AdminStatus {
|
||||
message?: TemplateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for admin status cards with robust state management
|
||||
*
|
||||
* @template T - Type of the primary data value used in the card
|
||||
*/
|
||||
export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
abstract getPrimaryValue(): Promise<T>;
|
||||
|
||||
abstract getStatus(value: T): Promise<AdminStatus>;
|
||||
|
||||
// Current data value state
|
||||
@state()
|
||||
value?: T;
|
||||
|
||||
// Current status state derived from value
|
||||
@state()
|
||||
protected status?: AdminStatus;
|
||||
|
||||
// Current error state if any request fails
|
||||
@state()
|
||||
protected error?: string;
|
||||
|
||||
// Abstract methods to be implemented by subclasses
|
||||
abstract getPrimaryValue(): Promise<T>;
|
||||
abstract getStatus(value: T): Promise<AdminStatus>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(EVENT_REFRESH, () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
// Proper binding for event handler
|
||||
this.fetchData = this.fetchData.bind(this);
|
||||
// Register refresh event listener
|
||||
this.addEventListener(EVENT_REFRESH, this.fetchData);
|
||||
}
|
||||
|
||||
renderValue(): TemplateResult {
|
||||
// Lifecycle method: Called when component is added to DOM
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// Initial data fetch
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch primary data and handle errors
|
||||
*/
|
||||
private fetchData() {
|
||||
this.getPrimaryValue()
|
||||
.then((value: T) => {
|
||||
this.value = value; // Triggers shouldUpdate
|
||||
this.error = undefined;
|
||||
})
|
||||
.catch((err: ResponseError) => {
|
||||
this.status = undefined;
|
||||
this.error = err?.response?.statusText ?? msg("Unknown error");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit lifecycle method: Determine if component should update
|
||||
*
|
||||
* @param changed - Map of changed properties
|
||||
* @returns boolean indicating if update should proceed
|
||||
*/
|
||||
shouldUpdate(changed: PropertyValues<this>) {
|
||||
if (changed.has("value") && this.value !== undefined) {
|
||||
// When value changes, fetch new status
|
||||
this.getStatus(this.value)
|
||||
.then((status) => {
|
||||
this.status = status;
|
||||
this.error = undefined;
|
||||
})
|
||||
.catch((err: ResponseError) => {
|
||||
this.status = undefined;
|
||||
this.error = err?.response?.statusText ?? msg("Unknown error");
|
||||
});
|
||||
|
||||
// Prevent immediate re-render if only value changed
|
||||
if (changed.size === 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the primary value display
|
||||
*
|
||||
* @returns TemplateResult displaying the value
|
||||
*/
|
||||
protected renderValue(): TemplateResult {
|
||||
return html`${this.value}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render status state
|
||||
*
|
||||
* @param status - AdminStatus object containing icon and message
|
||||
* @returns TemplateResult for status display
|
||||
*/
|
||||
private renderStatus(status: AdminStatus): TemplateResult {
|
||||
return html`
|
||||
<p><i class="${status.icon}"></i> ${this.renderValue()}</p>
|
||||
${status.message ? html`<p class="subtext">${status.message}</p>` : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render error state
|
||||
*
|
||||
* @param error - Error message to display
|
||||
* @returns TemplateResult for error display
|
||||
*/
|
||||
private renderError(error: string): TemplateResult {
|
||||
return html`
|
||||
<p><i class="fa fa-times"></i> ${error}</p>
|
||||
<p class="subtext">${msg("Failed to fetch")}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render loading state
|
||||
*
|
||||
* @returns TemplateResult for loading spinner
|
||||
*/
|
||||
private renderLoading(): TemplateResult {
|
||||
return html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main render method that selects appropriate state display
|
||||
*
|
||||
* @returns TemplateResult for current component state
|
||||
*/
|
||||
renderInner(): TemplateResult {
|
||||
return html`<p class="center-value">
|
||||
${until(
|
||||
this.getPrimaryValue()
|
||||
.then((v) => {
|
||||
this.value = v;
|
||||
return this.getStatus(v);
|
||||
})
|
||||
.then((status) => {
|
||||
return html`<p><i class="${status.icon}"></i> ${this.renderValue()}</p>
|
||||
${status.message
|
||||
? html`<p class="subtext">${status.message}</p>`
|
||||
: html``}`;
|
||||
})
|
||||
.catch((exc: ResponseError) => {
|
||||
return html` <p>
|
||||
<i class="fa fa-times"></i> ${exc.response.statusText}
|
||||
</p>
|
||||
<p class="subtext">${msg("Failed to fetch")}</p>`;
|
||||
}),
|
||||
html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`,
|
||||
)}
|
||||
</p>`;
|
||||
return html`
|
||||
<p class="center-value">
|
||||
${
|
||||
this.status
|
||||
? this.renderStatus(this.status) // Status available
|
||||
: this.error
|
||||
? this.renderError(this.error) // Error state
|
||||
: this.renderLoading() // Loading state
|
||||
}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown .md=${MDApplication} meta="applications/index.md"></ak-markdown>
|
||||
<ak-markdown .content=${MDApplication}></ak-markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@ -58,7 +58,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
|
||||
get bindingsAsColumns() {
|
||||
return this.wizard.bindings.map((binding, index) => {
|
||||
const { order, enabled, timeout } = binding;
|
||||
const isSet = P.string.minLength(1);
|
||||
const isSet = P.union(P.string.minLength(1), P.number);
|
||||
const policy = match(binding)
|
||||
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
|
||||
.with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`))
|
||||
|
@ -221,7 +221,7 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<div class="pf-c-description-list__text pf-m-monospace">
|
||||
${this.provider.clientId}
|
||||
</div>
|
||||
</dd>
|
||||
@ -236,7 +236,9 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
<div class="pf-c-description-list__text">
|
||||
<ul>
|
||||
${this.provider.redirectUris.map((ru) => {
|
||||
return html`<li>${ru.matchingMode}: ${ru.url}</li>`;
|
||||
return html`<li class="pf-m-monospace">
|
||||
${ru.matchingMode}: ${ru.url}
|
||||
</li>`;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
@ -356,6 +358,7 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown
|
||||
.content=${MDProviderOAuth2}
|
||||
.replacers=${[
|
||||
(input: string) => {
|
||||
if (!this.provider) {
|
||||
@ -367,8 +370,6 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
);
|
||||
},
|
||||
]}
|
||||
.md=${MDProviderOAuth2}
|
||||
meta="providers/oauth2/index.md"
|
||||
></ak-markdown>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -196,8 +196,8 @@ export class ProxyProviderViewPage extends AKElement {
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile ak-markdown-section"
|
||||
>
|
||||
<ak-markdown
|
||||
.content=${server.md}
|
||||
.replacers=${replacers}
|
||||
.md=${server.md}
|
||||
meta=${server.meta}
|
||||
></ak-markdown>
|
||||
</section>`;
|
||||
@ -266,7 +266,7 @@ export class ProxyProviderViewPage extends AKElement {
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown
|
||||
.md=${MDHeaderAuthentication}
|
||||
.content=${MDHeaderAuthentication}
|
||||
meta="proxy/header_authentication.md"
|
||||
></ak-markdown>
|
||||
</div>
|
||||
|
@ -245,6 +245,41 @@ export function renderForm(
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("AuthnContextClassRef Property Mapping")}
|
||||
name="authnContextClassRefMapping"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<SAMLPropertyMapping[]> => {
|
||||
const args: PropertymappingsProviderSamlListRequest = {
|
||||
ordering: "saml_name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const items = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderSamlList(args);
|
||||
return items.results;
|
||||
}}
|
||||
.renderElement=${(item: SAMLPropertyMapping): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: SAMLPropertyMapping | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: SAMLPropertyMapping): boolean => {
|
||||
return provider?.authnContextClassRefMapping === item.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-text-input
|
||||
name="assertionValidNotBefore"
|
||||
|
@ -11,6 +11,7 @@ import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CompatibilityModeEnum,
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
Group,
|
||||
@ -61,6 +62,35 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
|
||||
)}
|
||||
inputHint="code"
|
||||
></ak-text-input>
|
||||
<ak-radio-input
|
||||
name="compatibilityMode"
|
||||
label=${msg("Compatibility Mode")}
|
||||
.value=${provider?.compatibilityMode}
|
||||
required
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Default"),
|
||||
value: CompatibilityModeEnum.Default,
|
||||
default: true,
|
||||
description: html`${msg("Default behavior.")}`,
|
||||
},
|
||||
{
|
||||
label: msg("AWS"),
|
||||
value: CompatibilityModeEnum.Aws,
|
||||
description: html`${msg(
|
||||
"Altered behavior for usage with Amazon Web Services.",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Slack"),
|
||||
value: CompatibilityModeEnum.Slack,
|
||||
description: html`${msg("Altered behavior for usage with Slack.")}`,
|
||||
},
|
||||
]}
|
||||
help=${msg(
|
||||
"Alter authentik's behavior for vendor-specific SCIM implementations.",
|
||||
)}
|
||||
></ak-radio-input>
|
||||
<ak-form-element-horizontal name="dryRun">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
|
@ -24,6 +24,7 @@ export class SCIMProviderGroupList extends Table<SCIMProviderGroup> {
|
||||
return true;
|
||||
}
|
||||
|
||||
expandable = true;
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
|
||||
@ -81,6 +82,13 @@ export class SCIMProviderGroupList extends Table<SCIMProviderGroup> {
|
||||
html`${item.id}`,
|
||||
];
|
||||
}
|
||||
renderExpanded(item: SCIMProviderGroup): TemplateResult {
|
||||
return html`<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
|
||||
</div>
|
||||
</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -24,6 +24,7 @@ export class SCIMProviderUserList extends Table<SCIMProviderUser> {
|
||||
return true;
|
||||
}
|
||||
|
||||
expandable = true;
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
|
||||
@ -82,6 +83,13 @@ export class SCIMProviderUserList extends Table<SCIMProviderUser> {
|
||||
html`${item.id}`,
|
||||
];
|
||||
}
|
||||
renderExpanded(item: SCIMProviderUser): TemplateResult {
|
||||
return html`<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
|
||||
</div>
|
||||
</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -244,7 +244,7 @@ export class SCIMProviderViewPage extends AKElement {
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-5-col">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown
|
||||
.md=${MDSCIMProvider}
|
||||
.content=${MDSCIMProvider}
|
||||
meta="providers/scim/index.md"
|
||||
></ak-markdown>
|
||||
</div>
|
||||
|
@ -62,7 +62,11 @@ export class SSFProviderFormPage extends BaseProviderForm<SSFProvider> {
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Signing Key")}
|
||||
name="signingKey"
|
||||
required
|
||||
>
|
||||
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.signingKey ?? undefined)}
|
||||
|
@ -137,10 +137,13 @@ export class SSFProviderViewPage extends AKElement {
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<input
|
||||
class="pf-c-form-control"
|
||||
class="pf-c-form-control pf-m-monospace"
|
||||
readonly
|
||||
type="text"
|
||||
value=${this.provider.ssfUrl || ""}
|
||||
placeholder=${this.provider.ssfUrl
|
||||
? msg("SSF URL")
|
||||
: msg("No assigned application")}
|
||||
/>
|
||||
</div>
|
||||
</dd>
|
||||
|
@ -57,10 +57,13 @@ export class SourceListPage extends TablePage<Source> {
|
||||
}
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
const disabled =
|
||||
this.selectedElements.length < 1 ||
|
||||
this.selectedElements.some((item) => item.component === "");
|
||||
const nonBuiltInSources = this.selectedElements.filter((item) => item.component !== "");
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Source(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.objects=${nonBuiltInSources}
|
||||
.usedBy=${(item: Source) => {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesAllUsedByList({
|
||||
slug: item.slug,
|
||||
|
@ -187,7 +187,7 @@ export class KerberosSourceViewPage extends AKElement {
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown
|
||||
.md=${MDSourceKerberosBrowser}
|
||||
.content=${MDSourceKerberosBrowser}
|
||||
meta="users-sources/protocols/kerberos/browser.md"
|
||||
;
|
||||
></ak-markdown>
|
||||
|
@ -3,6 +3,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
@ -202,19 +203,20 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Token expiry")}
|
||||
label=${msg("Token expiration")}
|
||||
?required=${true}
|
||||
name="tokenExpiry"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.tokenExpiry, 30)}"
|
||||
type="text"
|
||||
value="${first(this.instance?.tokenExpiry, "minutes=30")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Time in minutes the token sent is valid.")}
|
||||
${msg("Time the token sent is valid.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Subject")}
|
||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2025.2.1";
|
||||
export const VERSION = "2025.2.2";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* #region Global */
|
||||
|
||||
:root {
|
||||
--ak-accent: #fd4b2d;
|
||||
|
||||
@ -45,7 +47,44 @@ html > form > input {
|
||||
left: -2000px;
|
||||
}
|
||||
|
||||
/*#region Icons*/
|
||||
/* #endregion */
|
||||
|
||||
/* #region Anchors */
|
||||
|
||||
a {
|
||||
--pf-global--link--Color: var(--pf-global--link--Color--light);
|
||||
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover);
|
||||
--pf-global--link--Color--visited: var(--pf-global--link--Color);
|
||||
}
|
||||
|
||||
/*
|
||||
Note that order of anchor pseudo-selectors must follow:
|
||||
|
||||
1. link
|
||||
2. visited
|
||||
3. hover
|
||||
4. active
|
||||
*/
|
||||
|
||||
a:link {
|
||||
color: var(--pf-global--link--Color);
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: var(--pf-global--link--Color--visited);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--pf-global--link--Color--hover);
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: var(--pf-global--link--Color);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Icons */
|
||||
|
||||
.pf-icon {
|
||||
display: inline-block;
|
||||
@ -66,7 +105,7 @@ html > form > input {
|
||||
);
|
||||
}
|
||||
|
||||
/*#endregion*/
|
||||
/* #endregion */
|
||||
|
||||
.pf-c-page__header {
|
||||
z-index: 0;
|
||||
@ -74,15 +113,15 @@ html > form > input {
|
||||
box-shadow: var(--pf-global--BoxShadow--lg-bottom);
|
||||
}
|
||||
|
||||
/*****************************
|
||||
* Login adjustments
|
||||
*****************************/
|
||||
/* #region Login adjustments */
|
||||
|
||||
/* Ensure card is displayed on small screens */
|
||||
.pf-c-login__main {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ak-login-container {
|
||||
height: calc(100vh - var(--pf-global--spacer--lg) - var(--pf-global--spacer--lg));
|
||||
width: 35rem;
|
||||
@ -90,30 +129,34 @@ html > form > input {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pf-c-login__header {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.pf-c-login__footer {
|
||||
flex-grow: 2;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pf-c-login__footer ul.pf-c-list.pf-m-inline {
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
/*****************************
|
||||
* End Login adjustments
|
||||
*****************************/
|
||||
|
||||
/* #endregion */
|
||||
|
||||
.pf-c-content h1 {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.pf-c-content h1 i {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.pf-c-content h1 :first-child {
|
||||
margin-right: var(--pf-global--spacer--sm);
|
||||
}
|
||||
@ -127,9 +170,11 @@ html > form > input {
|
||||
.pf-m-success {
|
||||
color: var(--pf-global--success-color--100) !important;
|
||||
}
|
||||
|
||||
.pf-m-warning {
|
||||
color: var(--pf-global--warning-color--100);
|
||||
}
|
||||
|
||||
.pf-m-danger {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
@ -167,6 +212,7 @@ html > form > input {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ak-brand img {
|
||||
padding: 0 2rem;
|
||||
max-height: inherit;
|
||||
@ -181,3 +227,48 @@ html > form > input {
|
||||
.pf-c-data-list {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
/* #region Mermaid */
|
||||
|
||||
svg[id^="mermaid-svg-"] {
|
||||
.rect {
|
||||
fill: var(
|
||||
--ak-mermaid-box-background-color,
|
||||
var(--pf-global--BackgroundColor--light-300)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.messageText {
|
||||
stroke-width: 4;
|
||||
fill: var(--ak-mermaid-message-text) !important;
|
||||
paint-order: stroke;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Tables */
|
||||
|
||||
table thead,
|
||||
table tr:nth-child(2n) {
|
||||
background-color: var(
|
||||
--ak-table-stripe-background,
|
||||
var(--pf-global--BackgroundColor--light-200)
|
||||
);
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
border: var(--pf-table-border-width) solid var(--ifm-table-border-color);
|
||||
padding: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Code blocks */
|
||||
|
||||
pre:has(.hljs) {
|
||||
padding: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
98
web/src/common/styles/one-dark.css
Normal file
98
web/src/common/styles/one-dark.css
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
|
||||
Atom One Dark by Daniel Gamage
|
||||
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
|
||||
|
||||
base: #282c34
|
||||
mono-1: #abb2bf
|
||||
mono-2: #818896
|
||||
mono-3: #5c6370
|
||||
hue-1: #56b6c2
|
||||
hue-2: #61aeee
|
||||
hue-3: #c678dd
|
||||
hue-4: #98c379
|
||||
hue-5: #e06c75
|
||||
hue-5-2: #be5046
|
||||
hue-6: #d19a66
|
||||
hue-6-2: #e6c07b
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
color: #abb2bf;
|
||||
background: #282c34;
|
||||
}
|
||||
|
||||
pre:has(.hljs) {
|
||||
background: #282c34;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #5c6370;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-formula {
|
||||
color: #c678dd;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name,
|
||||
.hljs-selector-tag,
|
||||
.hljs-deletion,
|
||||
.hljs-subst {
|
||||
color: #e06c75;
|
||||
}
|
||||
|
||||
.hljs-literal {
|
||||
color: #56b6c2;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-addition,
|
||||
.hljs-attribute,
|
||||
.hljs-meta .hljs-string {
|
||||
color: #98c379;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-number {
|
||||
color: #d19a66;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-link,
|
||||
.hljs-meta,
|
||||
.hljs-selector-id,
|
||||
.hljs-title {
|
||||
color: #61aeee;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-title.class_,
|
||||
.hljs-class .hljs-title {
|
||||
color: #e6c07b;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
@ -1,59 +1,84 @@
|
||||
/* #region Global */
|
||||
|
||||
:root {
|
||||
--pf-global--Color--100: var(--ak-dark-foreground) !important;
|
||||
--ak-global--Color--100: var(--ak-dark-foreground) !important;
|
||||
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
|
||||
--pf-global--link--Color: var(--ak-dark-foreground-link) !important;
|
||||
--pf-global--BorderColor--100: var(--ak-dark-background-lighter) !important;
|
||||
--ak-mermaid-message-text: var(--ak-dark-foreground) !important;
|
||||
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter) !important;
|
||||
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--ak-dark-background) !important;
|
||||
}
|
||||
|
||||
.pf-c-radio {
|
||||
--pf-c-radio__label--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
/* Global page background colour */
|
||||
.pf-c-page {
|
||||
--pf-c-page--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
.pf-c-drawer__content {
|
||||
--pf-c-drawer__content--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
.pf-c-title {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-u-mb-xl {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* Header sections */
|
||||
|
||||
.pf-c-page__main-section {
|
||||
--pf-c-page__main-section--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
.sidebar-trigger,
|
||||
.notification-trigger {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.pf-c-content {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
/* Card */
|
||||
|
||||
/* #region Card */
|
||||
|
||||
.pf-c-card {
|
||||
--pf-c-card--BackgroundColor: var(--ak-dark-background-light);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-card.pf-m-non-selectable-raised {
|
||||
--pf-c-card--BackgroundColor: var(--ak-dark-background-lighter);
|
||||
}
|
||||
|
||||
.pf-c-card__title,
|
||||
.pf-c-card__body {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
.pf-c-toolbar {
|
||||
--pf-c-toolbar--BackgroundColor: var(--ak-dark-background-light);
|
||||
}
|
||||
|
||||
.pf-c-pagination.pf-m-bottom {
|
||||
background-color: var(--ak-dark-background-light);
|
||||
}
|
||||
/* table */
|
||||
|
||||
/* #region Tables */
|
||||
.pf-c-table {
|
||||
--pf-c-table--BackgroundColor: var(--ak-dark-background-light);
|
||||
--pf-c-table--BorderColor: var(--ak-dark-background-lighter);
|
||||
@ -61,41 +86,58 @@ body {
|
||||
--pf-c-table--tr--m-hoverable--hover--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
--pf-c-table--tr--m-hoverable--active--BackgroundColor: var(--ak-dark-background-lighter);
|
||||
}
|
||||
|
||||
.pf-c-table__text {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-table__sort:not(.pf-m-selected) .pf-c-table__button .pf-c-table__text {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
|
||||
.pf-c-table__sort-indicator i {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
|
||||
.pf-c-table__expandable-row.pf-m-expanded {
|
||||
--pf-c-table__expandable-row--m-expanded--BorderBottomColor: var(--ak-dark-background-lighter);
|
||||
}
|
||||
/* tabs */
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Tabs */
|
||||
|
||||
.pf-c-tabs {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.pf-c-tabs.pf-m-box.pf-m-vertical .pf-c-tabs__list::before {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pf-c-tabs.pf-m-box .pf-c-tabs__item.pf-m-current:first-child .pf-c-tabs__link::before {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pf-c-tabs__link::before {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pf-c-tabs__item.pf-m-current {
|
||||
--pf-c-tabs__link--after--BorderColor: var(--ak-accent);
|
||||
}
|
||||
|
||||
.pf-c-tabs__link {
|
||||
--pf-c-tabs__link--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-tabs.pf-m-vertical .pf-c-tabs__link {
|
||||
background-color: transparent;
|
||||
}
|
||||
/* table, on mobile */
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #Region Mobile Tables */
|
||||
@media screen and (max-width: 1200px) {
|
||||
.pf-m-grid-xl.pf-c-table tbody:first-of-type {
|
||||
border-top-color: var(--ak-dark-background);
|
||||
@ -104,32 +146,42 @@ body {
|
||||
border-bottom-color: var(--ak-dark-background);
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* class for pagination text */
|
||||
.pf-c-options-menu__toggle {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
/* table icon used for expanding rows */
|
||||
.pf-c-table__toggle-icon {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
/* expandable elements */
|
||||
.pf-c-expandable-section__toggle-text {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-expandable-section__toggle-icon {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-expandable-section.pf-m-display-lg {
|
||||
background-color: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
|
||||
/* header for form group */
|
||||
.pf-c-form__field-group-header-title-text {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-form__field-group {
|
||||
border-bottom: 0;
|
||||
}
|
||||
/* inputs */
|
||||
|
||||
/* #region Inputs */
|
||||
optgroup,
|
||||
option {
|
||||
color: var(--ak-dark-foreground);
|
||||
@ -138,9 +190,11 @@ select[multiple] optgroup:checked,
|
||||
select[multiple] option:checked {
|
||||
color: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
.pf-c-input-group {
|
||||
--pf-c-input-group--BackgroundColor: transparent;
|
||||
}
|
||||
|
||||
.pf-c-form-control {
|
||||
--pf-c-form-control--BorderTopColor: transparent !important;
|
||||
--pf-c-form-control--BorderRightColor: transparent !important;
|
||||
@ -149,12 +203,15 @@ select[multiple] option:checked {
|
||||
--pf-c-form-control--BackgroundColor: var(--ak-dark-background-light);
|
||||
--pf-c-form-control--Color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
|
||||
.pf-c-form-control:disabled {
|
||||
background-color: var(--ak-dark-background-light);
|
||||
}
|
||||
|
||||
.pf-c-form-control[readonly] {
|
||||
background-color: var(--ak-dark-background-light) !important;
|
||||
}
|
||||
|
||||
.pf-c-switch__input:checked ~ .pf-c-switch__label {
|
||||
--pf-c-switch__input--checked__label--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
@ -162,38 +219,47 @@ input[type="datetime-local"]::-webkit-calendar-picker-indicator,
|
||||
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
/* select toggle */
|
||||
.pf-c-select__toggle::before {
|
||||
--pf-c-select__toggle--before--BorderTopColor: var(--ak-dark-background-lighter);
|
||||
--pf-c-select__toggle--before--BorderRightColor: var(--ak-dark-background-lighter);
|
||||
--pf-c-select__toggle--before--BorderLeftColor: var(--ak-dark-background-lighter);
|
||||
}
|
||||
|
||||
.pf-c-select__toggle.pf-m-typeahead {
|
||||
--pf-c-select__toggle--BackgroundColor: var(--ak-dark-background-light);
|
||||
}
|
||||
|
||||
.pf-c-select__menu {
|
||||
--pf-c-select__menu--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-select__menu-item {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-select__menu-wrapper:hover,
|
||||
.pf-c-select__menu-item:hover {
|
||||
--pf-c-select__menu-item--hover--BackgroundColor: var(--ak-dark-background-lighter);
|
||||
}
|
||||
|
||||
.pf-c-select__menu-wrapper:focus-within,
|
||||
.pf-c-select__menu-wrapper.pf-m-focus,
|
||||
.pf-c-select__menu-item:focus,
|
||||
.pf-c-select__menu-item.pf-m-focus {
|
||||
--pf-c-select__menu-item--focus--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
|
||||
.pf-c-button:disabled {
|
||||
color: var(--ak-dark-background-lighter);
|
||||
}
|
||||
|
||||
.pf-c-button.pf-m-plain:hover {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-button.pf-m-control {
|
||||
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter)
|
||||
var(--ak-dark-background-lighter) var(--pf-c-button--m-control--after--BorderBottomColor)
|
||||
@ -201,92 +267,127 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
background-color: var(--ak-dark-background-light);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-m-tertiary,
|
||||
.pf-c-button.pf-m-tertiary {
|
||||
--pf-c-button--after--BorderColor: var(--ak-dark-foreground-darker);
|
||||
color: var(--ak-dark-foreground-darker);
|
||||
}
|
||||
|
||||
.pf-m-tertiary:hover,
|
||||
.pf-c-button.pf-m-tertiary:hover {
|
||||
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter);
|
||||
}
|
||||
|
||||
.pf-c-form__label-text {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-check__label {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-dropdown__toggle::before {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pf-c-dropdown__menu {
|
||||
--pf-c-dropdown__menu--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
.pf-c-dropdown__menu-item {
|
||||
--pf-c-dropdown__menu-item--BackgroundColor: var(--ak-dark-background);
|
||||
--pf-c-dropdown__menu-item--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-dropdown__menu-item:hover,
|
||||
.pf-c-dropdown__menu-item:focus {
|
||||
--pf-c-dropdown__menu-item--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
--pf-c-dropdown__menu-item--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-toggle-group__button {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
|
||||
.pf-c-toggle-group__button:not(.pf-m-selected) {
|
||||
background-color: var(--ak-dark-background-light) !important;
|
||||
}
|
||||
|
||||
.pf-c-toggle-group__button.pf-m-selected {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
background-color: var(--pf-global--primary-color--100) !important;
|
||||
}
|
||||
|
||||
/* inputs help text */
|
||||
.pf-c-form__helper-text:not(.pf-m-error) {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
/* modal */
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Modal */
|
||||
|
||||
.pf-c-modal-box,
|
||||
.pf-c-modal-box__header,
|
||||
.pf-c-modal-box__footer,
|
||||
.pf-c-modal-box__body {
|
||||
background-color: var(--ak-dark-background);
|
||||
}
|
||||
/* sidebar */
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Sidebar */
|
||||
|
||||
.pf-c-nav {
|
||||
background-color: var(--ak-dark-background-light);
|
||||
}
|
||||
/* flows */
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Flows */
|
||||
|
||||
.pf-c-login__main {
|
||||
--pf-c-login__main--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
.pf-c-login__main-body,
|
||||
.pf-c-login__main-header,
|
||||
.pf-c-login__main-header-desc {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-login__main-footer-links-item img,
|
||||
.pf-c-login__main-footer-links-item .fas {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.pf-c-login__main-footer-band {
|
||||
--pf-c-login__main-footer-band--BackgroundColor: var(--ak-dark-background-lighter);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.form-control-static {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
/* notifications */
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Notifications */
|
||||
|
||||
.pf-c-drawer__panel {
|
||||
background-color: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
.pf-c-notification-drawer {
|
||||
--pf-c-notification-drawer--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
.pf-c-notification-drawer__header {
|
||||
background-color: var(--ak-dark-background-lighter);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-notification-drawer__list-item {
|
||||
background-color: var(--ak-dark-background-light-ish);
|
||||
color: var(--ak-dark-foreground);
|
||||
@ -294,46 +395,84 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
--ak-dark-background-lighter
|
||||
) !important;
|
||||
}
|
||||
/* data list */
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Data List */
|
||||
|
||||
.pf-c-data-list {
|
||||
padding-inline-start: 0;
|
||||
border-top-color: var(--ak-dark-background-lighter);
|
||||
}
|
||||
|
||||
.pf-c-data-list__item {
|
||||
--pf-c-data-list__item--BackgroundColor: transparent;
|
||||
--pf-c-data-list__item--BorderBottomColor: var(--ak-dark-background-lighter);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
/* wizards */
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Wizards */
|
||||
|
||||
.pf-c-wizard__nav {
|
||||
--pf-c-wizard__nav--BackgroundColor: var(--ak-dark-background-lighter);
|
||||
--pf-c-wizard__nav--lg--BorderRightColor: transparent;
|
||||
}
|
||||
|
||||
.pf-c-wizard__main {
|
||||
background-color: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
|
||||
.pf-c-wizard__footer {
|
||||
--pf-c-wizard__footer--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
|
||||
.pf-c-wizard__toggle-num,
|
||||
.pf-c-wizard__nav-link::before {
|
||||
--pf-c-wizard__nav-link--before--BackgroundColor: transparent;
|
||||
}
|
||||
/* tree view */
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Tree view */
|
||||
|
||||
.pf-c-tree-view__node {
|
||||
--pf-c-tree-view__node--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-tree-view__node-toggle {
|
||||
--pf-c-tree-view__node-toggle--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-tree-view__node:focus {
|
||||
--pf-c-tree-view__node--focus--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
|
||||
.pf-c-tree-view__content:hover,
|
||||
.pf-c-tree-view__content:focus-within {
|
||||
--pf-c-tree-view__node--hover--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
/* stepper */
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Stepper */
|
||||
.pf-c-progress-stepper__step-title {
|
||||
--pf-c-progress-stepper__step-title--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Mermaid */
|
||||
|
||||
svg[id^="mermaid-svg-"] {
|
||||
line[class^="messageLine"] {
|
||||
/*
|
||||
Mermaid's support for dynamic palette changes leaves a lot to be desired.
|
||||
This is a workaround to keep content readable while not breaking the rest of the theme.
|
||||
*/
|
||||
filter: invert(1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user