Compare commits
1 Commits
flows/conc
...
permission
Author | SHA1 | Date | |
---|---|---|---|
ff787a0f59 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.2.2
|
||||
current_version = 2025.2.1
|
||||
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,22 +9,17 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install apt deps
|
||||
- name: Install poetry & 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: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Setup python
|
||||
- name: Setup python and restore poetry
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
- name: Install Python deps
|
||||
shell: bash
|
||||
run: uv sync --all-extras --dev --frozen
|
||||
cache: "poetry"
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@ -44,9 +39,10 @@ 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: uv run python {0}
|
||||
shell: poetry 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: uv
|
||||
- package-ecosystem: pip
|
||||
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: |
|
||||
uv run make aws-cfn
|
||||
poetry 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: uv run make ci-${{ matrix.job }}
|
||||
run: poetry 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: uv run python -m lifecycle.migrate
|
||||
run: poetry run python -m lifecycle.migrate
|
||||
test-make-seed:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -69,21 +69,19 @@ 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 scripts/
|
||||
mv ../scripts .
|
||||
rm -rf .github/ scripts/
|
||||
mv ../.github ../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
|
||||
@ -93,13 +91,15 @@ 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: |
|
||||
uv run python -m lifecycle.migrate
|
||||
poetry 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: |
|
||||
uv run make ci-test
|
||||
poetry 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: |
|
||||
uv run make ci-test
|
||||
poetry 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: |
|
||||
uv run coverage run manage.py test tests/integration
|
||||
uv run coverage xml
|
||||
poetry run coverage run manage.py test tests/integration
|
||||
poetry run coverage xml
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
@ -214,8 +214,8 @@ jobs:
|
||||
npm run build
|
||||
- name: run e2e
|
||||
run: |
|
||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
uv run coverage xml
|
||||
poetry run coverage run manage.py test ${{ matrix.job.glob }}
|
||||
poetry 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: uv run ak update_webauthn_mds
|
||||
- run: poetry 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: |
|
||||
uv run make migrate
|
||||
uv run ak build_source_docs
|
||||
poetry run make migrate
|
||||
poetry 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: |
|
||||
uv run make i18n-extract
|
||||
poetry run make i18n-extract
|
||||
- name: run compile
|
||||
run: |
|
||||
uv run ak compilemessages
|
||||
poetry 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,13 +3,8 @@
|
||||
"tasks": [
|
||||
{
|
||||
"label": "authentik/core: make",
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"make",
|
||||
"lint-fix",
|
||||
"lint"
|
||||
],
|
||||
"command": "poetry",
|
||||
"args": ["run", "make", "lint-fix", "lint"],
|
||||
"presentation": {
|
||||
"panel": "new"
|
||||
},
|
||||
@ -17,12 +12,8 @@
|
||||
},
|
||||
{
|
||||
"label": "authentik/core: run",
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"ak",
|
||||
"server"
|
||||
],
|
||||
"command": "poetry",
|
||||
"args": ["run", "ak", "server"],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
@ -32,17 +23,13 @@
|
||||
{
|
||||
"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",
|
||||
@ -52,26 +39,19 @@
|
||||
{
|
||||
"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",
|
||||
@ -80,12 +60,8 @@
|
||||
},
|
||||
{
|
||||
"label": "authentik/api: generate",
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"make",
|
||||
"gen"
|
||||
],
|
||||
"command": "poetry",
|
||||
"args": ["run", "make", "gen"],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
|
@ -10,7 +10,7 @@ schemas/ @goauthentik/backend
|
||||
scripts/ @goauthentik/backend
|
||||
tests/ @goauthentik/backend
|
||||
pyproject.toml @goauthentik/backend
|
||||
uv.lock @goauthentik/backend
|
||||
poetry.lock @goauthentik/backend
|
||||
go.mod @goauthentik/backend
|
||||
go.sum @goauthentik/backend
|
||||
# Infrastructure
|
||||
|
88
Dockerfile
88
Dockerfile
@ -3,7 +3,8 @@
|
||||
# Stage 1: Build website
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NODE_ENV=production \
|
||||
GIT_UNAVAILABLE=true
|
||||
|
||||
WORKDIR /work/website
|
||||
|
||||
@ -93,59 +94,53 @@ 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: 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
|
||||
# Stage 5: Python dependencies
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-deps
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
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
|
||||
WORKDIR /ak-root/poetry
|
||||
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
ENV VENV_PATH="/ak-root/venv" \
|
||||
POETRY_VIRTUALENVS_CREATE=false \
|
||||
PATH="/ak-root/venv/bin:$PATH"
|
||||
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||
|
||||
RUN --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 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
|
||||
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"
|
||||
|
||||
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
|
||||
# Stage 6: Run
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS final-image
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
@ -177,7 +172,7 @@ RUN apt-get update && \
|
||||
|
||||
COPY ./authentik/ /authentik
|
||||
COPY ./pyproject.toml /
|
||||
COPY ./uv.lock /
|
||||
COPY ./poetry.lock /
|
||||
COPY ./schemas /schemas
|
||||
COPY ./locale /locale
|
||||
COPY ./tests /tests
|
||||
@ -186,7 +181,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/
|
||||
@ -197,6 +192,9 @@ USER 1000
|
||||
ENV TMPDIR=/dev/shm/ \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/ak-root/venv/bin:/lifecycle:$PATH" \
|
||||
VENV_PATH="/ak-root/venv" \
|
||||
POETRY_VIRTUALENVS_CREATE=false \
|
||||
GOFIPS=1
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
|
||||
|
51
Makefile
51
Makefile
@ -12,9 +12,9 @@ GEN_API_TS = "gen-ts-api"
|
||||
GEN_API_PY = "gen-py-api"
|
||||
GEN_API_GO = "gen-go-api"
|
||||
|
||||
pg_user := $(shell 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)
|
||||
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)
|
||||
|
||||
all: lint-fix lint test gen web ## Lint, build, and test everything
|
||||
|
||||
@ -32,37 +32,34 @@ go-test:
|
||||
go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
test: ## Run the server tests and produce a coverage report (locally)
|
||||
uv run coverage run manage.py test --keepdb authentik
|
||||
uv run coverage html
|
||||
uv run coverage report
|
||||
poetry run coverage run manage.py test --keepdb authentik
|
||||
poetry run coverage html
|
||||
poetry run coverage report
|
||||
|
||||
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||
uv run black $(PY_SOURCES)
|
||||
uv run ruff check --fix $(PY_SOURCES)
|
||||
poetry run black $(PY_SOURCES)
|
||||
poetry run ruff check --fix $(PY_SOURCES)
|
||||
|
||||
lint-codespell: ## Reports spelling errors.
|
||||
uv run codespell -w
|
||||
poetry run codespell -w
|
||||
|
||||
lint: ## Lint the python and golang sources
|
||||
uv run bandit -c pyproject.toml -r $(PY_SOURCES)
|
||||
poetry run bandit -c pyproject.toml -r $(PY_SOURCES)
|
||||
golangci-lint run -v
|
||||
|
||||
core-install:
|
||||
uv sync --frozen
|
||||
poetry install
|
||||
|
||||
migrate: ## Run the Authentik Django server's migrations
|
||||
uv run python -m lifecycle.migrate
|
||||
poetry 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:
|
||||
uv run ak makemessages \
|
||||
poetry run ak makemessages \
|
||||
--add-location file \
|
||||
--no-obsolete \
|
||||
--ignore web \
|
||||
@ -93,11 +90,11 @@ gen-build: ## Extract the schema from the database
|
||||
AUTHENTIK_DEBUG=true \
|
||||
AUTHENTIK_TENANTS__ENABLED=true \
|
||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||
uv run ak make_blueprint_schema > blueprints/schema.json
|
||||
poetry run ak make_blueprint_schema > blueprints/schema.json
|
||||
AUTHENTIK_DEBUG=true \
|
||||
AUTHENTIK_TENANTS__ENABLED=true \
|
||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||
uv run ak spectacular --file schema.yml
|
||||
poetry 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
|
||||
@ -176,7 +173,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
|
||||
uv run scripts/generate_config.py
|
||||
poetry run scripts/generate_config.py
|
||||
|
||||
gen: gen-build gen-client-ts
|
||||
|
||||
@ -257,21 +254,21 @@ ci--meta-debug:
|
||||
node --version
|
||||
|
||||
ci-black: ci--meta-debug
|
||||
uv run black --check $(PY_SOURCES)
|
||||
poetry run black --check $(PY_SOURCES)
|
||||
|
||||
ci-ruff: ci--meta-debug
|
||||
uv run ruff check $(PY_SOURCES)
|
||||
poetry run ruff check $(PY_SOURCES)
|
||||
|
||||
ci-codespell: ci--meta-debug
|
||||
uv run codespell -s
|
||||
poetry run codespell -s
|
||||
|
||||
ci-bandit: ci--meta-debug
|
||||
uv run bandit -r $(PY_SOURCES)
|
||||
poetry run bandit -r $(PY_SOURCES)
|
||||
|
||||
ci-pending-migrations: ci--meta-debug
|
||||
uv run ak makemigrations --check
|
||||
poetry run ak makemigrations --check
|
||||
|
||||
ci-test: ci--meta-debug
|
||||
uv run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
||||
uv run coverage report
|
||||
uv run coverage xml
|
||||
poetry run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
||||
poetry run coverage report
|
||||
poetry run coverage xml
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.2.2"
|
||||
__version__ = "2025.2.1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -54,7 +54,6 @@ class Challenge(PassiveSerializer):
|
||||
|
||||
flow_info = ContextualFlowInfo(required=False)
|
||||
component = CharField(default="")
|
||||
xid = CharField(required=False)
|
||||
|
||||
response_errors = DictField(
|
||||
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
|
||||
|
@ -143,12 +143,10 @@ class FlowPlan:
|
||||
request: HttpRequest,
|
||||
flow: Flow,
|
||||
allowed_silent_types: list["StageView"] | None = None,
|
||||
**get_params,
|
||||
) -> HttpResponse:
|
||||
"""Redirect to the flow executor for this flow plan"""
|
||||
from authentik.flows.views.executor import (
|
||||
SESSION_KEY_PLAN,
|
||||
FlowContainer,
|
||||
FlowExecutorView,
|
||||
)
|
||||
|
||||
@ -159,7 +157,6 @@ class FlowPlan:
|
||||
# No unskippable stages found, so we can directly return the response of the last stage
|
||||
final_stage: type[StageView] = self.bindings[-1].stage.view
|
||||
temp_exec = FlowExecutorView(flow=flow, request=request, plan=self)
|
||||
temp_exec.container = FlowContainer(request)
|
||||
temp_exec.current_stage = self.bindings[-1].stage
|
||||
temp_exec.current_stage_view = final_stage
|
||||
temp_exec.setup(request, flow.slug)
|
||||
@ -177,9 +174,6 @@ class FlowPlan:
|
||||
):
|
||||
get_qs["inspector"] = "available"
|
||||
|
||||
for key, value in get_params:
|
||||
get_qs[key] = value
|
||||
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
get_qs,
|
||||
|
@ -191,7 +191,6 @@ class ChallengeStageView(StageView):
|
||||
)
|
||||
flow_info.is_valid()
|
||||
challenge.initial_data["flow_info"] = flow_info.data
|
||||
challenge.initial_data["xid"] = self.executor.container.exec_id
|
||||
if isinstance(challenge, WithUserInfoChallenge):
|
||||
# If there's a pending user, update the `username` field
|
||||
# this field is only used by password managers.
|
||||
|
@ -28,7 +28,7 @@ window.authentik.flow = {
|
||||
|
||||
{% block body %}
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-flow-executor flowSlug="{{ flow.slug }}" xid="{{ xid }}">
|
||||
<ak-flow-executor flowSlug="{{ flow.slug }}">
|
||||
<ak-loading></ak-loading>
|
||||
</ak-flow-executor>
|
||||
{% endblock %}
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""authentik multi-stage authentication engine"""
|
||||
|
||||
from copy import deepcopy
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
@ -65,7 +64,6 @@ from authentik.policies.engine import PolicyEngine
|
||||
LOGGER = get_logger()
|
||||
# Argument used to redirect user after login
|
||||
NEXT_ARG_NAME = "next"
|
||||
SESSION_KEY_PLAN_CONTAINER = "authentik/flows/plan_container/%s"
|
||||
SESSION_KEY_PLAN = "authentik/flows/plan"
|
||||
SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
|
||||
SESSION_KEY_GET = "authentik/flows/get"
|
||||
@ -73,7 +71,6 @@ SESSION_KEY_POST = "authentik/flows/post"
|
||||
SESSION_KEY_HISTORY = "authentik/flows/history"
|
||||
QS_KEY_TOKEN = "flow_token" # nosec
|
||||
QS_QUERY = "query"
|
||||
QS_EXEC_ID = "xid"
|
||||
|
||||
|
||||
def challenge_types():
|
||||
@ -100,88 +97,6 @@ class InvalidStageError(SentryIgnoredException):
|
||||
"""Error raised when a challenge from a stage is not valid"""
|
||||
|
||||
|
||||
class FlowContainer:
|
||||
"""Allow for multiple concurrent flow executions in the same session"""
|
||||
|
||||
def __init__(self, request: HttpRequest, exec_id: str | None = None) -> None:
|
||||
self.request = request
|
||||
self.exec_id = exec_id
|
||||
|
||||
@staticmethod
|
||||
def new(request: HttpRequest):
|
||||
exec_id = str(uuid4())
|
||||
request.session[SESSION_KEY_PLAN_CONTAINER % exec_id] = {}
|
||||
return FlowContainer(request, exec_id)
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""Check if flow exists in container/session"""
|
||||
return SESSION_KEY_PLAN in self.session
|
||||
|
||||
def save(self):
|
||||
self.request.session.modified = True
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
# Backwards compatibility: store session plan/etc directly in session
|
||||
if not self.exec_id:
|
||||
return self.request.session
|
||||
self.request.session.setdefault(SESSION_KEY_PLAN_CONTAINER % self.exec_id, {})
|
||||
return self.request.session.get(SESSION_KEY_PLAN_CONTAINER % self.exec_id, {})
|
||||
|
||||
@property
|
||||
def plan(self) -> FlowPlan:
|
||||
return self.session.get(SESSION_KEY_PLAN)
|
||||
|
||||
def to_redirect(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
flow: Flow,
|
||||
allowed_silent_types: list[StageView] | None = None,
|
||||
**get_params,
|
||||
) -> HttpResponse:
|
||||
get_params[QS_EXEC_ID] = self.exec_id
|
||||
return self.plan.to_redirect(
|
||||
request, flow, allowed_silent_types=allowed_silent_types, **get_params
|
||||
)
|
||||
|
||||
@plan.setter
|
||||
def plan(self, value: FlowPlan):
|
||||
self.session[SESSION_KEY_PLAN] = value
|
||||
self.request.session.modified = True
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def application_pre(self):
|
||||
return self.session.get(SESSION_KEY_APPLICATION_PRE)
|
||||
|
||||
@property
|
||||
def get(self) -> QueryDict:
|
||||
return self.session.get(SESSION_KEY_GET)
|
||||
|
||||
@get.setter
|
||||
def get(self, value: QueryDict):
|
||||
self.session[SESSION_KEY_GET] = value
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def post(self) -> QueryDict:
|
||||
return self.session.get(SESSION_KEY_POST)
|
||||
|
||||
@post.setter
|
||||
def post(self, value: QueryDict):
|
||||
self.session[SESSION_KEY_POST] = value
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def history(self) -> list[FlowPlan]:
|
||||
return self.session.get(SESSION_KEY_HISTORY)
|
||||
|
||||
@history.setter
|
||||
def history(self, value: list[FlowPlan]):
|
||||
self.session[SESSION_KEY_HISTORY] = value
|
||||
self.save()
|
||||
|
||||
|
||||
@method_decorator(xframe_options_sameorigin, name="dispatch")
|
||||
class FlowExecutorView(APIView):
|
||||
"""Flow executor, passing requests to Stage Views"""
|
||||
@ -189,9 +104,8 @@ class FlowExecutorView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
flow: Flow = None
|
||||
plan: FlowPlan | None = None
|
||||
container: FlowContainer
|
||||
|
||||
plan: FlowPlan | None = None
|
||||
current_binding: FlowStageBinding | None = None
|
||||
current_stage: Stage
|
||||
current_stage_view: View
|
||||
@ -246,12 +160,10 @@ class FlowExecutorView(APIView):
|
||||
if QS_KEY_TOKEN in get_params:
|
||||
plan = self._check_flow_token(get_params[QS_KEY_TOKEN])
|
||||
if plan:
|
||||
container = FlowContainer.new(request)
|
||||
container.plan = plan
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
# Early check if there's an active Plan for the current session
|
||||
self.container = FlowContainer(request, request.GET.get(QS_EXEC_ID))
|
||||
if self.container.exists():
|
||||
self.plan: FlowPlan = self.container.plan
|
||||
if SESSION_KEY_PLAN in self.request.session:
|
||||
self.plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
||||
if self.plan.flow_pk != self.flow.pk.hex:
|
||||
self._logger.warning(
|
||||
"f(exec): Found existing plan for other flow, deleting plan",
|
||||
@ -264,14 +176,13 @@ class FlowExecutorView(APIView):
|
||||
self._logger.debug("f(exec): Continuing existing plan")
|
||||
|
||||
# Initial flow request, check if we have an upstream query string passed in
|
||||
self.container.get = get_params
|
||||
request.session[SESSION_KEY_GET] = get_params
|
||||
# Don't check session again as we've either already loaded the plan or we need to plan
|
||||
if not self.plan:
|
||||
self.container.history = []
|
||||
request.session[SESSION_KEY_HISTORY] = []
|
||||
self._logger.debug("f(exec): No active Plan found, initiating planner")
|
||||
try:
|
||||
self.plan = self._initiate_plan()
|
||||
self.container.plan = self.plan
|
||||
except FlowNonApplicableException as exc:
|
||||
self._logger.warning("f(exec): Flow not applicable to current user", exc=exc)
|
||||
return self.handle_invalid_flow(exc)
|
||||
@ -343,19 +254,12 @@ class FlowExecutorView(APIView):
|
||||
request=OpenApiTypes.NONE,
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name=QS_QUERY,
|
||||
name="query",
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=True,
|
||||
description="Querystring as received",
|
||||
type=OpenApiTypes.STR,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name=QS_EXEC_ID,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=False,
|
||||
description="Flow execution ID",
|
||||
type=OpenApiTypes.STR,
|
||||
),
|
||||
)
|
||||
],
|
||||
operation_id="flows_executor_get",
|
||||
)
|
||||
@ -382,7 +286,7 @@ class FlowExecutorView(APIView):
|
||||
span.set_data("authentik Stage", self.current_stage_view)
|
||||
span.set_data("authentik Flow", self.flow.slug)
|
||||
stage_response = self.current_stage_view.dispatch(request)
|
||||
return to_stage_response(request, stage_response, self.container.exec_id)
|
||||
return to_stage_response(request, stage_response)
|
||||
except Exception as exc:
|
||||
return self.handle_exception(exc)
|
||||
|
||||
@ -401,19 +305,12 @@ class FlowExecutorView(APIView):
|
||||
),
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name=QS_QUERY,
|
||||
name="query",
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=True,
|
||||
description="Querystring as received",
|
||||
type=OpenApiTypes.STR,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name=QS_EXEC_ID,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=True,
|
||||
description="Flow execution ID",
|
||||
type=OpenApiTypes.STR,
|
||||
),
|
||||
)
|
||||
],
|
||||
operation_id="flows_executor_solve",
|
||||
)
|
||||
@ -440,15 +337,14 @@ class FlowExecutorView(APIView):
|
||||
span.set_data("authentik Stage", self.current_stage_view)
|
||||
span.set_data("authentik Flow", self.flow.slug)
|
||||
stage_response = self.current_stage_view.dispatch(request)
|
||||
return to_stage_response(request, stage_response, self.container.exec_id)
|
||||
return to_stage_response(request, stage_response)
|
||||
except Exception as exc:
|
||||
return self.handle_exception(exc)
|
||||
|
||||
def _initiate_plan(self) -> FlowPlan:
|
||||
planner = FlowPlanner(self.flow)
|
||||
plan = planner.plan(self.request)
|
||||
container = FlowContainer.new(self.request)
|
||||
container.plan = plan
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
try:
|
||||
# Call the has_stages getter to check that
|
||||
# there are no issues with the class we might've gotten
|
||||
@ -472,7 +368,7 @@ class FlowExecutorView(APIView):
|
||||
except FlowNonApplicableException as exc:
|
||||
self._logger.warning("f(exec): Flow restart not applicable to current user", exc=exc)
|
||||
return self.handle_invalid_flow(exc)
|
||||
self.container.plan = plan
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
kwargs = self.kwargs
|
||||
kwargs.update({"flow_slug": self.flow.slug})
|
||||
return redirect_with_qs("authentik_api:flow-executor", self.request.GET, **kwargs)
|
||||
@ -494,13 +390,9 @@ class FlowExecutorView(APIView):
|
||||
)
|
||||
self.cancel()
|
||||
if next_param and not is_url_absolute(next_param):
|
||||
return to_stage_response(
|
||||
self.request, redirect_with_qs(next_param), self.container.exec_id
|
||||
)
|
||||
return to_stage_response(self.request, redirect_with_qs(next_param))
|
||||
return to_stage_response(
|
||||
self.request,
|
||||
self.stage_invalid(error_message=_("Invalid next URL")),
|
||||
self.container.exec_id,
|
||||
self.request, self.stage_invalid(error_message=_("Invalid next URL"))
|
||||
)
|
||||
|
||||
def stage_ok(self) -> HttpResponse:
|
||||
@ -514,7 +406,7 @@ class FlowExecutorView(APIView):
|
||||
self.current_stage_view.cleanup()
|
||||
self.request.session.get(SESSION_KEY_HISTORY, []).append(deepcopy(self.plan))
|
||||
self.plan.pop()
|
||||
self.container.plan = self.plan
|
||||
self.request.session[SESSION_KEY_PLAN] = self.plan
|
||||
if self.plan.bindings:
|
||||
self._logger.debug(
|
||||
"f(exec): Continuing with next stage",
|
||||
@ -557,7 +449,6 @@ class FlowExecutorView(APIView):
|
||||
|
||||
def cancel(self):
|
||||
"""Cancel current flow execution"""
|
||||
# TODO: Clean up container
|
||||
keys_to_delete = [
|
||||
SESSION_KEY_APPLICATION_PRE,
|
||||
SESSION_KEY_PLAN,
|
||||
@ -580,8 +471,8 @@ class CancelView(View):
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
"""View which canels the currently active plan"""
|
||||
if FlowContainer(request, request.GET.get(QS_EXEC_ID)).exists():
|
||||
del request.session[SESSION_KEY_PLAN_CONTAINER % request.GET.get(QS_EXEC_ID)]
|
||||
if SESSION_KEY_PLAN in request.session:
|
||||
del request.session[SESSION_KEY_PLAN]
|
||||
LOGGER.debug("Canceled current plan")
|
||||
return redirect("authentik_flows:default-invalidation")
|
||||
|
||||
@ -629,12 +520,19 @@ class ToDefaultFlow(View):
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
flow = self.get_flow()
|
||||
get_qs = request.GET.copy()
|
||||
get_qs[QS_EXEC_ID] = str(uuid4())
|
||||
return redirect_with_qs("authentik_core:if-flow", get_qs, flow_slug=flow.slug)
|
||||
# If user already has a pending plan, clear it so we don't have to later.
|
||||
if SESSION_KEY_PLAN in self.request.session:
|
||||
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
||||
if plan.flow_pk != flow.pk.hex:
|
||||
LOGGER.warning(
|
||||
"f(def): Found existing plan for other flow, deleting plan",
|
||||
flow_slug=flow.slug,
|
||||
)
|
||||
del self.request.session[SESSION_KEY_PLAN]
|
||||
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
||||
|
||||
|
||||
def to_stage_response(request: HttpRequest, source: HttpResponse, xid: str) -> HttpResponse:
|
||||
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse:
|
||||
"""Convert normal HttpResponse into JSON Response"""
|
||||
if (
|
||||
isinstance(source, HttpResponseRedirect)
|
||||
@ -653,7 +551,6 @@ def to_stage_response(request: HttpRequest, source: HttpResponse, xid: str) -> H
|
||||
RedirectChallenge(
|
||||
{
|
||||
"to": str(redirect_url),
|
||||
"xid": xid,
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -662,7 +559,6 @@ def to_stage_response(request: HttpRequest, source: HttpResponse, xid: str) -> H
|
||||
ShellChallenge(
|
||||
{
|
||||
"body": source.render().content.decode("utf-8"),
|
||||
"xid": xid,
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -672,7 +568,6 @@ def to_stage_response(request: HttpRequest, source: HttpResponse, xid: str) -> H
|
||||
ShellChallenge(
|
||||
{
|
||||
"body": source.content.decode("utf-8"),
|
||||
"xid": xid,
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -704,6 +599,4 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
|
||||
except FlowNonApplicableException:
|
||||
LOGGER.warning("Flow not applicable to user")
|
||||
raise Http404 from None
|
||||
container = FlowContainer.new(request)
|
||||
container.plan = plan
|
||||
return container.to_redirect(request, stage.configure_flow)
|
||||
return plan.to_redirect(request, stage.configure_flow)
|
||||
|
@ -7,7 +7,6 @@ from ua_parser.user_agent_parser import Parse
|
||||
|
||||
from authentik.core.views.interface import InterfaceView
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.views.executor import QS_EXEC_ID
|
||||
|
||||
|
||||
class FlowInterfaceView(InterfaceView):
|
||||
@ -16,7 +15,6 @@ class FlowInterfaceView(InterfaceView):
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||
kwargs["inspector"] = "inspector" in self.request.GET
|
||||
kwargs["xid"] = self.request.GET.get(QS_EXEC_ID)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def compat_needs_sfe(self) -> bool:
|
||||
|
@ -180,7 +180,6 @@ 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",
|
||||
|
@ -1,28 +0,0 @@
|
||||
# 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,20 +71,6 @@ 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",
|
||||
@ -184,6 +170,7 @@ 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,6 +1,5 @@
|
||||
"""SAML Assertion generator"""
|
||||
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
from types import GeneratorType
|
||||
|
||||
@ -53,7 +52,6 @@ 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
|
||||
@ -67,11 +65,6 @@ 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)
|
||||
)
|
||||
@ -138,7 +131,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._auth_instant
|
||||
auth_n_statement.attrib["AuthnInstant"] = self._valid_not_before
|
||||
auth_n_statement.attrib["SessionIndex"] = sha256(
|
||||
self.http_request.session.session_key.encode("ascii")
|
||||
).hexdigest()
|
||||
@ -165,28 +158,6 @@ 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,61 +294,6 @@ 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()
|
||||
@ -376,10 +321,8 @@ class TestAuthNRequest(TestCase):
|
||||
request = request_proc.build_auth_n()
|
||||
|
||||
# Create invalid PropertyMapping
|
||||
mapping = SAMLPropertyMapping.objects.create(
|
||||
name=generate_id(), saml_name="test", expression="q"
|
||||
)
|
||||
self.provider.property_mappings.add(mapping)
|
||||
scope = SAMLPropertyMapping.objects.create(name="test", saml_name="test", expression="q")
|
||||
self.provider.property_mappings.add(scope)
|
||||
|
||||
# To get an assertion we need a parsed request (parsed by provider)
|
||||
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||
@ -395,7 +338,7 @@ class TestAuthNRequest(TestCase):
|
||||
self.assertTrue(events.exists())
|
||||
self.assertEqual(
|
||||
events.first().context["message"],
|
||||
f"Failed to evaluate property-mapping: '{mapping.name}'",
|
||||
"Failed to evaluate property-mapping: 'test'",
|
||||
)
|
||||
|
||||
def test_idp_initiated(self):
|
||||
|
@ -1,16 +1,12 @@
|
||||
"""Time utilities"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.utils.timezone import now
|
||||
import datetime
|
||||
|
||||
|
||||
def get_time_string(delta: timedelta | datetime | None = None) -> str:
|
||||
def get_time_string(delta: datetime.timedelta | None = None) -> str:
|
||||
"""Get Data formatted in SAML format"""
|
||||
if delta is None:
|
||||
delta = timedelta()
|
||||
if isinstance(delta, timedelta):
|
||||
final = now() + delta
|
||||
else:
|
||||
final = delta
|
||||
delta = datetime.timedelta()
|
||||
now = datetime.datetime.now()
|
||||
final = now + delta
|
||||
return final.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
@ -24,9 +24,7 @@ class SCIMProviderGroupSerializer(ModelSerializer):
|
||||
"group",
|
||||
"group_obj",
|
||||
"provider",
|
||||
"attributes",
|
||||
]
|
||||
extra_kwargs = {"attributes": {"read_only": True}}
|
||||
|
||||
|
||||
class SCIMProviderGroupViewSet(
|
||||
|
@ -24,9 +24,7 @@ class SCIMProviderUserSerializer(ModelSerializer):
|
||||
"user",
|
||||
"user_obj",
|
||||
"provider",
|
||||
"attributes",
|
||||
]
|
||||
extra_kwargs = {"attributes": {"read_only": True}}
|
||||
|
||||
|
||||
class SCIMProviderUserViewSet(
|
||||
|
@ -102,7 +102,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
if not scim_id or scim_id == "":
|
||||
raise StopSync("SCIM Response with missing or invalid `id`")
|
||||
connection = SCIMProviderGroup.objects.create(
|
||||
provider=self.provider, group=group, scim_id=scim_id, attributes=response
|
||||
provider=self.provider, group=group, scim_id=scim_id
|
||||
)
|
||||
users = list(group.users.order_by("id").values_list("id", flat=True))
|
||||
self._patch_add_users(connection, users)
|
||||
|
@ -77,24 +77,21 @@ 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"],
|
||||
attributes=users_res[0],
|
||||
provider=self.provider, user=user, scim_id=users_res[0]["id"]
|
||||
)
|
||||
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, attributes=response
|
||||
provider=self.provider, user=user, scim_id=scim_id
|
||||
)
|
||||
|
||||
def update(self, user: User, connection: SCIMProviderUser):
|
||||
"""Update existing user"""
|
||||
scim_user = self.to_schema(user, connection)
|
||||
scim_user.id = connection.scim_id
|
||||
response = self._request(
|
||||
self._request(
|
||||
"PUT",
|
||||
f"/Users/{connection.scim_id}",
|
||||
json=scim_user.model_dump(
|
||||
@ -102,5 +99,3 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
|
||||
exclude_unset=True,
|
||||
),
|
||||
)
|
||||
connection.attributes = response
|
||||
connection.save()
|
||||
|
@ -1,23 +0,0 @@
|
||||
# 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,7 +22,6 @@ 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]:
|
||||
@ -44,7 +43,6 @@ class SCIMProviderGroup(SerializerModel):
|
||||
scim_id = models.TextField()
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
provider = models.ForeignKey("SCIMProvider", on_delete=models.CASCADE)
|
||||
attributes = models.JSONField(default=dict)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
|
File diff suppressed because one or more lines are too long
@ -142,35 +142,38 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
raise ValidationError("Failed to authenticate.")
|
||||
self.pre_user = pre_user
|
||||
|
||||
# Password check
|
||||
if current_stage.password_stage:
|
||||
password = attrs.get("password", None)
|
||||
if not password:
|
||||
self.stage.logger.warning("Password not set for ident+auth attempt")
|
||||
try:
|
||||
with start_span(
|
||||
op="authentik.stages.identification.authenticate",
|
||||
name="User authenticate call (combo stage)",
|
||||
):
|
||||
user = authenticate(
|
||||
self.stage.request,
|
||||
current_stage.password_stage.backends,
|
||||
current_stage,
|
||||
username=self.pre_user.username,
|
||||
password=password,
|
||||
)
|
||||
if not user:
|
||||
raise ValidationError("Failed to authenticate.")
|
||||
self.pre_user = user
|
||||
except PermissionDenied as exc:
|
||||
raise ValidationError(str(exc)) from exc
|
||||
|
||||
# Captcha check
|
||||
if captcha_stage := current_stage.captcha_stage:
|
||||
captcha_token = attrs.get("captcha_token", None)
|
||||
if not captcha_token:
|
||||
self.stage.logger.warning("Token not set for captcha attempt")
|
||||
verify_captcha_token(captcha_stage, captcha_token, client_ip)
|
||||
|
||||
# Password check
|
||||
if not current_stage.password_stage:
|
||||
# No password stage select, don't validate the password
|
||||
return attrs
|
||||
|
||||
password = attrs.get("password", None)
|
||||
if not password:
|
||||
self.stage.logger.warning("Password not set for ident+auth attempt")
|
||||
try:
|
||||
with start_span(
|
||||
op="authentik.stages.identification.authenticate",
|
||||
name="User authenticate call (combo stage)",
|
||||
):
|
||||
user = authenticate(
|
||||
self.stage.request,
|
||||
current_stage.password_stage.backends,
|
||||
current_stage,
|
||||
username=self.pre_user.username,
|
||||
password=password,
|
||||
)
|
||||
if not user:
|
||||
raise ValidationError("Failed to authenticate.")
|
||||
self.pre_user = user
|
||||
except PermissionDenied as exc:
|
||||
raise ValidationError(str(exc)) from exc
|
||||
return attrs
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2025.2.2 Blueprint schema",
|
||||
"title": "authentik 2025.2.1 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@ -6462,11 +6462,6 @@
|
||||
"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": [
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.1}
|
||||
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.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.1}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
4
go.mod
4
go.mod
@ -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.2025022.3
|
||||
goauthentik.io/api/v3 v3.2025021.4
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/sync v0.12.0
|
||||
@ -82,5 +82,3 @@ require (
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace goauthentik.io/api/v3 => ./gen-go-api
|
||||
|
4
go.sum
4
go.sum
@ -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.2025022.3 h1:cipaxl0il4/s1fU2f6+CD7nzgAktbV0XD7r5qHh0fUc=
|
||||
goauthentik.io/api/v3 v3.2025022.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025021.4 h1:KFap2KW+8CwhOxjBkRnRB4flvuHEMw24+fZei9dOhzw=
|
||||
goauthentik.io/api/v3 v3.2025021.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
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=
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2025.2.2"
|
||||
const VERSION = "2025.2.1"
|
||||
|
@ -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 uv sync --frozen
|
||||
VIRTUAL_ENV=/ak-root/venv poetry install --no-ansi --no-interaction
|
||||
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 uv"""
|
||||
"""Wrapper for lifecycle/ak, to be installed by poetry"""
|
||||
|
||||
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.1005.0",
|
||||
"aws-cdk": "^2.1004.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,9 +17,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1005.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz",
|
||||
"integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==",
|
||||
"version": "2.1004.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1004.0.tgz",
|
||||
"integrity": "sha512-3E5ICmSc7ZCZCwLX7NY+HFmmdUYgRaL+67h/BDoDQmkhx9StC8wG4xgzHFY9k8WQS0+ib/MP28f2d9yzHtQLlQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1005.0",
|
||||
"aws-cdk": "^2.1004.0",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2025.2.2
|
||||
Default: 2025.2.1
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
12
package-lock.json
generated
12
package-lock.json
generated
@ -1,12 +0,0 @@
|
||||
{
|
||||
"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.2",
|
||||
"version": "2025.2.1",
|
||||
"private": true
|
||||
}
|
||||
|
6120
poetry.lock
generated
Normal file
6120
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
222
pyproject.toml
222
pyproject.toml
@ -1,116 +1,8 @@
|
||||
[project]
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2025.2.2"
|
||||
version = "2025.2.1"
|
||||
description = ""
|
||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.12.*"
|
||||
dependencies = [
|
||||
"argon2-cffi",
|
||||
"celery",
|
||||
"channels",
|
||||
"channels-redis",
|
||||
"cryptography",
|
||||
"dacite",
|
||||
"deepmerge",
|
||||
"defusedxml",
|
||||
"django",
|
||||
"django-countries",
|
||||
"django-cte",
|
||||
"django-filter",
|
||||
"django-guardian",
|
||||
"django-model-utils",
|
||||
"django-pglock",
|
||||
"django-prometheus",
|
||||
"django-redis",
|
||||
"django-storages[s3]",
|
||||
"django-tenants",
|
||||
"djangorestframework ==3.14.0",
|
||||
"djangorestframework-guardian",
|
||||
"docker",
|
||||
"drf-orjson-renderer",
|
||||
"drf-spectacular",
|
||||
"dumb-init",
|
||||
"duo-client",
|
||||
"fido2",
|
||||
"flower",
|
||||
"geoip2",
|
||||
"geopy",
|
||||
"google-api-python-client",
|
||||
"gssapi",
|
||||
"gunicorn",
|
||||
"jsonpatch",
|
||||
"jwcrypto",
|
||||
"kubernetes",
|
||||
"ldap3",
|
||||
"lxml",
|
||||
"msgraph-sdk",
|
||||
"opencontainers",
|
||||
"packaging",
|
||||
"paramiko",
|
||||
"psycopg[c]",
|
||||
"pydantic",
|
||||
"pydantic-scim",
|
||||
"pyjwt",
|
||||
"pyrad",
|
||||
"python-kadmin-rs ==0.5.3",
|
||||
"pyyaml",
|
||||
"requests-oauthlib",
|
||||
"scim2-filter-parser",
|
||||
"sentry-sdk",
|
||||
"service_identity",
|
||||
"setproctitle",
|
||||
"structlog",
|
||||
"swagger-spec-validator",
|
||||
"tenant-schemas-celery",
|
||||
"twilio",
|
||||
"ua-parser",
|
||||
"unidecode",
|
||||
"urllib3 <3",
|
||||
"uvicorn[standard]",
|
||||
"watchdog",
|
||||
"webauthn",
|
||||
"wsproto",
|
||||
"xmlsec <= 1.3.14",
|
||||
"zxcvbn",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"aws-cdk-lib",
|
||||
"bandit",
|
||||
"black",
|
||||
"bump2version",
|
||||
"channels[daphne]",
|
||||
"codespell",
|
||||
"colorama",
|
||||
"constructs",
|
||||
"coverage[toml]",
|
||||
"debugpy",
|
||||
"drf-jsonschema-serializer",
|
||||
"freezegun",
|
||||
"importlib-metadata",
|
||||
"k5test",
|
||||
"pdoc",
|
||||
"pytest",
|
||||
"pytest-django",
|
||||
"pytest-github-actions-annotate-failures",
|
||||
"pytest-randomly",
|
||||
"pytest-timeout",
|
||||
"requests-mock",
|
||||
"ruff",
|
||||
"selenium",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" }
|
||||
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf" }
|
||||
|
||||
[project.scripts]
|
||||
ak = "lifecycle.ak:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
[tool.bandit]
|
||||
exclude_dirs = ["**/node_modules/**"]
|
||||
@ -138,7 +30,6 @@ skip = [
|
||||
]
|
||||
dictionary = ".github/codespell-dictionary.txt,-"
|
||||
ignore-words = ".github/codespell-words.txt"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py312']
|
||||
@ -169,7 +60,6 @@ select = [
|
||||
ignore = [
|
||||
"DJ001", # Avoid using `null=True` on string-based fields,
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pylint]
|
||||
max-args = 7
|
||||
max-branches = 18
|
||||
@ -217,3 +107,109 @@ filterwarnings = [
|
||||
"ignore:defusedxml.lxml is no longer supported and will be removed in a future release.:DeprecationWarning",
|
||||
"ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
argon2-cffi = "*"
|
||||
celery = "*"
|
||||
channels = "*"
|
||||
channels-redis = "*"
|
||||
cryptography = "*"
|
||||
dacite = "*"
|
||||
deepmerge = "*"
|
||||
defusedxml = "*"
|
||||
django = "*"
|
||||
django-countries = "*"
|
||||
django-cte = "*"
|
||||
django-filter = "*"
|
||||
django-guardian = "*"
|
||||
django-model-utils = "*"
|
||||
django-pglock = "*"
|
||||
django-prometheus = "*"
|
||||
django-redis = "*"
|
||||
django-storages = { extras = ["s3"], version = "*" }
|
||||
# See https://github.com/django-tenants/django-tenants/pull/997
|
||||
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" }
|
||||
djangorestframework = "3.14.0"
|
||||
djangorestframework-guardian = "*"
|
||||
docker = "*"
|
||||
drf-orjson-renderer = "*"
|
||||
drf-spectacular = "*"
|
||||
dumb-init = "*"
|
||||
duo-client = "*"
|
||||
fido2 = "*"
|
||||
flower = "*"
|
||||
geoip2 = "*"
|
||||
geopy = "*"
|
||||
google-api-python-client = "*"
|
||||
gunicorn = "*"
|
||||
gssapi = "*"
|
||||
jsonpatch = "*"
|
||||
jwcrypto = "*"
|
||||
kubernetes = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
msgraph-sdk = "*"
|
||||
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = [
|
||||
"reggie",
|
||||
] }
|
||||
packaging = "*"
|
||||
paramiko = "*"
|
||||
psycopg = { extras = ["c"], version = "*" }
|
||||
pydantic = "*"
|
||||
pydantic-scim = "*"
|
||||
pyjwt = "*"
|
||||
pyrad = "*"
|
||||
python = "~3.12"
|
||||
python-kadmin-rs = "0.5.3"
|
||||
pyyaml = "*"
|
||||
requests-oauthlib = "*"
|
||||
scim2-filter-parser = "*"
|
||||
sentry-sdk = "*"
|
||||
service_identity = "*"
|
||||
setproctitle = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
tenant-schemas-celery = "*"
|
||||
twilio = "*"
|
||||
ua-parser = "*"
|
||||
unidecode = "*"
|
||||
# Pinned because of botocore https://github.com/orgs/python-poetry/discussions/7937
|
||||
urllib3 = { extras = ["secure"], version = "<3" }
|
||||
uvicorn = { extras = ["standard"], version = "*" }
|
||||
watchdog = "*"
|
||||
webauthn = "*"
|
||||
wsproto = "*"
|
||||
xmlsec = "*"
|
||||
zxcvbn = "*"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
aws-cdk-lib = "*"
|
||||
bandit = "*"
|
||||
black = "*"
|
||||
bump2version = "*"
|
||||
channels = { version = "*", extras = ["daphne"] }
|
||||
codespell = "*"
|
||||
colorama = "*"
|
||||
constructs = "*"
|
||||
coverage = { extras = ["toml"], version = "*" }
|
||||
debugpy = "*"
|
||||
drf-jsonschema-serializer = "*"
|
||||
freezegun = "*"
|
||||
importlib-metadata = "*"
|
||||
k5test = "*"
|
||||
pdoc = "*"
|
||||
pytest = "*"
|
||||
pytest-django = "*"
|
||||
pytest-github-actions-annotate-failures = "*"
|
||||
pytest-randomly = "*"
|
||||
pytest-timeout = "*"
|
||||
requests-mock = "*"
|
||||
ruff = "*"
|
||||
selenium = "*"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
ak = "lifecycle.ak:main"
|
||||
|
131
schema.yml
131
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2025.2.2
|
||||
version: 2025.2.1
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@ -8917,11 +8917,6 @@ paths:
|
||||
type: string
|
||||
description: Querystring as received
|
||||
required: true
|
||||
- in: query
|
||||
name: xid
|
||||
schema:
|
||||
type: string
|
||||
description: Flow execution ID
|
||||
tags:
|
||||
- flows
|
||||
security:
|
||||
@ -8962,12 +8957,6 @@ paths:
|
||||
type: string
|
||||
description: Querystring as received
|
||||
required: true
|
||||
- in: query
|
||||
name: xid
|
||||
schema:
|
||||
type: string
|
||||
description: Flow execution ID
|
||||
required: true
|
||||
tags:
|
||||
- flows
|
||||
requestBody:
|
||||
@ -22202,11 +22191,6 @@ 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:
|
||||
@ -25761,7 +25745,7 @@ paths:
|
||||
description: ''
|
||||
delete:
|
||||
operationId: sources_all_destroy
|
||||
description: Prevent deletion of built-in sources
|
||||
description: Source Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: slug
|
||||
@ -39448,8 +39432,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-access-denied
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -39465,7 +39447,6 @@ components:
|
||||
required:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- xid
|
||||
AlgEnum:
|
||||
enum:
|
||||
- rsa
|
||||
@ -39565,8 +39546,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-source-oauth-apple
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -39586,7 +39565,6 @@ components:
|
||||
- redirect_uri
|
||||
- scope
|
||||
- state
|
||||
- xid
|
||||
Application:
|
||||
type: object
|
||||
description: Application Serializer
|
||||
@ -39895,8 +39873,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-authenticator-duo
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -39919,7 +39895,6 @@ components:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- stage_uuid
|
||||
- xid
|
||||
AuthenticatorDuoChallengeResponseRequest:
|
||||
type: object
|
||||
description: Pseudo class for duo response
|
||||
@ -40057,8 +40032,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-authenticator-email
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -40078,7 +40051,6 @@ components:
|
||||
required:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- xid
|
||||
AuthenticatorEmailChallengeResponseRequest:
|
||||
type: object
|
||||
description: Authenticator Email Challenge response, device is set by get_response_instance
|
||||
@ -40316,8 +40288,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-authenticator-sms
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -40334,7 +40304,6 @@ components:
|
||||
required:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- xid
|
||||
AuthenticatorSMSChallengeResponseRequest:
|
||||
type: object
|
||||
description: SMS Challenge response, device is set by get_response_instance
|
||||
@ -40482,8 +40451,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-authenticator-static
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -40502,7 +40469,6 @@ components:
|
||||
- codes
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- xid
|
||||
AuthenticatorStaticChallengeResponseRequest:
|
||||
type: object
|
||||
description: Pseudo class for static response
|
||||
@ -40606,8 +40572,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-authenticator-totp
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -40624,7 +40588,6 @@ components:
|
||||
- config_url
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- xid
|
||||
AuthenticatorTOTPChallengeResponseRequest:
|
||||
type: object
|
||||
description: TOTP Challenge response, device is set by get_response_instance
|
||||
@ -40836,8 +40799,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-authenticator-validate
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -40861,7 +40822,6 @@ components:
|
||||
- device_challenges
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- xid
|
||||
AuthenticatorValidationChallengeResponseRequest:
|
||||
type: object
|
||||
description: Challenge used for Code-based and WebAuthn authenticators
|
||||
@ -40892,8 +40852,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-authenticator-webauthn
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -40911,7 +40869,6 @@ components:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- registration
|
||||
- xid
|
||||
AuthenticatorWebAuthnChallengeResponseRequest:
|
||||
type: object
|
||||
description: WebAuthn Challenge response
|
||||
@ -41044,8 +41001,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-autosubmit
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -41063,7 +41018,6 @@ components:
|
||||
required:
|
||||
- attrs
|
||||
- url
|
||||
- xid
|
||||
BackendsEnum:
|
||||
enum:
|
||||
- authentik.core.auth.InbuiltBackend
|
||||
@ -41310,8 +41264,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-captcha
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -41334,7 +41286,6 @@ components:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- site_key
|
||||
- xid
|
||||
CaptchaChallengeResponseRequest:
|
||||
type: object
|
||||
description: Validate captcha token
|
||||
@ -41718,8 +41669,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-consent
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -41748,7 +41697,6 @@ components:
|
||||
- pending_user_avatar
|
||||
- permissions
|
||||
- token
|
||||
- xid
|
||||
ConsentChallengeResponseRequest:
|
||||
type: object
|
||||
description: Consent challenge response, any valid response request is valid
|
||||
@ -42522,8 +42470,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-dummy
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -42534,7 +42480,6 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- xid
|
||||
DummyChallengeResponseRequest:
|
||||
type: object
|
||||
description: Dummy challenge response
|
||||
@ -42727,16 +42672,12 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-email
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorDetail'
|
||||
required:
|
||||
- xid
|
||||
EmailChallengeResponseRequest:
|
||||
type: object
|
||||
description: |-
|
||||
@ -43655,8 +43596,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-flow-error
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -43671,7 +43610,6 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- request_id
|
||||
- xid
|
||||
FlowImportResult:
|
||||
type: object
|
||||
description: Logs of an attempted flow import
|
||||
@ -43986,8 +43924,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: xak-flow-frame
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -44004,7 +43940,6 @@ components:
|
||||
required:
|
||||
- loading_text
|
||||
- url
|
||||
- xid
|
||||
FrameChallengeResponseRequest:
|
||||
type: object
|
||||
description: Base class for all challenge responses
|
||||
@ -44807,8 +44742,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-identification
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -44853,7 +44786,6 @@ components:
|
||||
- primary_action
|
||||
- show_source_labels
|
||||
- user_fields
|
||||
- xid
|
||||
IdentificationChallengeResponseRequest:
|
||||
type: object
|
||||
description: Identification challenge
|
||||
@ -47296,16 +47228,12 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-provider-oauth2-device-code
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorDetail'
|
||||
required:
|
||||
- xid
|
||||
OAuthDeviceCodeChallengeResponseRequest:
|
||||
type: object
|
||||
description: Response that includes the user-entered device code
|
||||
@ -47328,16 +47256,12 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-provider-oauth2-device-code-finish
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorDetail'
|
||||
required:
|
||||
- xid
|
||||
OAuthDeviceCodeFinishChallengeResponseRequest:
|
||||
type: object
|
||||
description: Response that device has been authenticated and tab can be closed
|
||||
@ -49482,8 +49406,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-password
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -49502,7 +49424,6 @@ components:
|
||||
required:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- xid
|
||||
PasswordChallengeResponseRequest:
|
||||
type: object
|
||||
description: Password challenge response
|
||||
@ -52307,14 +52228,6 @@ 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:
|
||||
@ -53064,8 +52977,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-source-plex
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -53079,7 +52990,6 @@ components:
|
||||
required:
|
||||
- client_id
|
||||
- slug
|
||||
- xid
|
||||
PlexAuthenticationChallengeResponseRequest:
|
||||
type: object
|
||||
description: Pseudo class for plex response
|
||||
@ -53592,8 +53502,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-prompt
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -53606,7 +53514,6 @@ components:
|
||||
$ref: '#/components/schemas/StagePrompt'
|
||||
required:
|
||||
- fields
|
||||
- xid
|
||||
PromptChallengeResponseRequest:
|
||||
type: object
|
||||
description: |-
|
||||
@ -54791,8 +54698,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: xak-flow-redirect
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -54803,7 +54708,6 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- to
|
||||
- xid
|
||||
RedirectChallengeResponseRequest:
|
||||
type: object
|
||||
description: Redirect challenge response
|
||||
@ -55279,14 +55183,6 @@ 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:
|
||||
@ -55452,14 +55348,6 @@ 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:
|
||||
@ -56009,10 +55897,7 @@ components:
|
||||
readOnly: true
|
||||
provider:
|
||||
type: integer
|
||||
attributes:
|
||||
readOnly: true
|
||||
required:
|
||||
- attributes
|
||||
- group
|
||||
- group_obj
|
||||
- id
|
||||
@ -56099,10 +55984,7 @@ components:
|
||||
readOnly: true
|
||||
provider:
|
||||
type: integer
|
||||
attributes:
|
||||
readOnly: true
|
||||
required:
|
||||
- attributes
|
||||
- id
|
||||
- provider
|
||||
- scim_id
|
||||
@ -56699,8 +56581,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-session-end
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -56723,7 +56603,6 @@ components:
|
||||
- brand_name
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- xid
|
||||
SessionUser:
|
||||
type: object
|
||||
description: |-
|
||||
@ -56836,8 +56715,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: xak-flow-shell
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -56848,7 +56725,6 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- body
|
||||
- xid
|
||||
SignatureAlgorithmEnum:
|
||||
enum:
|
||||
- http://www.w3.org/2000/09/xmldsig#rsa-sha1
|
||||
@ -58123,8 +57999,6 @@ components:
|
||||
component:
|
||||
type: string
|
||||
default: ak-stage-user-login
|
||||
xid:
|
||||
type: string
|
||||
response_errors:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -58138,7 +58012,6 @@ components:
|
||||
required:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- xid
|
||||
UserLoginChallengeResponseRequest:
|
||||
type: object
|
||||
description: User login challenge
|
||||
|
@ -7,8 +7,6 @@ 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,7 +13,6 @@ 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',
|
||||
];
|
||||
|
||||
@ -40,10 +39,6 @@ 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",
|
||||
|
@ -8,7 +8,7 @@ import path from "path";
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serializeCustomEventToStream(event) {
|
||||
// @ts-expect-error - TS doesn't know about the detail property
|
||||
// @ts-ignore
|
||||
const data = event.detail ?? {};
|
||||
|
||||
const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`];
|
@ -3,16 +3,15 @@ import esbuild from "esbuild";
|
||||
import findFreePorts from "find-free-ports";
|
||||
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
|
||||
import { globSync } from "glob";
|
||||
import * as path from "path";
|
||||
import path from "path";
|
||||
import { cwd } from "process";
|
||||
import process from "process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
|
||||
import { buildObserverPlugin } from "./esbuild/build-observer-plugin.mjs";
|
||||
import { buildObserverPlugin } from "./build-observer-plugin.mjs";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
let authentikProjectRoot = path.join(__dirname, "..", "..");
|
||||
let authentikProjectRoot = __dirname + "../";
|
||||
|
||||
try {
|
||||
// Use the package.json file in the root folder, as it has the current version information.
|
||||
@ -123,10 +122,11 @@ const BASE_ESBUILD_OPTIONS = {
|
||||
loader: {
|
||||
".css": "text",
|
||||
".md": "text",
|
||||
".mdx": "text",
|
||||
},
|
||||
define: definitions,
|
||||
format: "esm",
|
||||
plugins: [mdxPlugin()],
|
||||
plugins: [],
|
||||
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: path.join(__dirname, ".."),
|
||||
relativeRoot: __dirname,
|
||||
}),
|
||||
],
|
||||
define: {
|
@ -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!
|
||||
|
4383
web/package-lock.json
generated
4383
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,14 +11,12 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2025.2.2-1742395408",
|
||||
"@goauthentik/api": "^2025.2.1-1741798605",
|
||||
"@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",
|
||||
@ -38,11 +36,9 @@
|
||||
"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",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"showdown": "^2.1.0",
|
||||
"style-mod": "^4.1.2",
|
||||
"ts-pattern": "^5.4.0",
|
||||
"webcomponent-qr-code": "^1.2.0",
|
||||
@ -70,13 +66,13 @@
|
||||
"@types/guacamole-common-js": "^1.5.2",
|
||||
"@types/mocha": "^10.0.8",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/react": "^18.3.13",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@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",
|
||||
"change-case": "^5.4.4",
|
||||
"chokidar": "^4.0.1",
|
||||
"chromedriver": "^131.0.1",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.11.1",
|
||||
@ -91,13 +87,6 @@
|
||||
"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",
|
||||
@ -129,7 +118,7 @@
|
||||
"build-locales:build": "wireit",
|
||||
"build-proxy": "wireit",
|
||||
"build:sfe": "wireit",
|
||||
"esbuild:watch": "node scripts/build-web.mjs --watch",
|
||||
"esbuild:watch": "node build.mjs --watch",
|
||||
"extract-locales": "wireit",
|
||||
"format": "wireit",
|
||||
"lint": "wireit",
|
||||
@ -164,7 +153,7 @@
|
||||
"instead of `npm run watch`. The former is more comprehensive, but ",
|
||||
"the latter is faster."
|
||||
],
|
||||
"command": "${NODE_RUNNER} scripts/build-web.mjs",
|
||||
"command": "${NODE_RUNNER} build.mjs",
|
||||
"files": [
|
||||
"src/**/*.{css,jpg,png,ts,js,json}",
|
||||
"!src/**/*.stories.ts",
|
||||
@ -184,7 +173,6 @@
|
||||
"./dist/poly-*.js.map",
|
||||
"./dist/custom.css",
|
||||
"./dist/theme-dark.css",
|
||||
"./dist/one-dark.css",
|
||||
"./dist/patternfly.min.css"
|
||||
],
|
||||
"dependencies": [
|
||||
@ -207,7 +195,7 @@
|
||||
]
|
||||
},
|
||||
"build-proxy": {
|
||||
"command": "node scripts/build-web.mjs --proxy",
|
||||
"command": "node build.mjs --proxy",
|
||||
"dependencies": [
|
||||
"build-locales"
|
||||
]
|
||||
|
@ -1,299 +0,0 @@
|
||||
/**
|
||||
* @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,
|
||||
};
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/**
|
||||
* @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);
|
||||
};
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* @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);
|
||||
};
|
||||
};
|
@ -1,59 +0,0 @@
|
||||
/**
|
||||
* @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);
|
||||
};
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
/**
|
||||
* @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);
|
||||
};
|
||||
};
|
@ -28,6 +28,7 @@ import { when } from "lit/directives/when.js";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@ -54,6 +55,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
return [
|
||||
PFBase,
|
||||
PFGrid,
|
||||
PFFlex,
|
||||
PFPage,
|
||||
PFContent,
|
||||
PFDivider,
|
||||
@ -67,13 +69,6 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
.card-container {
|
||||
max-height: 10em;
|
||||
}
|
||||
.ak-external-link {
|
||||
display: inline-block;
|
||||
margin-left: 0.175rem;
|
||||
vertical-align: super;
|
||||
line-height: normal;
|
||||
font-size: var(--pf-global--icon--FontSize--sm);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@ -99,43 +94,34 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}>
|
||||
<span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
|
||||
</ak-page-header>
|
||||
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<!-- row 1 -->
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl pf-l-grid pf-m-gutter"
|
||||
>
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
|
||||
<ak-quick-actions-card .actions=${this.quickActions}>
|
||||
</ak-quick-actions-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-zone"
|
||||
header=${msg("Outpost status")}
|
||||
headerLink="#/outpost/outposts"
|
||||
>
|
||||
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-4-col-on-2xl"
|
||||
>
|
||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}>
|
||||
<ak-admin-status-chart-sync></ak-admin-status-chart-sync>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col">
|
||||
<hr class="pf-c-divider" />
|
||||
</div>
|
||||
${this.renderCards()}
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl">
|
||||
${this.renderCards()}
|
||||
<div class="pf-l-grid__item pf-m-9-col pf-m-3-row">
|
||||
<ak-recent-events pageSize="6"></ak-recent-events>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col">
|
||||
<hr class="pf-c-divider" />
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl">
|
||||
<ak-quick-actions-card .actions=${this.quickActions}>
|
||||
</ak-quick-actions-card>
|
||||
</div>
|
||||
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl">
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-zone"
|
||||
header=${msg("Outpost status")}
|
||||
headerLink="#/outpost/outposts"
|
||||
>
|
||||
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl">
|
||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}>
|
||||
<ak-admin-status-chart-sync></ak-admin-status-chart-sync>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
|
||||
<!-- row 3 -->
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container"
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import {
|
||||
APIError,
|
||||
parseAPIResponseError,
|
||||
pluckErrorDetail,
|
||||
} from "@goauthentik/common/errors/network";
|
||||
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@ -29,7 +34,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
|
||||
// Current error state if any request fails
|
||||
@state()
|
||||
protected error?: string;
|
||||
protected error?: APIError;
|
||||
|
||||
// Abstract methods to be implemented by subclasses
|
||||
abstract getPrimaryValue(): Promise<T>;
|
||||
@ -59,9 +64,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
this.value = value; // Triggers shouldUpdate
|
||||
this.error = undefined;
|
||||
})
|
||||
.catch((err: ResponseError) => {
|
||||
.catch(async (error) => {
|
||||
this.status = undefined;
|
||||
this.error = err?.response?.statusText ?? msg("Unknown error");
|
||||
this.error = await parseAPIResponseError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -79,9 +84,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
this.status = status;
|
||||
this.error = undefined;
|
||||
})
|
||||
.catch((err: ResponseError) => {
|
||||
.catch(async (error: ResponseError) => {
|
||||
this.status = undefined;
|
||||
this.error = err?.response?.statusText ?? msg("Unknown error");
|
||||
this.error = await parseAPIResponseError(error);
|
||||
});
|
||||
|
||||
// Prevent immediate re-render if only value changed
|
||||
@ -120,8 +125,8 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
*/
|
||||
private renderError(error: string): TemplateResult {
|
||||
return html`
|
||||
<p><i class="fa fa-times"></i> ${error}</p>
|
||||
<p class="subtext">${msg("Failed to fetch")}</p>
|
||||
<p><i class="fa fa-times"></i> ${msg("Failed to fetch")}</p>
|
||||
<p class="subtext">${error}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -146,7 +151,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
this.status
|
||||
? this.renderStatus(this.status) // Status available
|
||||
: this.error
|
||||
? this.renderError(this.error) // Error state
|
||||
? this.renderError(pluckErrorDetail(this.error)) // Error state
|
||||
: this.renderLoading() // Loading state
|
||||
}
|
||||
</p>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
|
||||
import { EventUser, formatGeoEvent } from "@goauthentik/admin/events/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EventWithContext } from "@goauthentik/common/events";
|
||||
import { actionToLabel } from "@goauthentik/common/labels";
|
||||
@ -10,6 +10,7 @@ import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
@ -38,6 +39,22 @@ export class RecentEventsCard extends Table<Event> {
|
||||
return super.styles.concat(
|
||||
PFCard,
|
||||
css`
|
||||
.pf-c-table__sort.pf-m-selected {
|
||||
background-color: var(--pf-global--BackgroundColor--dark-400);
|
||||
border-block-end: var(--pf-global--BorderWidth--xl) solid var(--ak-accent);
|
||||
|
||||
.pf-c-table__button {
|
||||
--pf-c-table__sort__button__text--Color: var(--ak-accent);
|
||||
color: var(--pf-c-nav__link--m-current--Color);
|
||||
|
||||
.pf-c-table__text {
|
||||
--pf-c-table__sort__button__text--Color: var(
|
||||
--pf-c-nav__link--m-current--Color
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-card__title {
|
||||
--pf-c-card__title--FontFamily: var(
|
||||
--pf-global--FontFamily--heading--sans-serif
|
||||
@ -45,7 +62,47 @@ export class RecentEventsCard extends Table<Event> {
|
||||
--pf-c-card__title--FontSize: var(--pf-global--FontSize--md);
|
||||
--pf-c-card__title--FontWeight: var(--pf-global--FontWeight--bold);
|
||||
}
|
||||
* {
|
||||
|
||||
td[role="cell"] .ip-address {
|
||||
max-width: 18ch;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
th[role="columnheader"]:nth-child(3) {
|
||||
--pf-c-table--cell--MinWidth: fit-content;
|
||||
--pf-c-table--cell--MaxWidth: none;
|
||||
--pf-c-table--cell--Width: 1%;
|
||||
--pf-c-table--cell--Overflow: visible;
|
||||
--pf-c-table--cell--TextOverflow: clip;
|
||||
--pf-c-table--cell--WhiteSpace: nowrap;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
font-weight: var(--pf-global--FontWeight--bold);
|
||||
font-size: var(--pf-global--FontSize--md);
|
||||
font-variant: all-petite-caps;
|
||||
}
|
||||
|
||||
.pf-c-table thead:not(:first-child) {
|
||||
background: hsl(0deg 0% 0% / 10%);
|
||||
|
||||
> tr {
|
||||
border-block-end: 2px solid
|
||||
var(
|
||||
--pf-c-page__header-tools--c-button--m-selected--before--BackgroundColor
|
||||
);
|
||||
font-family: var(--pf-global--FontFamily--heading--sans-serif);
|
||||
}
|
||||
}
|
||||
|
||||
tbody * {
|
||||
word-break: break-all;
|
||||
}
|
||||
`,
|
||||
@ -68,20 +125,57 @@ export class RecentEventsCard extends Table<Event> {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
row(item: EventWithContext): TemplateResult[] {
|
||||
override groupBy(items: Event[]): [SlottedTemplateResult, Event[]][] {
|
||||
const groupedByDay = new Map<string, Event[]>();
|
||||
|
||||
for (const item of items) {
|
||||
const day = new Date(item.created);
|
||||
day.setHours(0, 0, 0, 0);
|
||||
const serializedDay = day.toISOString();
|
||||
|
||||
let dayEvents = groupedByDay.get(serializedDay);
|
||||
if (!dayEvents) {
|
||||
dayEvents = [];
|
||||
groupedByDay.set(serializedDay, dayEvents);
|
||||
}
|
||||
|
||||
dayEvents.push(item);
|
||||
}
|
||||
|
||||
return Array.from(groupedByDay, ([serializedDay, events]) => {
|
||||
const day = new Date(serializedDay);
|
||||
return [
|
||||
html` <div class="pf-c-content group-header">
|
||||
<div>${getRelativeTime(day)}</div>
|
||||
<small>${day.toLocaleDateString()}</small>
|
||||
</div>`,
|
||||
events,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
row(item: EventWithContext): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div>
|
||||
<small>${item.app}</small>`,
|
||||
<small class="pf-m-monospace">${item.app}</small>`,
|
||||
EventUser(item),
|
||||
html`<div>${getRelativeTime(item.created)}</div>
|
||||
<small>${item.created.toLocaleString()}</small>`,
|
||||
html` <div>${item.clientIp || msg("-")}</div>
|
||||
<small>${EventGeo(item)}</small>`,
|
||||
|
||||
html`<time datetime="${item.created.toISOString()}" class="pf-c-content">
|
||||
<div><small>${item.created.toLocaleTimeString()}</small></div>
|
||||
</time>`,
|
||||
|
||||
html`<div class="ip-address pf-m-monospace">${item.clientIp || msg("-")}</div>
|
||||
<small class="geographic-location">${formatGeoEvent(item)}</small>`,
|
||||
|
||||
html`<span>${item.brand?.name || msg("-")}</span>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderEmpty(): TemplateResult {
|
||||
renderEmpty(inner?: SlottedTemplateResult): TemplateResult {
|
||||
if (this.error) {
|
||||
return super.renderEmpty(inner);
|
||||
}
|
||||
|
||||
return super.renderEmpty(
|
||||
html`<ak-empty-state header=${msg("No Events found.")}>
|
||||
<div slot="body">${msg("No matching events could be found.")}</div>
|
||||
|
@ -30,11 +30,13 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
||||
const api = new OutpostsApi(DEFAULT_CONFIG);
|
||||
const outposts = await api.outpostsInstancesList({});
|
||||
const outpostStats: SummarizedSyncStatus[] = [];
|
||||
|
||||
await Promise.all(
|
||||
outposts.results.map(async (element) => {
|
||||
const health = await api.outpostsInstancesHealthList({
|
||||
uuid: element.pk || "",
|
||||
});
|
||||
|
||||
const singleStats: SummarizedSyncStatus = {
|
||||
unsynced: 0,
|
||||
healthy: 0,
|
||||
@ -42,9 +44,11 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
||||
total: health.length,
|
||||
label: element.name,
|
||||
};
|
||||
|
||||
if (health.length === 0) {
|
||||
singleStats.unsynced += 1;
|
||||
}
|
||||
|
||||
health.forEach((h) => {
|
||||
if (h.versionOutdated) {
|
||||
singleStats.failed += 1;
|
||||
@ -52,11 +56,14 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
||||
singleStats.healthy += 1;
|
||||
}
|
||||
});
|
||||
|
||||
outpostStats.push(singleStats);
|
||||
}),
|
||||
);
|
||||
|
||||
this.centerText = outposts.pagination.count.toString();
|
||||
outpostStats.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
return outpostStats;
|
||||
}
|
||||
|
||||
|
@ -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 .content=${MDApplication}></ak-markdown>
|
||||
<ak-markdown .md=${MDApplication} meta="applications/index.md"></ak-markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@ -14,9 +14,9 @@ import { property, query } from "lit/decorators.js";
|
||||
import { ValidationError } from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
ApplicationTransactionValidationError,
|
||||
type ApplicationWizardState,
|
||||
type ApplicationWizardStateUpdate,
|
||||
ExtendedValidationError,
|
||||
} from "./types";
|
||||
|
||||
export class ApplicationWizardStep extends WizardStep {
|
||||
@ -48,7 +48,7 @@ export class ApplicationWizardStep extends WizardStep {
|
||||
}
|
||||
|
||||
protected removeErrors(
|
||||
keyToDelete: keyof ExtendedValidationError,
|
||||
keyToDelete: keyof ApplicationTransactionValidationError,
|
||||
): ValidationError | undefined {
|
||||
if (!this.wizard.errors) {
|
||||
return undefined;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { parseAPIError } from "@goauthentik/common/errors";
|
||||
import { parseAPIResponseError } from "@goauthentik/common/errors/network";
|
||||
import { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js";
|
||||
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
@ -33,7 +33,7 @@ import {
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
||||
import { ExtendedValidationError, OneOfProvider } from "../types.js";
|
||||
import { ApplicationTransactionValidationError, OneOfProvider } from "../types.js";
|
||||
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
|
||||
|
||||
const _submitStates = ["reviewing", "running", "submitted"] as const;
|
||||
@ -131,39 +131,36 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
|
||||
this.state = "running";
|
||||
|
||||
return (
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTransactionalApplicationsUpdate({
|
||||
transactionApplicationRequest: request,
|
||||
})
|
||||
.then((_response: TransactionApplicationResponse) => {
|
||||
this.dispatchCustomEvent(EVENT_REFRESH);
|
||||
this.state = "submitted";
|
||||
})
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTransactionalApplicationsUpdate({
|
||||
transactionApplicationRequest: request,
|
||||
})
|
||||
.then((_response: TransactionApplicationResponse) => {
|
||||
this.dispatchCustomEvent(EVENT_REFRESH);
|
||||
this.state = "submitted";
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.catch(async (resolution: any) => {
|
||||
const errors = (await parseAPIError(
|
||||
await resolution,
|
||||
)) as ExtendedValidationError;
|
||||
.catch(async (resolution) => {
|
||||
const errors =
|
||||
await parseAPIResponseError<ApplicationTransactionValidationError>(resolution);
|
||||
|
||||
// THIS is a really gross special case; if the user is duplicating the name of
|
||||
// an existing provider, the error appears on the `app` (!) error object. We
|
||||
// have to move that to the `provider.name` error field so it shows up in the
|
||||
// right place.
|
||||
if (Array.isArray(errors?.app?.provider)) {
|
||||
const providerError = errors.app.provider;
|
||||
errors.provider = errors.provider ?? {};
|
||||
errors.provider.name = providerError;
|
||||
delete errors.app.provider;
|
||||
if (Object.keys(errors.app).length === 0) {
|
||||
delete errors.app;
|
||||
}
|
||||
// THIS is a really gross special case; if the user is duplicating the name of an existing provider, the error appears on the `app` (!) error object.
|
||||
// We have to move that to the `provider.name` error field so it shows up in the right place.
|
||||
if (Array.isArray(errors?.app?.provider)) {
|
||||
const providerError = errors.app.provider;
|
||||
errors.provider = errors.provider ?? {};
|
||||
errors.provider.name = providerError;
|
||||
|
||||
delete errors.app.provider;
|
||||
|
||||
if (Object.keys(errors.app).length === 0) {
|
||||
delete errors.app;
|
||||
}
|
||||
this.handleUpdate({ errors });
|
||||
this.state = "reviewing";
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.handleUpdate({ errors });
|
||||
this.state = "reviewing";
|
||||
});
|
||||
}
|
||||
|
||||
override handleButton(button: WizardButton) {
|
||||
@ -232,7 +229,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
const navTo = (step: string) => () => this.dispatchEvent(new WizardNavigationEvent(step));
|
||||
const errors = this.wizard.errors;
|
||||
return html` <hr class="pf-c-divider" />
|
||||
${match(errors as ExtendedValidationError)
|
||||
${match(errors as ApplicationTransactionValidationError)
|
||||
.with(
|
||||
{ app: P.nonNullable },
|
||||
() =>
|
||||
|
@ -9,7 +9,7 @@ import { customElement, state } from "lit/decorators.js";
|
||||
import { OAuth2ProviderRequest, SourcesApi } from "@goauthentik/api";
|
||||
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
||||
|
||||
import { ExtendedValidationError } from "../../types.js";
|
||||
import { ApplicationTransactionValidationError } from "../../types.js";
|
||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||
|
||||
@customElement("ak-application-wizard-provider-for-oauth")
|
||||
@ -34,7 +34,7 @@ export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProvid
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(provider: OAuth2Provider, errors: ExtendedValidationError) {
|
||||
renderForm(provider: OAuth2Provider, errors: ApplicationTransactionValidationError) {
|
||||
const showClientSecretCallback = (show: boolean) => {
|
||||
this.showClientSecret = show;
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { APIError } from "@goauthentik/common/errors/network";
|
||||
|
||||
import {
|
||||
type ApplicationRequest,
|
||||
type LDAPProviderRequest,
|
||||
@ -25,16 +27,31 @@ export type OneOfProvider =
|
||||
|
||||
export type ValidationRecord = { [key: string]: string[] };
|
||||
|
||||
// TODO: Elf, extend this type and apply it to every object in the wizard. Then run
|
||||
// the type-checker again.
|
||||
|
||||
export type ExtendedValidationError = ValidationError & {
|
||||
/**
|
||||
* An error that occurs during the creation or modification of an application.
|
||||
*
|
||||
* @todo (Elf) Extend this type to include all possible errors that can occur during the creation or modification of an application.
|
||||
*/
|
||||
export interface ApplicationTransactionValidationError extends ValidationError {
|
||||
app?: ValidationRecord;
|
||||
provider?: ValidationRecord;
|
||||
bindings?: ValidationRecord;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
detail?: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-guard to determine if an API response is shaped like an {@linkcode ApplicationTransactionValidationError}.
|
||||
*/
|
||||
export function isApplicationTransactionValidationError(
|
||||
error: APIError,
|
||||
): error is ApplicationTransactionValidationError {
|
||||
if ("app" in error) return true;
|
||||
if ("provider" in error) return true;
|
||||
if ("bindings" in error) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// We use the PolicyBinding instead of the PolicyBindingRequest here, because that gives us a slot
|
||||
// in which to preserve the retrieved policy, group, or user object from the SearchSelect used to
|
||||
@ -49,7 +66,7 @@ export interface ApplicationWizardState {
|
||||
proxyMode: ProxyMode;
|
||||
bindings: PolicyBinding[];
|
||||
currentBinding: number;
|
||||
errors: ExtendedValidationError;
|
||||
errors: ApplicationTransactionValidationError;
|
||||
}
|
||||
|
||||
export interface ApplicationWizardStateUpdate {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import "@goauthentik/admin/events/EventVolumeChart";
|
||||
import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
|
||||
import { EventUser, formatGeoEvent } from "@goauthentik/admin/events/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EventWithContext } from "@goauthentik/common/events";
|
||||
import { actionToLabel } from "@goauthentik/common/labels";
|
||||
@ -80,7 +80,7 @@ export class EventListPage extends TablePage<Event> {
|
||||
html`<div>${getRelativeTime(item.created)}</div>
|
||||
<small>${item.created.toLocaleString()}</small>`,
|
||||
html`<div>${item.clientIp || msg("-")}</div>
|
||||
<small>${EventGeo(item)}</small>`,
|
||||
<small>${formatGeoEvent(item)}</small>`,
|
||||
html`<span>${item.brand?.name || msg("-")}</span>`,
|
||||
html`<a href="#/events/log/${item.pk}">
|
||||
<pf-tooltip position="top" content=${msg("Show details")}>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
|
||||
import { EventUser, formatGeoEvent } from "@goauthentik/admin/events/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EventWithContext } from "@goauthentik/common/events";
|
||||
import { actionToLabel } from "@goauthentik/common/labels";
|
||||
@ -118,7 +118,7 @@ export class EventViewPage extends AKElement {
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<div>${this.event.clientIp || msg("-")}</div>
|
||||
<small>${EventGeo(this.event)}</small>
|
||||
<small>${formatGeoEvent(this.event)}</small>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
@ -1,27 +1,31 @@
|
||||
import { EventWithContext } from "@goauthentik/common/events";
|
||||
import { truncate } from "@goauthentik/common/utils";
|
||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
export function EventGeo(event: EventWithContext): TemplateResult {
|
||||
let geo: KeyUnknown | undefined = undefined;
|
||||
if (Object.hasOwn(event.context, "geo")) {
|
||||
geo = event.context.geo as KeyUnknown;
|
||||
const parts = [geo.city, geo.country, geo.continent].filter(
|
||||
(v) => v !== "" && v !== undefined,
|
||||
);
|
||||
return html`${parts.join(", ")}`;
|
||||
}
|
||||
return html``;
|
||||
/**
|
||||
* Given event with a geographical context, format it into a string for display.
|
||||
*/
|
||||
export function formatGeoEvent(event: EventWithContext): SlottedTemplateResult {
|
||||
if (!event.context.geo) return nothing;
|
||||
|
||||
const { city, country, continent } = event.context.geo;
|
||||
|
||||
const parts = [city, country, continent].filter(Boolean);
|
||||
|
||||
return html`${parts.join(", ")}`;
|
||||
}
|
||||
|
||||
export function EventUser(event: EventWithContext, truncateUsername?: number): TemplateResult {
|
||||
if (!event.user.username) {
|
||||
return html`-`;
|
||||
}
|
||||
let body = html``;
|
||||
export function EventUser(
|
||||
event: EventWithContext,
|
||||
truncateUsername?: number,
|
||||
): SlottedTemplateResult {
|
||||
if (!event.user.username) return html`-`;
|
||||
|
||||
let body: SlottedTemplateResult = nothing;
|
||||
|
||||
if (event.user.is_anonymous) {
|
||||
body = html`<div>${msg("Anonymous user")}</div>`;
|
||||
} else {
|
||||
@ -33,12 +37,14 @@ export function EventUser(event: EventWithContext, truncateUsername?: number): T
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (event.user.on_behalf_of) {
|
||||
body = html`${body}<small>
|
||||
return html`${body}<small>
|
||||
<a href="#/identity/users/${event.user.on_behalf_of.pk}"
|
||||
>${msg(str`On behalf of ${event.user.on_behalf_of.username}`)}</a
|
||||
>
|
||||
</small>`;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
||||
import "@goauthentik/components/ak-status-label";
|
||||
import "@goauthentik/elements/events/LogViewer";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
|
@ -221,7 +221,7 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text pf-m-monospace">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.provider.clientId}
|
||||
</div>
|
||||
</dd>
|
||||
@ -236,9 +236,7 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
<div class="pf-c-description-list__text">
|
||||
<ul>
|
||||
${this.provider.redirectUris.map((ru) => {
|
||||
return html`<li class="pf-m-monospace">
|
||||
${ru.matchingMode}: ${ru.url}
|
||||
</li>`;
|
||||
return html`<li>${ru.matchingMode}: ${ru.url}</li>`;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
@ -358,7 +356,6 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown
|
||||
.content=${MDProviderOAuth2}
|
||||
.replacers=${[
|
||||
(input: string) => {
|
||||
if (!this.provider) {
|
||||
@ -370,6 +367,8 @@ 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
|
||||
.content=${MDHeaderAuthentication}
|
||||
.md=${MDHeaderAuthentication}
|
||||
meta="proxy/header_authentication.md"
|
||||
></ak-markdown>
|
||||
</div>
|
||||
|
@ -245,41 +245,6 @@ 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"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
@ -24,7 +24,6 @@ export class SCIMProviderGroupList extends Table<SCIMProviderGroup> {
|
||||
return true;
|
||||
}
|
||||
|
||||
expandable = true;
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
|
||||
@ -82,13 +81,6 @@ 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,7 +24,6 @@ export class SCIMProviderUserList extends Table<SCIMProviderUser> {
|
||||
return true;
|
||||
}
|
||||
|
||||
expandable = true;
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
|
||||
@ -83,13 +82,6 @@ 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
|
||||
.content=${MDSCIMProvider}
|
||||
.md=${MDSCIMProvider}
|
||||
meta="providers/scim/index.md"
|
||||
></ak-markdown>
|
||||
</div>
|
||||
|
@ -62,11 +62,7 @@ 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"
|
||||
required
|
||||
>
|
||||
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.signingKey ?? undefined)}
|
||||
|
@ -137,13 +137,10 @@ 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 pf-m-monospace"
|
||||
class="pf-c-form-control"
|
||||
readonly
|
||||
type="text"
|
||||
value=${this.provider.ssfUrl || ""}
|
||||
placeholder=${this.provider.ssfUrl
|
||||
? msg("SSF URL")
|
||||
: msg("No assigned application")}
|
||||
/>
|
||||
</div>
|
||||
</dd>
|
||||
|
@ -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
|
||||
.content=${MDSourceKerberosBrowser}
|
||||
.md=${MDSourceKerberosBrowser}
|
||||
meta="users-sources/protocols/kerberos/browser.md"
|
||||
;
|
||||
></ak-markdown>
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { parseAPIError } from "@goauthentik/common/errors";
|
||||
import {
|
||||
containsNonFieldErrors,
|
||||
parseAPIResponseError,
|
||||
pluckErrorDetail,
|
||||
} from "@goauthentik/common/errors/network";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
@ -17,14 +21,7 @@ import { map } from "lit/directives/map.js";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
|
||||
import {
|
||||
Prompt,
|
||||
PromptChallenge,
|
||||
PromptTypeEnum,
|
||||
ResponseError,
|
||||
StagesApi,
|
||||
ValidationError,
|
||||
} from "@goauthentik/api";
|
||||
import { Prompt, PromptChallenge, PromptTypeEnum, StagesApi } from "@goauthentik/api";
|
||||
|
||||
class PreviewStageHost implements StageHost {
|
||||
challenge = undefined;
|
||||
@ -78,15 +75,22 @@ export class PromptForm extends ModelForm<Prompt, string> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
this.preview = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsPreviewCreate({
|
||||
|
||||
return new StagesApi(DEFAULT_CONFIG)
|
||||
.stagesPromptPromptsPreviewCreate({
|
||||
promptRequest: prompt,
|
||||
})
|
||||
.then((nextPreview) => {
|
||||
this.preview = nextPreview;
|
||||
this.previewError = undefined;
|
||||
})
|
||||
.catch(async (error) => {
|
||||
const parsedError = await parseAPIResponseError(error);
|
||||
|
||||
this.previewError = containsNonFieldErrors(parsedError)
|
||||
? error.nonFieldErrors
|
||||
: [pluckErrorDetail(parsedError, msg("Failed to preview prompt"))];
|
||||
});
|
||||
this.previewError = undefined;
|
||||
} catch (exc) {
|
||||
const errorMessage = parseAPIError(exc as ResponseError);
|
||||
this.previewError = (errorMessage as ValidationError).nonFieldErrors;
|
||||
}
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||
import { deviceTypeName } from "@goauthentik/common/labels";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
||||
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
|
@ -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.2";
|
||||
export const VERSION = "2025.2.1";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -1,36 +0,0 @@
|
||||
import {
|
||||
GenericError,
|
||||
GenericErrorFromJSON,
|
||||
ResponseError,
|
||||
ValidationError,
|
||||
ValidationErrorFromJSON,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
export class SentryIgnoredError extends Error {}
|
||||
export class NotFoundError extends Error {}
|
||||
export class RequestError extends Error {}
|
||||
|
||||
export type APIErrorTypes = ValidationError | GenericError;
|
||||
|
||||
export const HTTP_BAD_REQUEST = 400;
|
||||
export const HTTP_INTERNAL_SERVICE_ERROR = 500;
|
||||
|
||||
export async function parseAPIError(error: Error): Promise<APIErrorTypes> {
|
||||
if (!(error instanceof ResponseError)) {
|
||||
return error;
|
||||
}
|
||||
if (
|
||||
error.response.status < HTTP_BAD_REQUEST ||
|
||||
error.response.status >= HTTP_INTERNAL_SERVICE_ERROR
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
const body = await error.response.json();
|
||||
if (error.response.status === 400) {
|
||||
return ValidationErrorFromJSON(body);
|
||||
}
|
||||
if (error.response.status === 403) {
|
||||
return GenericErrorFromJSON(body);
|
||||
}
|
||||
return body;
|
||||
}
|
194
web/src/common/errors/network.ts
Normal file
194
web/src/common/errors/network.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import {
|
||||
GenericError,
|
||||
GenericErrorFromJSON,
|
||||
ResponseError,
|
||||
ValidationError,
|
||||
ValidationErrorFromJSON,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
//#region HTTP
|
||||
|
||||
/**
|
||||
* Common HTTP status names used in the API and their corresponding codes.
|
||||
*/
|
||||
export const HTTPStatusCode = {
|
||||
BadRequest: 400,
|
||||
Forbidden: 403,
|
||||
InternalServiceError: 500,
|
||||
} as const satisfies Record<string, number>;
|
||||
|
||||
export type HTTPStatusCode = (typeof HTTPStatusCode)[keyof typeof HTTPStatusCode];
|
||||
|
||||
export type HTTPErrorJSONTransformer<T = unknown> = (json: T) => APIError;
|
||||
|
||||
export const HTTPStatusCodeTransformer: Record<number, HTTPErrorJSONTransformer> = {
|
||||
[HTTPStatusCode.BadRequest]: ValidationErrorFromJSON,
|
||||
[HTTPStatusCode.Forbidden]: GenericErrorFromJSON,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Type guard to check if a response contains a JSON body.
|
||||
*
|
||||
* This is useful to guard against parsing errors when attempting to read the response body.
|
||||
*/
|
||||
export function isJSONResponse(response: Response): boolean {
|
||||
return Boolean(response.headers.get("content-type")?.includes("application/json"));
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region API
|
||||
|
||||
/**
|
||||
* An API response error, typically derived from a {@linkcode Response} body.
|
||||
*
|
||||
* @see {@linkcode parseAPIResponseError}
|
||||
*/
|
||||
export type APIError = ValidationError | GenericError;
|
||||
|
||||
/**
|
||||
* Given an error-like object, attempts to normalize it into a {@linkcode GenericError}
|
||||
* suitable for display to the user.
|
||||
*/
|
||||
export function createSyntheticGenericError(detail?: string): GenericError {
|
||||
const syntheticGenericError: GenericError = {
|
||||
detail: detail || ResponseErrorMessages[HTTPStatusCode.InternalServiceError].reason,
|
||||
};
|
||||
|
||||
return syntheticGenericError;
|
||||
}
|
||||
|
||||
/**
|
||||
* An error that contains a native response object.
|
||||
*
|
||||
* @see {@linkcode isResponseErrorLike} to determine if an error contains a response object.
|
||||
*/
|
||||
export type APIErrorWithResponse = Pick<ResponseError, "response" | "message">;
|
||||
|
||||
/**
|
||||
* Type guard to check if an error contains a HTTP {@linkcode Response} object.
|
||||
*
|
||||
* @see {@linkcode parseAPIError} to parse the response body into a {@linkcode APIError}.
|
||||
*/
|
||||
export function isResponseErrorLike(errorLike: unknown): errorLike is APIErrorWithResponse {
|
||||
if (!errorLike || typeof errorLike !== "object") return false;
|
||||
|
||||
return "response" in errorLike && errorLike.response instanceof Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an error contains non-field errors.
|
||||
*
|
||||
* This is a reasonable heuristic to determine if an error is a {@linkcode ValidationError}.
|
||||
*
|
||||
* @see {@linkcode parseAPIError} to parse the response body into a {@linkcode APIError}.
|
||||
*/
|
||||
export function containsNonFieldErrors(error: APIError): error is ValidationError {
|
||||
return "non_field_errors" in error;
|
||||
}
|
||||
/**
|
||||
* A descriptor to provide a human readable error message for a given HTTP status code.
|
||||
*
|
||||
* @see {@linkcode ResponseErrorMessages} for a list of fallback error messages.
|
||||
*/
|
||||
interface ResponseErrorDescriptor {
|
||||
headline: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback error messages for HTTP status codes used when a more specific error message is not available in the response.
|
||||
*/
|
||||
export const ResponseErrorMessages: Record<number, ResponseErrorDescriptor> = {
|
||||
[HTTPStatusCode.BadRequest]: {
|
||||
headline: "Bad request",
|
||||
reason: "The server did not understand the request",
|
||||
},
|
||||
[HTTPStatusCode.InternalServiceError]: {
|
||||
headline: "Internal server error",
|
||||
reason: "An unexpected error occurred",
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Composes a human readable error message from a {@linkcode ResponseErrorDescriptor}.
|
||||
*
|
||||
* Note that this is kept separate from localization to lower the complexity of the error handling code.
|
||||
*/
|
||||
export function composeResponseErrorDescriptor(descriptor: ResponseErrorDescriptor): string {
|
||||
return `${descriptor.headline}: ${descriptor.reason}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to pluck a human readable error message from a {@linkcode ValidationError}.
|
||||
*/
|
||||
export function pluckErrorDetail(validationError: ValidationError, fallback?: string): string;
|
||||
/**
|
||||
* Attempts to pluck a human readable error message from a {@linkcode GenericError}.
|
||||
*/
|
||||
export function pluckErrorDetail(genericError: GenericError, fallback?: string): string;
|
||||
/**
|
||||
* Attempts to pluck a human readable error message from an `Error` object.
|
||||
*/
|
||||
export function pluckErrorDetail(error: Error, fallback?: string): string;
|
||||
/**
|
||||
* Attempts to pluck a human readable error message from an error-like object.
|
||||
*
|
||||
* Prioritizes the `detail` key, then the `message` key.
|
||||
*
|
||||
*/
|
||||
export function pluckErrorDetail(errorLike: unknown, fallback?: string): string;
|
||||
export function pluckErrorDetail(errorLike: unknown, fallback?: string): string {
|
||||
fallback ||= composeResponseErrorDescriptor(
|
||||
ResponseErrorMessages[HTTPStatusCode.InternalServiceError],
|
||||
);
|
||||
|
||||
if (!errorLike || typeof errorLike !== "object") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if ("detail" in errorLike && typeof errorLike.detail === "string") {
|
||||
return errorLike.detail;
|
||||
}
|
||||
|
||||
if ("message" in errorLike && typeof errorLike.message === "string") {
|
||||
return errorLike.message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given API error, parses the response body and transforms it into a {@linkcode APIError}.
|
||||
*/
|
||||
export async function parseAPIResponseError<T extends APIError = APIError>(
|
||||
error: unknown,
|
||||
): Promise<T> {
|
||||
if (!isResponseErrorLike(error)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
return createSyntheticGenericError(message) as T;
|
||||
}
|
||||
|
||||
const { response, message } = error;
|
||||
|
||||
if (!isJSONResponse(response)) {
|
||||
return createSyntheticGenericError(message || response.statusText) as T;
|
||||
}
|
||||
|
||||
return response
|
||||
.json()
|
||||
.then((body) => {
|
||||
const transformer = HTTPStatusCodeTransformer[response.status];
|
||||
const transformedBody = transformer ? transformer(body) : body;
|
||||
|
||||
return transformedBody as unknown as T;
|
||||
})
|
||||
.catch((transformerError) => {
|
||||
console.error("Failed to parse response error body", transformerError);
|
||||
|
||||
return createSyntheticGenericError(message || response.statusText) as T;
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
0
web/src/common/errors/sentry.ts
Normal file
0
web/src/common/errors/sentry.ts
Normal file
@ -8,13 +8,10 @@ export interface EventUser {
|
||||
is_anonymous?: boolean;
|
||||
}
|
||||
|
||||
export interface EventContext {
|
||||
[key: string]: EventContext | EventModel | string | number | string[];
|
||||
}
|
||||
|
||||
export interface EventWithContext extends Event {
|
||||
user: EventUser;
|
||||
context: EventContext;
|
||||
export interface EventGeo {
|
||||
city?: string;
|
||||
country?: string;
|
||||
continent?: string;
|
||||
}
|
||||
|
||||
export interface EventModel {
|
||||
@ -28,3 +25,13 @@ export interface EventRequest {
|
||||
path: string;
|
||||
method: string;
|
||||
}
|
||||
|
||||
export interface EventContext {
|
||||
[key: string]: EventContext | EventModel | EventGeo | string | number | string[] | undefined;
|
||||
geo?: EventGeo;
|
||||
}
|
||||
|
||||
export interface EventWithContext extends Event {
|
||||
user: EventUser;
|
||||
context: EventContext;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { VERSION } from "@goauthentik/common/constants";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
||||
|
||||
export interface PlexPinResponse {
|
||||
// Only has the fields we care about
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { config } from "@goauthentik/common/api/config";
|
||||
import { VERSION } from "@goauthentik/common/constants";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
import {
|
||||
ErrorEvent,
|
||||
@ -16,69 +15,85 @@ import { CapabilitiesEnum, Config, ResponseError } from "@goauthentik/api";
|
||||
export const TAG_SENTRY_COMPONENT = "authentik.component";
|
||||
export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities";
|
||||
|
||||
export async function configureSentry(canDoPpi = false): Promise<Config> {
|
||||
const cfg = await config();
|
||||
if (cfg.errorReporting.enabled) {
|
||||
init({
|
||||
dsn: cfg.errorReporting.sentryDsn,
|
||||
ignoreErrors: [
|
||||
/network/gi,
|
||||
/fetch/gi,
|
||||
/module/gi,
|
||||
// Error on edge on ios,
|
||||
// https://stackoverflow.com/questions/69261499/what-is-instantsearchsdkjsbridgeclearhighlight
|
||||
/instantSearchSDKJSBridgeClearHighlight/gi,
|
||||
// Seems to be an issue in Safari and Firefox
|
||||
/MutationObserver.observe/gi,
|
||||
/NS_ERROR_FAILURE/gi,
|
||||
],
|
||||
release: `authentik@${VERSION}`,
|
||||
integrations: [
|
||||
browserTracingIntegration({
|
||||
shouldCreateSpanForRequest: (url: string) => {
|
||||
return url.startsWith(window.location.host);
|
||||
},
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: cfg.errorReporting.tracesSampleRate,
|
||||
environment: cfg.errorReporting.environment,
|
||||
beforeSend: (
|
||||
event: ErrorEvent,
|
||||
hint: EventHint,
|
||||
): ErrorEvent | PromiseLike<ErrorEvent | null> | null => {
|
||||
if (!hint) {
|
||||
return event;
|
||||
}
|
||||
if (hint.originalException instanceof SentryIgnoredError) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
hint.originalException instanceof ResponseError ||
|
||||
hint.originalException instanceof DOMException
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
});
|
||||
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
|
||||
if (window.location.pathname.includes("if/")) {
|
||||
setTag(TAG_SENTRY_COMPONENT, `web/${currentInterface()}`);
|
||||
}
|
||||
if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
||||
const Spotlight = await import("@spotlightjs/spotlight");
|
||||
/**
|
||||
* A generic error that can be thrown without triggering Sentry's reporting.
|
||||
*/
|
||||
export class SentryIgnoredError extends Error {}
|
||||
|
||||
Spotlight.init({ injectImmediately: true });
|
||||
}
|
||||
if (cfg.errorReporting.sendPii && canDoPpi) {
|
||||
me().then((user) => {
|
||||
setUser({ email: user.user.email });
|
||||
console.debug("authentik/config: Sentry with PII enabled.");
|
||||
});
|
||||
} else {
|
||||
console.debug("authentik/config: Sentry enabled.");
|
||||
}
|
||||
/**
|
||||
* Configure Sentry with the given configuration.
|
||||
*
|
||||
* @param canSendPII Whether the user can send personally identifiable information.
|
||||
*/
|
||||
export async function configureSentry(canSendPII = false): Promise<Config> {
|
||||
const cfg = await config();
|
||||
|
||||
if (!cfg.errorReporting.enabled) return cfg;
|
||||
|
||||
init({
|
||||
dsn: cfg.errorReporting.sentryDsn,
|
||||
ignoreErrors: [
|
||||
/network/gi,
|
||||
/fetch/gi,
|
||||
/module/gi,
|
||||
// Error on edge on ios,
|
||||
// https://stackoverflow.com/questions/69261499/what-is-instantsearchsdkjsbridgeclearhighlight
|
||||
/instantSearchSDKJSBridgeClearHighlight/gi,
|
||||
// Seems to be an issue in Safari and Firefox
|
||||
/MutationObserver.observe/gi,
|
||||
/NS_ERROR_FAILURE/gi,
|
||||
],
|
||||
release: `authentik@${VERSION}`,
|
||||
integrations: [
|
||||
browserTracingIntegration({
|
||||
shouldCreateSpanForRequest: (url: string) => {
|
||||
return url.startsWith(window.location.host);
|
||||
},
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: cfg.errorReporting.tracesSampleRate,
|
||||
environment: cfg.errorReporting.environment,
|
||||
beforeSend: (
|
||||
event: ErrorEvent,
|
||||
hint: EventHint,
|
||||
): ErrorEvent | PromiseLike<ErrorEvent | null> | null => {
|
||||
if (!hint) {
|
||||
return event;
|
||||
}
|
||||
if (hint.originalException instanceof SentryIgnoredError) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
hint.originalException instanceof ResponseError ||
|
||||
hint.originalException instanceof DOMException
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
});
|
||||
|
||||
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
|
||||
|
||||
if (window.location.pathname.includes("if/")) {
|
||||
setTag(TAG_SENTRY_COMPONENT, `web/${currentInterface()}`);
|
||||
}
|
||||
|
||||
if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
||||
const Spotlight = await import("@spotlightjs/spotlight");
|
||||
|
||||
Spotlight.init({ injectImmediately: true });
|
||||
}
|
||||
|
||||
if (cfg.errorReporting.sendPii && canSendPII) {
|
||||
await me().then((user) => {
|
||||
setUser({ email: user.user.email });
|
||||
console.debug("authentik/config: Sentry with PII enabled.");
|
||||
});
|
||||
} else {
|
||||
console.debug("authentik/config: Sentry enabled.");
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
/* #region Global */
|
||||
|
||||
:root {
|
||||
--ak-accent: #fd4b2d;
|
||||
|
||||
@ -47,44 +45,7 @@ html > form > input {
|
||||
left: -2000px;
|
||||
}
|
||||
|
||||
/* #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 */
|
||||
/*#region Icons*/
|
||||
|
||||
.pf-icon {
|
||||
display: inline-block;
|
||||
@ -95,6 +56,19 @@ a:active {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.pf-c-card__title {
|
||||
.pf-icon:first-child,
|
||||
.fa:first-child {
|
||||
margin-inline-end: var(--pf-global--spacer--sm);
|
||||
}
|
||||
}
|
||||
|
||||
a > .fas.fa-external-link-alt {
|
||||
margin-inline-start: var(--pf-global--spacer--xs);
|
||||
font-size: var(--pf-global--FontSize--sm);
|
||||
transform: translateY(-0.1em);
|
||||
}
|
||||
|
||||
.pf-c-form-control {
|
||||
--pf-c-form-control--m-caps-lock--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23aaabac' viewBox='0 0 56 56' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M 20.7812 37.6211 L 35.2421 37.6211 C 38.5233 37.6211 40.2577 35.6992 40.2577 32.6055 L 40.2577 28.4570 L 49.1404 28.4570 C 51.0859 28.4570 52.6329 27.3086 52.6329 25.5039 C 52.6329 24.4024 52.0703 23.5351 51.0158 22.6211 L 30.9062 4.8789 C 29.9452 4.0351 29.0546 3.4727 27.9999 3.4727 C 26.9687 3.4727 26.0780 4.0351 25.1171 4.8789 L 4.9843 22.6445 C 3.8828 23.6055 3.3671 24.4024 3.3671 25.5039 C 3.3671 27.3086 4.9140 28.4570 6.8828 28.4570 L 15.7421 28.4570 L 15.7421 32.6055 C 15.7421 35.6992 17.4999 37.6211 20.7812 37.6211 Z M 21.1562 34.0820 C 20.2655 34.0820 19.6562 33.4961 19.6562 32.6055 L 19.6562 25.7149 C 19.6562 25.1524 19.4452 24.9180 18.8828 24.9180 L 8.6640 24.9180 C 8.4999 24.9180 8.4296 24.8476 8.4296 24.7305 C 8.4296 24.6367 8.4530 24.5430 8.5702 24.4492 L 27.5077 7.9961 C 27.7187 7.8086 27.8359 7.7383 27.9999 7.7383 C 28.1640 7.7383 28.3046 7.8086 28.4921 7.9961 L 47.4532 24.4492 C 47.5703 24.5430 47.5939 24.6367 47.5939 24.7305 C 47.5939 24.8476 47.4998 24.9180 47.3356 24.9180 L 37.1406 24.9180 C 36.5780 24.9180 36.3671 25.1524 36.3671 25.7149 L 36.3671 32.6055 C 36.3671 33.4727 35.7109 34.0820 34.8671 34.0820 Z M 19.7733 52.5273 L 36.0624 52.5273 C 38.7577 52.5273 40.3046 51.0273 40.3046 48.3086 L 40.3046 44.9336 C 40.3046 42.2148 38.7577 40.6680 36.0624 40.6680 L 19.7733 40.6680 C 17.0546 40.6680 15.5077 42.2383 15.5077 44.9336 L 15.5077 48.3086 C 15.5077 51.0039 17.0546 52.5273 19.7733 52.5273 Z M 20.3124 49.2227 C 19.4921 49.2227 19.0468 48.8008 19.0468 47.9805 L 19.0468 45.2617 C 19.0468 44.4414 19.4921 43.9727 20.3124 43.9727 L 35.5233 43.9727 C 36.3202 43.9727 36.7655 44.4414 36.7655 45.2617 L 36.7655 47.9805 C 36.7655 48.8008 36.3202 49.2227 35.5233 49.2227 Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
@ -105,7 +79,7 @@ a:active {
|
||||
);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
/*#endregion*/
|
||||
|
||||
.pf-c-page__header {
|
||||
z-index: 0;
|
||||
@ -113,15 +87,15 @@ a:active {
|
||||
box-shadow: var(--pf-global--BoxShadow--lg-bottom);
|
||||
}
|
||||
|
||||
/* #region Login adjustments */
|
||||
|
||||
/*****************************
|
||||
* 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;
|
||||
@ -129,34 +103,30 @@ a:active {
|
||||
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;
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
/*****************************
|
||||
* End Login adjustments
|
||||
*****************************/
|
||||
|
||||
.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);
|
||||
}
|
||||
@ -170,11 +140,9 @@ a:active {
|
||||
.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);
|
||||
}
|
||||
@ -212,7 +180,6 @@ a:active {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ak-brand img {
|
||||
padding: 0 2rem;
|
||||
max-height: inherit;
|
||||
@ -227,48 +194,3 @@ a:active {
|
||||
.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 */
|
||||
|
@ -1,98 +0,0 @@
|
||||
/*
|
||||
|
||||
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,84 +1,60 @@
|
||||
/* #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);
|
||||
--pf-c-table--m-striped__tr--BackgroundColor: var(--pf-global--BackgroundColor--dark-300);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/* #region Card */
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* #region Tables */
|
||||
/* table */
|
||||
.pf-c-table {
|
||||
--pf-c-table--BackgroundColor: var(--ak-dark-background-light);
|
||||
--pf-c-table--BorderColor: var(--ak-dark-background-lighter);
|
||||
@ -86,58 +62,41 @@ 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);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Tabs */
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #Region Mobile Tables */
|
||||
/* table, on mobile */
|
||||
@media screen and (max-width: 1200px) {
|
||||
.pf-m-grid-xl.pf-c-table tbody:first-of-type {
|
||||
border-top-color: var(--ak-dark-background);
|
||||
@ -146,42 +105,32 @@ 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;
|
||||
}
|
||||
|
||||
/* #region Inputs */
|
||||
/* inputs */
|
||||
optgroup,
|
||||
option {
|
||||
color: var(--ak-dark-foreground);
|
||||
@ -190,11 +139,9 @@ 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;
|
||||
@ -203,15 +150,12 @@ 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);
|
||||
}
|
||||
@ -219,47 +163,38 @@ 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)
|
||||
@ -267,127 +202,92 @@ 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);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Modal */
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Sidebar */
|
||||
|
||||
/* sidebar */
|
||||
.pf-c-nav {
|
||||
background-color: var(--ak-dark-background-light);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Flows */
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Notifications */
|
||||
|
||||
/* 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);
|
||||
@ -395,84 +295,46 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
--ak-dark-background-lighter
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Data List */
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Wizards */
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Tree view */
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Stepper */
|
||||
/* 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 */
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
||||
|
||||
import { CSSResult, css } from "lit";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
|
||||
import { EventUser, formatGeoEvent } from "@goauthentik/admin/events/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EventWithContext } from "@goauthentik/common/events";
|
||||
import { actionToLabel } from "@goauthentik/common/labels";
|
||||
@ -76,7 +76,7 @@ export class ObjectChangelog extends Table<Event> {
|
||||
<small>${item.created.toLocaleString()}</small>`,
|
||||
html`<div>${item.clientIp || msg("-")}</div>
|
||||
|
||||
<small>${EventGeo(item)}</small>`,
|
||||
<small>${formatGeoEvent(item)}</small>`,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@ import { localized } from "@lit/localize";
|
||||
import { LitElement, ReactiveElement } from "lit";
|
||||
|
||||
import AKGlobal from "@goauthentik/common/styles/authentik.css";
|
||||
import OneDark from "@goauthentik/common/styles/one-dark.css";
|
||||
import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
|
||||
|
||||
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
|
||||
@ -71,7 +70,6 @@ export class AKElement extends LitElement {
|
||||
styleRoot.adoptedStyleSheets = adaptCSS([
|
||||
...styleRoot.adoptedStyleSheets,
|
||||
ensureCSSStyleSheet(AKGlobal),
|
||||
ensureCSSStyleSheet(OneDark),
|
||||
]);
|
||||
this._initTheme(styleRoot);
|
||||
this._initCustomCSS(styleRoot);
|
||||
|
@ -1,19 +1,30 @@
|
||||
import { docLink } from "@goauthentik/common/global";
|
||||
import "@goauthentik/elements/Alert";
|
||||
import { Level } from "@goauthentik/elements/Alert";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { matter } from "md-front-matter";
|
||||
import * as showdown from "showdown";
|
||||
|
||||
import { CSSResult, PropertyValues, css, nothing } from "lit";
|
||||
import { CSSResult, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
|
||||
export type Replacer = (input: string) => string;
|
||||
export interface MarkdownDocument {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export type Replacer = (input: string, md: MarkdownDocument) => string;
|
||||
|
||||
const isRelativeLink = /href="(\.[^"]*)"/gm;
|
||||
const isFile = /[^/]+\.md/;
|
||||
|
||||
@customElement("ak-markdown")
|
||||
export class Markdown extends AKElement {
|
||||
@property()
|
||||
content: string = "";
|
||||
md: string = "";
|
||||
|
||||
@property()
|
||||
meta: string = "";
|
||||
@ -21,7 +32,14 @@ export class Markdown extends AKElement {
|
||||
@property({ attribute: false })
|
||||
replacers: Replacer[] = [];
|
||||
|
||||
resolvedHTML = "";
|
||||
docHtml = "";
|
||||
docTitle = "";
|
||||
|
||||
defaultReplacers: Replacer[] = [
|
||||
this.replaceAdmonitions,
|
||||
this.replaceList,
|
||||
this.replaceRelativeLinks,
|
||||
];
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
@ -35,47 +53,55 @@ export class Markdown extends AKElement {
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
converter = new showdown.Converter({ metadata: true, tables: true });
|
||||
|
||||
const headingLinks =
|
||||
this.shadowRoot?.querySelectorAll<HTMLAnchorElement>("a.markdown-heading") ?? [];
|
||||
replaceAdmonitions(input: string): string {
|
||||
const admonitionStart = /:::(\w+)(<br\s*\/>|\s*$)/gm;
|
||||
const admonitionEnd = /:::/gm;
|
||||
return (
|
||||
input
|
||||
.replaceAll(admonitionStart, "<ak-alert level='pf-m-$1'>")
|
||||
.replaceAll(admonitionEnd, "</ak-alert>")
|
||||
// Workaround for admonitions using caution instead of warning
|
||||
.replaceAll("pf-m-caution", Level.Warning)
|
||||
);
|
||||
}
|
||||
|
||||
for (const headingLink of headingLinks) {
|
||||
headingLink.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
replaceList(input: string): string {
|
||||
return input.replace("<ul>", "<ul class='pf-c-list'>");
|
||||
}
|
||||
|
||||
const url = new URL(headingLink.href);
|
||||
const elementID = url.hash.slice(1);
|
||||
|
||||
const target = this.shadowRoot?.getElementById(elementID);
|
||||
|
||||
if (!target) {
|
||||
console.warn(`Element with ID ${elementID} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
});
|
||||
}
|
||||
replaceRelativeLinks(input: string, md: MarkdownDocument): string {
|
||||
const baseName = md.path.replace(isFile, "");
|
||||
const baseUrl = docLink("");
|
||||
return input.replace(isRelativeLink, (_match, path) => {
|
||||
const pathName = path.replace(".md", "");
|
||||
const link = `docs/${baseName}${pathName}`;
|
||||
const url = new URL(link, baseUrl).toString();
|
||||
return `href="${url}" _target="blank" rel="noopener noreferrer"`;
|
||||
});
|
||||
}
|
||||
|
||||
willUpdate(properties: PropertyValues<this>) {
|
||||
if (properties.has("content")) {
|
||||
this.resolvedHTML = this.replacers.reduce(
|
||||
(html, replacer) => replacer(html),
|
||||
this.content,
|
||||
if (properties.has("md") || properties.has("meta")) {
|
||||
const parsedContent = matter(this.md);
|
||||
const parsedHTML = this.converter.makeHtml(parsedContent.content);
|
||||
const replacers = [...this.defaultReplacers, ...this.replacers];
|
||||
this.docTitle = parsedContent?.data?.title ?? "";
|
||||
this.docHtml = replacers.reduce(
|
||||
(html, replacer) => replacer(html, { path: this.meta }),
|
||||
parsedHTML,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.content) return nothing;
|
||||
if (!this.md) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return unsafeHTML(this.resolvedHTML);
|
||||
return html`${this.docTitle ? html`<h2>${this.docTitle}</h2>` : nothing}
|
||||
${unsafeHTML(this.docHtml)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user