Compare commits

..

7 Commits

Author SHA1 Message Date
bbd639c37d Merge branch 'main' into better-version-bump
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	.bumpversion.cfg
#	authentik/__init__.py
#	docker-compose.yml
#	internal/constants/constants.go
2025-06-05 23:31:49 +02:00
5c131fec36 fully get rid of bumpversion
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-04 18:27:04 +02:00
a575de21bc update go version
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-04 18:24:55 +02:00
02275584a6 generate aws and fix compose
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-04 18:14:34 +02:00
27268d533c generate compose
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-04 18:08:27 +02:00
0dba4b61f5 use make and tools instead, less search&replace
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-04 18:08:27 +02:00
c4d4512818 get authentik version from importlib
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-04 18:08:27 +02:00
203 changed files with 1416 additions and 1426 deletions

View File

@ -1,36 +0,0 @@
[bumpversion]
current_version = 2025.6.0
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*))?
serialize =
{major}.{minor}.{patch}-{rc_t}{rc_n}
{major}.{minor}.{patch}
message = release: {new_version}
tag_name = version/{new_version}
[bumpversion:part:rc_t]
values =
rc
final
optional_value = final
[bumpversion:file:pyproject.toml]
[bumpversion:file:uv.lock]
[bumpversion:file:package.json]
[bumpversion:file:docker-compose.yml]
[bumpversion:file:schema.yml]
[bumpversion:file:blueprints/schema.json]
[bumpversion:file:authentik/__init__.py]
[bumpversion:file:internal/constants/constants.go]
[bumpversion:file:web/src/common/constants.ts]
[bumpversion:file:lifecycle/aws/template.yaml]

View File

@ -5,7 +5,6 @@ dist/**
build/**
build_docs/**
*Dockerfile
**/*Dockerfile
blueprints/local
.git
!gen-ts-api/node_modules

View File

@ -1,13 +1,9 @@
"""Helper script to get the actual branch name, docker safe"""
import configparser
import os
from importlib.metadata import version as package_version
from json import dumps
from time import time
parser = configparser.ConfigParser()
parser.read(".bumpversion.cfg")
# Decide if we should push the image or not
should_push = True
if len(os.environ.get("DOCKER_USERNAME", "")) < 1:
@ -31,7 +27,7 @@ is_release = "dev" not in image_names[0]
sha = os.environ["GITHUB_SHA"] if not is_pull_request else os.getenv("PR_HEAD_SHA")
# 2042.1.0 or 2042.1.0-rc1
version = parser.get("bumpversion", "current_version")
version = package_version("authentik")
# 2042.1
version_family = ".".join(version.split("-", 1)[0].split(".")[:-1])
prerelease = "-" in version

View File

@ -41,60 +41,32 @@ jobs:
- name: test
working-directory: website/
run: npm test
build-container:
build:
runs-on: ubuntu-latest
permissions:
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
name: ${{ matrix.job }}
strategy:
fail-fast: false
matrix:
job:
- build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/dev-docs
- name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
platforms: linux/amd64,linux/arm64
context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache,mode=max' || '' }}
- uses: actions/attest-build-provenance@v2
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci
- name: build
working-directory: website/
run: npm run ${{ matrix.job }}
ci-website-mark:
if: always()
needs:
- lint
- test
- build-container
- build
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1

View File

@ -20,49 +20,6 @@ jobs:
release: true
registry_dockerhub: true
registry_ghcr: true
build-docs:
runs-on: ubuntu-latest
permissions:
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/docs
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@v2
id: attest
if: true
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
build-outpost:
runs-on: ubuntu-latest
permissions:
@ -236,6 +193,6 @@ jobs:
SENTRY_ORG: authentik-security-inc
SENTRY_PROJECT: authentik
with:
release: authentik@${{ steps.ev.outputs.version }}
version: authentik@${{ steps.ev.outputs.version }}
sourcemaps: "./web/dist"
url_prefix: "~/static/dist"

View File

@ -1,7 +1,26 @@
# syntax=docker/dockerfile:1
# Stage 1: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24-slim AS node-builder
# Stage 1: Build website
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24 AS website-builder
ENV NODE_ENV=production
WORKDIR /work/website
RUN --mount=type=bind,target=/work/website/package.json,src=./website/package.json \
--mount=type=bind,target=/work/website/package-lock.json,src=./website/package-lock.json \
--mount=type=cache,id=npm-website,sharing=shared,target=/root/.npm \
npm ci --include=dev
COPY ./website /work/website/
COPY ./blueprints /work/blueprints/
COPY ./schema.yml /work/
COPY ./SECURITY.md /work/
RUN npm run build-bundled
# Stage 2: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24 AS web-builder
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
@ -13,7 +32,7 @@ RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
--mount=type=cache,id=npm-ak,sharing=shared,target=/root/.npm \
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
npm ci --include=dev
COPY ./package.json /work
@ -24,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build && \
npm run build:sfe
# Stage 2: Build go proxy
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
ARG TARGETOS
@ -49,8 +68,8 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
COPY ./cmd /go/src/goauthentik.io/cmd
COPY ./authentik/lib /go/src/goauthentik.io/authentik/lib
COPY ./web/static.go /go/src/goauthentik.io/web/static.go
COPY --from=node-builder /work/web/robots.txt /go/src/goauthentik.io/web/robots.txt
COPY --from=node-builder /work/web/security.txt /go/src/goauthentik.io/web/security.txt
COPY --from=web-builder /work/web/robots.txt /go/src/goauthentik.io/web/robots.txt
COPY --from=web-builder /work/web/security.txt /go/src/goauthentik.io/web/security.txt
COPY ./internal /go/src/goauthentik.io/internal
COPY ./go.mod /go/src/goauthentik.io/go.mod
COPY ./go.sum /go/src/goauthentik.io/go.sum
@ -61,7 +80,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
go build -o /go/authentik ./cmd/server
# Stage 3: MaxMind GeoIP
# Stage 4: MaxMind GeoIP
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
@ -74,9 +93,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
mkdir -p /usr/share/GeoIP && \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 4: Download uv
# Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.7.11 AS uv
# Stage 5: Base python image
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \
@ -90,7 +109,7 @@ WORKDIR /ak-root/
COPY --from=uv /uv /uvx /bin/
# Stage 6: Python dependencies
# Stage 7: Python dependencies
FROM python-base AS python-deps
ARG TARGETARCH
@ -125,7 +144,7 @@ RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
--mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
# Stage 7: Run
# Stage 8: Run
FROM python-base AS final-image
ARG VERSION
@ -168,8 +187,9 @@ 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=node-builder /work/web/dist/ /web/dist/
COPY --from=node-builder /work/web/authentik/ /web/authentik/
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/
COPY --from=geoip /usr/share/GeoIP /geoip
USER 1000

View File

@ -57,7 +57,7 @@ migrate: ## Run the Authentik Django server's migrations
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
cd lifecycle/aws && npm i && npm run aws-cfn
run: ## Run the main authentik server process
uv run ak server
@ -86,6 +86,15 @@ dev-create-db:
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
bump:
uv version $(version)
$(MAKE) gen-build
$(MAKE) gen-compose
$(MAKE) aws-cfn
npm version --no-git-tag-version --allow-same-version $(version)
cd ${PWD}/web && npm version --no-git-tag-version --allow-same-version $(version)
echo $(version) > ${PWD}/internal/constants/VERSION
#########################
## API Schema
#########################
@ -100,6 +109,9 @@ gen-build: ## Extract the schema from the database
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
uv run ak spectacular --file schema.yml
gen-compose:
uv run scripts/generate_docker_compose.py
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
npx prettier --write changelog.md

View File

@ -1,20 +1,28 @@
"""authentik root module"""
from functools import lru_cache
from importlib.metadata import version
from os import environ
__version__ = "2025.6.0"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
def get_build_hash(fallback: str | None = None) -> str:
@lru_cache
def authentik_version() -> str:
return version("authentik")
@lru_cache
def authentik_build_hash(fallback: str | None = None) -> str:
"""Get build hash"""
build_hash = environ.get(ENV_GIT_HASH_KEY, fallback if fallback else "")
return fallback if build_hash == "" and fallback else build_hash
def get_full_version() -> str:
@lru_cache
def authentik_full_version() -> str:
"""Get full version, with build hash appended"""
version = __version__
if (build_hash := get_build_hash()) != "":
version = authentik_version()
if (build_hash := authentik_build_hash()) != "":
return f"{version}+{build_hash}"
return version

View File

@ -16,7 +16,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.core.api.utils import PassiveSerializer
from authentik.enterprise.license import LicenseKey
from authentik.lib.config import CONFIG
@ -78,7 +78,7 @@ class SystemInfoSerializer(PassiveSerializer):
"""Get versions"""
return {
"architecture": platform.machine(),
"authentik_version": get_full_version(),
"authentik_version": authentik_full_version(),
"environment": get_env(),
"openssl_fips_enabled": (
backend._fips_enabled if LicenseKey.get_total().status().is_valid else None

View File

@ -10,7 +10,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentik import __version__, get_build_hash
from authentik import authentik_build_hash, authentik_version
from authentik.admin.tasks import VERSION_CACHE_KEY, VERSION_NULL, update_latest_version
from authentik.core.api.utils import PassiveSerializer
from authentik.outposts.models import Outpost
@ -29,11 +29,11 @@ class VersionSerializer(PassiveSerializer):
def get_build_hash(self, _) -> str:
"""Get build hash, if version is not latest or released"""
return get_build_hash()
return authentik_build_hash()
def get_version_current(self, _) -> str:
"""Get current version"""
return __version__
return authentik_version()
def get_version_latest(self, _) -> str:
"""Get latest version from cache"""
@ -42,7 +42,7 @@ class VersionSerializer(PassiveSerializer):
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache: # pragma: no cover
update_latest_version.delay()
return __version__
return authentik_version()
return version_in_cache
def get_version_latest_valid(self, _) -> bool:

View File

@ -10,7 +10,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.rbac.permissions import HasPermission
from authentik.root.celery import CELERY_APP
@ -34,7 +34,7 @@ class WorkerView(APIView):
def get(self, request: Request) -> Response:
"""Get currently connected worker count."""
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5)
our_version = parse(get_full_version())
our_version = parse(authentik_full_version())
response = []
for worker in raw:
key = list(worker.keys())[0]
@ -50,7 +50,7 @@ class WorkerView(APIView):
response.append(
{
"worker_id": f"authentik-debug@{gethostname()}",
"version": get_full_version(),
"version": authentik_full_version(),
"version_matching": True,
}
)

View File

@ -4,7 +4,7 @@ from django.dispatch import receiver
from packaging.version import parse
from prometheus_client import Gauge
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.root.celery import CELERY_APP
from authentik.root.monitoring import monitoring_set
@ -15,7 +15,7 @@ GAUGE_WORKERS = Gauge(
)
_version = parse(get_full_version())
_version = parse(authentik_full_version())
@receiver(monitoring_set)

View File

@ -6,7 +6,7 @@ from packaging.version import parse
from requests import RequestException
from structlog.stdlib import get_logger
from authentik import __version__, get_build_hash
from authentik import authentik_build_hash, authentik_version
from authentik.admin.apps import PROM_INFO
from authentik.events.models import Event, EventAction
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
@ -18,16 +18,16 @@ LOGGER = get_logger()
VERSION_NULL = "0.0.0"
VERSION_CACHE_KEY = "authentik_latest_version"
VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours
LOCAL_VERSION = parse(__version__)
LOCAL_VERSION = parse(authentik_version())
def _set_prom_info():
"""Set prometheus info for version"""
PROM_INFO.info(
{
"version": __version__,
"version": authentik_version(),
"latest": cache.get(VERSION_CACHE_KEY, ""),
"build_hash": get_build_hash(),
"build_hash": authentik_build_hash(),
}
)

View File

@ -5,7 +5,7 @@ from json import loads
from django.test import TestCase
from django.urls import reverse
from authentik import __version__
from authentik import authentik_version
from authentik.blueprints.tests import reconcile_app
from authentik.core.models import Group, User
from authentik.lib.generators import generate_id
@ -27,7 +27,7 @@ class TestAdminAPI(TestCase):
response = self.client.get(reverse("authentik_api:admin_version"))
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(body["version_current"], __version__)
self.assertEqual(body["version_current"], authentik_version())
def test_workers(self):
"""Test Workers API"""

View File

@ -11,7 +11,7 @@ from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik import __version__
from authentik import authentik_version
from authentik.blueprints.v1.common import BlueprintEntryDesiredState
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, is_model_allowed
from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry
@ -48,7 +48,7 @@ class Command(BaseCommand):
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": f"authentik {__version__} Blueprint schema",
"title": f"authentik {authentik_version()} Blueprint schema",
"required": ["version", "entries"],
"properties": {
"version": {

View File

@ -6,7 +6,7 @@ from django.db.models import F, Q
from django.db.models import Value as V
from django.http.request import HttpRequest
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.brands.models import Brand
from authentik.lib.sentry import get_http_meta
from authentik.tenants.models import Tenant
@ -36,5 +36,5 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
"brand": brand,
"footer_links": tenant.footer_links,
"html_meta": {**get_http_meta()},
"version": get_full_version(),
"version": authentik_full_version(),
}

View File

@ -153,10 +153,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
return applications
def _filter_applications_with_launch_url(
self, paginated_apps: Iterator[Application]
self, pagined_apps: Iterator[Application]
) -> list[Application]:
applications = []
for app in paginated_apps:
for app in pagined_apps:
if app.get_launch_url():
applications.append(app)
return applications

View File

@ -11,7 +11,7 @@ from django.core.management.base import BaseCommand
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.core.models import User
from authentik.events.middleware import should_log_model
from authentik.events.models import Event, EventAction
@ -19,7 +19,7 @@ from authentik.events.utils import model_to_dict
def get_banner_text(shell_type="shell") -> str:
return f"""### authentik {shell_type} ({get_full_version()})
return f"""### authentik {shell_type} ({authentik_full_version()})
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """

View File

@ -10,7 +10,7 @@
{% endblock %}
{% block body %}
<ak-message-container alignment="bottom"></ak-message-container>
<ak-message-container></ak-message-container>
<ak-interface-admin>
<ak-loading></ak-loading>
</ak-interface-admin>

View File

@ -3,7 +3,7 @@
from django import template
from django.templatetags.static import static as static_loader
from authentik import get_full_version
from authentik import authentik_full_version
register = template.Library()
@ -11,4 +11,4 @@ register = template.Library()
@register.simple_tag()
def versioned_script(path: str) -> str:
"""Wrapper around {% static %} tag that supports setting the version"""
return static_loader(path.replace("%v", get_full_version()))
return static_loader(path.replace("%v", authentik_full_version()))

View File

@ -10,7 +10,7 @@ from django.utils.translation import gettext as _
from django.views.generic.base import RedirectView, TemplateView
from rest_framework.request import Request
from authentik import get_build_hash
from authentik import authentik_build_hash
from authentik.admin.tasks import LOCAL_VERSION
from authentik.api.v3.config import ConfigView
from authentik.brands.api import CurrentBrandSerializer
@ -50,7 +50,7 @@ class InterfaceView(TemplateView):
kwargs["brand_json"] = dumps(CurrentBrandSerializer(self.request.brand).data)
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
kwargs["build"] = get_build_hash()
kwargs["build"] = authentik_build_hash()
kwargs["url_kwargs"] = self.kwargs
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
kwargs["base_url_rel"] = CONFIG.get("web.path", "/")

View File

@ -12,7 +12,7 @@ from cryptography.x509.oid import NameOID
from django.db import models
from django.utils.translation import gettext_lazy as _
from authentik import __version__
from authentik import authentik_version
from authentik.crypto.models import CertificateKeyPair
@ -85,7 +85,7 @@ class CertificateBuilder:
.issuer_name(
x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, f"authentik {__version__}"),
x509.NameAttribute(NameOID.COMMON_NAME, f"authentik {authentik_version()}"),
]
)
)

View File

@ -24,7 +24,7 @@ from requests import RequestException
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.brands.models import Brand
from authentik.brands.utils import DEFAULT_BRAND
from authentik.core.middleware import (
@ -473,7 +473,7 @@ class NotificationTransport(SerializerModel):
"title": notification.body,
"color": "#fd4b2d",
"fields": fields,
"footer": f"authentik {get_full_version()}",
"footer": f"authentik {authentik_full_version()}",
}
],
}

View File

@ -7,7 +7,7 @@ from django.core.mail.backends.locmem import EmailBackend
from django.test import TestCase
from requests_mock import Mocker
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import (
Event,
@ -118,7 +118,7 @@ class TestEventTransports(TestCase):
{"short": True, "title": "Event user", "value": self.user.username},
{"title": "foo", "value": "bar,"},
],
"footer": f"authentik {get_full_version()}",
"footer": f"authentik {authentik_full_version()}",
}
],
},

View File

@ -10,7 +10,7 @@ from django.core.management.base import BaseCommand
from django.test import RequestFactory
from structlog.stdlib import get_logger
from authentik import __version__
from authentik import authentik_version
from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.models import Flow
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
@ -99,7 +99,7 @@ class Command(BaseCommand):
total_min: int = min(min(inner) for inner in values)
total_avg = sum(sum(inner) for inner in values) / sum(len(inner) for inner in values)
print(f"Version: {__version__}")
print(f"Version: {authentik_version()}")
print(f"Processes: {len(values)}")
print(f"\tMax: {total_max * 100}ms")
print(f"\tMin: {total_min * 100}ms")

View File

@ -31,7 +31,7 @@ from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME
from structlog.stdlib import get_logger
from websockets.exceptions import WebSocketException
from authentik import __version__, get_build_hash
from authentik import authentik_build_hash, authentik_version
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import authentik_user_agent
from authentik.lib.utils.reflection import get_env
@ -78,11 +78,11 @@ def sentry_init(**sentry_init_kwargs):
],
before_send=before_send,
traces_sampler=traces_sampler,
release=f"authentik@{__version__}",
release=f"authentik@{authentik_version()}",
transport=SentryTransport,
**kwargs,
)
set_tag("authentik.build_hash", get_build_hash("tagged"))
set_tag("authentik.build_hash", authentik_build_hash("tagged"))
set_tag("authentik.env", get_env())
set_tag("authentik.component", "backend")

View File

@ -5,7 +5,7 @@ from uuid import uuid4
from requests.sessions import PreparedRequest, Session
from structlog.stdlib import get_logger
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.lib.config import CONFIG
LOGGER = get_logger()
@ -13,7 +13,7 @@ LOGGER = get_logger()
def authentik_user_agent() -> str:
"""Get a common user agent"""
return f"authentik@{get_full_version()}"
return f"authentik@{authentik_full_version()}"
class TimeoutSession(Session):

View File

@ -13,7 +13,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik import get_build_hash
from authentik import authentik_build_hash
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
@ -194,7 +194,7 @@ class OutpostViewSet(UsedByMixin, ModelViewSet):
"openssl_version": state.openssl_version,
"fips_enabled": state.fips_enabled,
"hostname": state.hostname,
"build_hash_should": get_build_hash(),
"build_hash_should": authentik_build_hash(),
}
)
return Response(OutpostHealthSerializer(states, many=True).data)

View File

@ -4,7 +4,7 @@ from dataclasses import dataclass
from structlog.stdlib import get_logger
from authentik import __version__, get_build_hash
from authentik import authentik_build_hash, authentik_version
from authentik.events.logs import LogEvent, capture_logs
from authentik.lib.config import CONFIG
from authentik.lib.sentry import SentryIgnoredException
@ -99,6 +99,6 @@ class BaseController:
image_name_template: str = CONFIG.get("outposts.container_image_base")
return image_name_template % {
"type": self.outpost.type,
"version": __version__,
"build_hash": get_build_hash(),
"version": authentik_version(),
"build_hash": authentik_build_hash(),
}

View File

@ -13,7 +13,7 @@ from paramiko.ssh_exception import SSHException
from structlog.stdlib import get_logger
from yaml import safe_dump
from authentik import __version__
from authentik import authentik_version
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
@ -185,7 +185,7 @@ class DockerController(BaseController):
try:
self.client.images.pull(image)
except DockerException: # pragma: no cover
image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}"
image = f"ghcr.io/goauthentik/{self.outpost.type}:{authentik_version()}"
self.client.images.pull(image)
return image

View File

@ -17,7 +17,7 @@ from requests import Response
from structlog.stdlib import get_logger
from urllib3.exceptions import HTTPError
from authentik import __version__
from authentik import authentik_version
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import ControllerException
from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
@ -29,8 +29,8 @@ T = TypeVar("T", V1Pod, V1Deployment)
def get_version() -> str:
"""Wrapper for __version__ to make testing easier"""
return __version__
"""Wrapper for authentik_version() to make testing easier"""
return authentik_version()
class KubernetesObjectReconciler(Generic[T]):

View File

@ -23,7 +23,7 @@ from kubernetes.client import (
V1SecurityContext,
)
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
@ -94,7 +94,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
meta = self.get_object_meta(name=self.name)
image_name = self.controller.get_container_image()
image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets
version = get_full_version().replace("+", "-")
version = authentik_full_version().replace("+", "-")
return V1Deployment(
metadata=meta,
spec=V1DeploymentSpec(

View File

@ -19,7 +19,7 @@ from packaging.version import Version, parse
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik import __version__, get_build_hash
from authentik import authentik_build_hash, authentik_version
from authentik.blueprints.models import ManagedModel
from authentik.brands.models import Brand
from authentik.core.models import (
@ -38,7 +38,7 @@ from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.errors import exception_to_string
from authentik.outposts.controllers.k8s.utils import get_namespace
OUR_VERSION = parse(__version__)
OUR_VERSION = parse(authentik_version())
OUTPOST_HELLO_INTERVAL = 10
LOGGER = get_logger()
@ -451,7 +451,7 @@ class OutpostState:
"""Check if outpost version matches our version"""
if not self.version:
return False
if self.build_hash != get_build_hash():
if self.build_hash != authentik_build_hash():
return False
return parse(self.version) != OUR_VERSION

View File

@ -8,7 +8,7 @@ from channels.testing import WebsocketCommunicator
from django.contrib.contenttypes.models import ContentType
from django.test import TransactionTestCase
from authentik import __version__
from authentik import authentik_version
from authentik.core.tests.utils import create_test_flow
from authentik.outposts.consumer import WebsocketMessage, WebsocketMessageInstruction
from authentik.outposts.models import Outpost, OutpostType
@ -73,7 +73,7 @@ class TestOutpostWS(TransactionTestCase):
WebsocketMessage(
instruction=WebsocketMessageInstruction.HELLO,
args={
"version": __version__,
"version": authentik_version(),
"buildHash": "foo",
"uuid": "123",
},

View File

@ -1,11 +1,11 @@
"""authentik policy engine"""
from collections.abc import Iterable
from collections.abc import Iterator
from multiprocessing import Pipe, current_process
from multiprocessing.connection import Connection
from time import perf_counter
from django.core.cache import cache
from django.db.models import Count, Q, QuerySet
from django.http import HttpRequest
from sentry_sdk import start_span
from sentry_sdk.tracing import Span
@ -67,11 +67,14 @@ class PolicyEngine:
self.__processes: list[PolicyProcessInfo] = []
self.use_cache = True
self.__expected_result_count = 0
self.__static_result: PolicyResult | None = None
def bindings(self) -> QuerySet[PolicyBinding] | Iterable[PolicyBinding]:
def iterate_bindings(self) -> Iterator[PolicyBinding]:
"""Make sure all Policies are their respective classes"""
return PolicyBinding.objects.filter(target=self.__pbm, enabled=True).order_by("order")
return (
PolicyBinding.objects.filter(target=self.__pbm, enabled=True)
.order_by("order")
.iterator()
)
def _check_policy_type(self, binding: PolicyBinding):
"""Check policy type, make sure it's not the root class as that has no logic implemented"""
@ -81,66 +84,30 @@ class PolicyEngine:
def _check_cache(self, binding: PolicyBinding):
if not self.use_cache:
return False
# It's a bit silly to time this, but
with HIST_POLICIES_EXECUTION_TIME.labels(
binding_order=binding.order,
binding_target_type=binding.target_type,
binding_target_name=binding.target_name,
object_pk=str(self.request.obj.pk),
object_type=class_to_path(self.request.obj.__class__),
mode="cache_retrieve",
).time():
key = cache_key(binding, self.request)
cached_policy = cache.get(key, None)
if not cached_policy:
return False
before = perf_counter()
key = cache_key(binding, self.request)
cached_policy = cache.get(key, None)
duration = max(perf_counter() - before, 0)
if not cached_policy:
return False
self.logger.debug(
"P_ENG: Taking result from cache",
binding=binding,
cache_key=key,
request=self.request,
)
HIST_POLICIES_EXECUTION_TIME.labels(
binding_order=binding.order,
binding_target_type=binding.target_type,
binding_target_name=binding.target_name,
object_pk=str(self.request.obj.pk),
object_type=class_to_path(self.request.obj.__class__),
mode="cache_retrieve",
).observe(duration)
# It's a bit silly to time this, but
self.__cached_policies.append(cached_policy)
return True
def compute_static_bindings(self, bindings: QuerySet[PolicyBinding]):
"""Check static bindings if possible"""
aggrs = {
"total": Count(
"pk", filter=Q(Q(group__isnull=False) | Q(user__isnull=False), policy=None)
),
}
if self.request.user.pk:
all_groups = self.request.user.all_groups()
aggrs["passing"] = Count(
"pk",
filter=Q(
Q(
Q(user=self.request.user) | Q(group__in=all_groups),
negate=False,
)
| Q(
Q(~Q(user=self.request.user), user__isnull=False)
| Q(~Q(group__in=all_groups), group__isnull=False),
negate=True,
),
enabled=True,
),
)
matched_bindings = bindings.aggregate(**aggrs)
passing = False
if matched_bindings["total"] == 0 and matched_bindings.get("passing", 0) == 0:
# If we didn't find any static bindings, do nothing
return
self.logger.debug("P_ENG: Found static bindings", **matched_bindings)
if matched_bindings.get("passing", 0) > 0:
# Any passing static binding -> passing
passing = True
elif matched_bindings["total"] > 0 and matched_bindings.get("passing", 0) < 1:
# No matching static bindings but at least one is configured -> not passing
passing = False
self.__static_result = PolicyResult(passing)
def build(self) -> "PolicyEngine":
"""Build wrapper which monitors performance"""
with (
@ -156,12 +123,7 @@ class PolicyEngine:
span: Span
span.set_data("pbm", self.__pbm)
span.set_data("request", self.request)
bindings = self.bindings()
policy_bindings = bindings
if isinstance(bindings, QuerySet):
self.compute_static_bindings(bindings)
policy_bindings = [x for x in bindings if x.policy]
for binding in policy_bindings:
for binding in self.iterate_bindings():
self.__expected_result_count += 1
self._check_policy_type(binding)
@ -191,13 +153,10 @@ class PolicyEngine:
@property
def result(self) -> PolicyResult:
"""Get policy-checking result"""
self.__processes.sort(key=lambda x: x.binding.order)
process_results: list[PolicyResult] = [x.result for x in self.__processes if x.result]
all_results = list(process_results + self.__cached_policies)
if len(all_results) < self.__expected_result_count: # pragma: no cover
raise AssertionError("Got less results than polices")
if self.__static_result:
all_results.append(self.__static_result)
# No results, no policies attached -> passing
if len(all_results) == 0:
return PolicyResult(self.empty_result)

View File

@ -1,12 +1,9 @@
"""policy engine tests"""
from django.core.cache import cache
from django.db import connections
from django.test import TestCase
from django.test.utils import CaptureQueriesContext
from authentik.core.models import Group
from authentik.core.tests.utils import create_test_user
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.engine import PolicyEngine
@ -22,7 +19,7 @@ class TestPolicyEngine(TestCase):
def setUp(self):
clear_policy_cache()
self.user = create_test_user()
self.user = create_test_admin_user()
self.policy_false = DummyPolicy.objects.create(
name=generate_id(), result=False, wait_min=0, wait_max=1
)
@ -130,43 +127,3 @@ class TestPolicyEngine(TestCase):
self.assertEqual(len(cache.keys(f"{CACHE_PREFIX}{binding.policy_binding_uuid.hex}*")), 1)
self.assertEqual(engine.build().passing, False)
self.assertEqual(len(cache.keys(f"{CACHE_PREFIX}{binding.policy_binding_uuid.hex}*")), 1)
def test_engine_static_bindings(self):
"""Test static bindings"""
group_a = Group.objects.create(name=generate_id())
group_b = Group.objects.create(name=generate_id())
group_b.users.add(self.user)
user = create_test_user()
for case in [
{
"message": "Group, not member",
"binding_args": {"group": group_a},
"passing": False,
},
{
"message": "Group, member",
"binding_args": {"group": group_b},
"passing": True,
},
{
"message": "User, other",
"binding_args": {"user": user},
"passing": False,
},
{
"message": "User, same",
"binding_args": {"user": self.user},
"passing": True,
},
]:
with self.subTest():
pbm = PolicyBindingModel.objects.create()
for x in range(1000):
PolicyBinding.objects.create(target=pbm, order=x, **case["binding_args"])
engine = PolicyEngine(pbm, self.user)
engine.use_cache = False
with CaptureQueriesContext(connections["default"]) as ctx:
engine.build()
self.assertLess(ctx.final_queries, 1000)
self.assertEqual(engine.result.passing, case["passing"])

View File

@ -29,12 +29,13 @@ class TestPolicyProcess(TestCase):
def setUp(self):
clear_policy_cache()
self.factory = RequestFactory()
self.user = User.objects.create_user(username=generate_id())
self.user = User.objects.create_user(username="policyuser")
def test_group_passing(self):
"""Test binding to group"""
group = Group.objects.create(name=generate_id())
group = Group.objects.create(name="test-group")
group.users.add(self.user)
group.save()
binding = PolicyBinding(group=group)
request = PolicyRequest(self.user)
@ -43,7 +44,8 @@ class TestPolicyProcess(TestCase):
def test_group_negative(self):
"""Test binding to group"""
group = Group.objects.create(name=generate_id())
group = Group.objects.create(name="test-group")
group.save()
binding = PolicyBinding(group=group)
request = PolicyRequest(self.user)
@ -113,10 +115,8 @@ class TestPolicyProcess(TestCase):
def test_exception(self):
"""Test policy execution"""
policy = Policy.objects.create(name=generate_id())
binding = PolicyBinding(
policy=policy, target=Application.objects.create(name=generate_id())
)
policy = Policy.objects.create(name="test-execution")
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
request = PolicyRequest(self.user)
response = PolicyProcess(binding, request, None).execute()
@ -125,15 +125,13 @@ class TestPolicyProcess(TestCase):
def test_execution_logging(self):
"""Test policy execution creates event"""
policy = DummyPolicy.objects.create(
name=generate_id(),
name="test-execution-logging",
result=False,
wait_min=0,
wait_max=1,
execution_logging=True,
)
binding = PolicyBinding(
policy=policy, target=Application.objects.create(name=generate_id())
)
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
http_request = self.factory.get(reverse("authentik_api:user-impersonate-end"))
http_request.user = self.user
@ -188,15 +186,13 @@ class TestPolicyProcess(TestCase):
def test_execution_logging_anonymous(self):
"""Test policy execution creates event with anonymous user"""
policy = DummyPolicy.objects.create(
name=generate_id(),
name="test-execution-logging-anon",
result=False,
wait_min=0,
wait_max=1,
execution_logging=True,
)
binding = PolicyBinding(
policy=policy, target=Application.objects.create(name=generate_id())
)
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
user = AnonymousUser()
@ -223,9 +219,9 @@ class TestPolicyProcess(TestCase):
def test_raises(self):
"""Test policy that raises error"""
policy_raises = ExpressionPolicy.objects.create(name=generate_id(), expression="{{ 0/0 }}")
policy_raises = ExpressionPolicy.objects.create(name="raises", expression="{{ 0/0 }}")
binding = PolicyBinding(
policy=policy_raises, target=Application.objects.create(name=generate_id())
policy=policy_raises, target=Application.objects.create(name="test")
)
request = PolicyRequest(self.user)

View File

@ -26,7 +26,7 @@ from structlog.contextvars import STRUCTLOG_KEY_PREFIX
from structlog.stdlib import get_logger
from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.lib.sentry import before_send
from authentik.lib.utils.errors import exception_to_string
@ -158,7 +158,7 @@ class LivenessProbe(bootsteps.StartStopStep):
@inspect_command(default_timeout=0.2)
def ping(state, **kwargs):
"""Ping worker(s)."""
return {"ok": "pong", "version": get_full_version()}
return {"ok": "pong", "version": authentik_full_version()}
CELERY_APP.config_from_object(settings.CELERY)

View File

@ -10,7 +10,7 @@ from celery.schedules import crontab
from sentry_sdk import set_tag
from xmlsec import enable_debug_trace
from authentik import __version__
from authentik import authentik_version
from authentik.lib.config import CONFIG, django_db_config, redis_url
from authentik.lib.logging import get_logger_config, structlog_configure
from authentik.lib.sentry import sentry_init
@ -137,7 +137,7 @@ GUARDIAN_MONKEY_PATCH_USER = False
SPECTACULAR_SETTINGS = {
"TITLE": "authentik",
"DESCRIPTION": "Making authentication simple.",
"VERSION": __version__,
"VERSION": authentik_version(),
"COMPONENT_SPLIT_REQUEST": True,
"SCHEMA_PATH_PREFIX": "/api/v([0-9]+(beta)?)",
"SCHEMA_PATH_PREFIX_TRIM": True,
@ -486,7 +486,7 @@ if DEBUG:
TENANT_APPS.append("authentik.core")
CONFIG.log("info", "Booting authentik", version=__version__)
CONFIG.log("info", "Booting authentik", version=authentik_version())
# Attempt to load enterprise app, if available
try:

View File

@ -5,7 +5,7 @@ from ssl import OPENSSL_VERSION
import pytest
from cryptography.hazmat.backends.openssl.backend import backend
from authentik import get_full_version
from authentik import authentik_full_version
IS_CI = "CI" in environ
@ -22,7 +22,7 @@ def pytest_sessionstart(*_, **__):
def pytest_report_header(*_, **__):
"""Add authentik version to pytest output"""
return [
f"authentik version: {get_full_version()}",
f"authentik version: {authentik_full_version()}",
f"OpenSSL version: {OPENSSL_VERSION}, FIPS: {backend._fips_enabled}",
]

View File

@ -6,7 +6,7 @@ from django.http.response import Http404
from requests.exceptions import RequestException
from structlog.stdlib import get_logger
from authentik import __version__
from authentik import authentik_version
from authentik.core.sources.flow_manager import SourceFlowManager
from authentik.lib.utils.http import get_http_session
from authentik.sources.plex.models import PlexSource, UserPlexSourceConnection
@ -34,7 +34,7 @@ class PlexAuth:
"""Get common headers"""
return {
"X-Plex-Product": "authentik",
"X-Plex-Version": __version__,
"X-Plex-Version": authentik_version(),
"X-Plex-Device-Vendor": "goauthentik.io",
}

View File

@ -1,6 +1,6 @@
"""Prompt Stage Logic"""
from collections.abc import Callable
from collections.abc import Callable, Iterator
from email.policy import Policy
from types import MethodType
from typing import Any
@ -190,7 +190,7 @@ class ListPolicyEngine(PolicyEngine):
self.__list = policies
self.use_cache = False
def bindings(self):
def iterate_bindings(self) -> Iterator[PolicyBinding]:
for policy in self.__list:
yield PolicyBinding(
policy=policy,

View File

@ -2,13 +2,17 @@ package main
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak/entrypoint"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/ldap"
)
@ -21,15 +25,65 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: common.PreRun,
RunE: func(cmd *cobra.Command, args []string) error {
err := entrypoint.OutpostMain("authentik.outpost.ldap", ldap.NewServer)
if err != nil {
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL := config.Get().AuthentikHost
if akURL == "" {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken := config.Get().AuthentikToken
if akToken == "" {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = ldap.NewServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
return err
},
}

View File

@ -2,13 +2,17 @@ package main
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak/entrypoint"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/proxyv2"
)
@ -24,15 +28,65 @@ Optionally, you can set these:
- AUTHENTIK_HOST_BROWSER: URL to use in the browser, when it differs from AUTHENTIK_HOST`
var rootCmd = &cobra.Command{
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: common.PreRun,
RunE: func(cmd *cobra.Command, args []string) error {
err := entrypoint.OutpostMain("authentik.outpost.proxy", proxyv2.NewProxyServer)
if err != nil {
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL := config.Get().AuthentikHost
if akURL == "" {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken := config.Get().AuthentikToken
if akToken == "" {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = proxyv2.NewProxyServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
return err
},
}

View File

@ -2,13 +2,16 @@ package main
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak/entrypoint"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/rac"
)
@ -21,15 +24,65 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: common.PreRun,
RunE: func(cmd *cobra.Command, args []string) error {
err := entrypoint.OutpostMain("authentik.outpost.rac", rac.NewServer)
if err != nil {
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
if !found {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
if !found {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = rac.NewServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
return err
},
}

View File

@ -2,13 +2,16 @@ package main
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak/entrypoint"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/radius"
)
@ -21,15 +24,65 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: common.PreRun,
RunE: func(cmd *cobra.Command, args []string) error {
err := entrypoint.OutpostMain("authentik.outpost.radius", radius.NewServer)
if err != nil {
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
if !found {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
if !found {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = radius.NewServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
return err
},
}

View File

@ -22,12 +22,21 @@ import (
)
var rootCmd = &cobra.Command{
Use: "authentik",
Short: "Start authentik instance",
Version: constants.FullVersion(),
PersistentPreRun: common.PreRun,
Use: "authentik",
Short: "Start authentik instance",
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer("authentik.core")
debug.EnableDebugServer()
l := log.WithField("logger", "authentik.root")
if config.Get().ErrorReporting.Enabled {
@ -36,7 +45,7 @@ var rootCmd = &cobra.Command{
AttachStacktrace: true,
EnableTracing: true,
TracesSampler: sentryutils.SamplerFunc(config.Get().ErrorReporting.SampleRate),
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
Release: fmt.Sprintf("authentik@%s", constants.VERSION()),
Environment: config.Get().ErrorReporting.Environment,
HTTPTransport: webutils.NewUserAgentTransport(constants.UserAgent(), http.DefaultTransport),
IgnoreErrors: []string{
@ -90,7 +99,7 @@ func attemptProxyStart(ws *web.WebServer, u *url.URL) {
})
srv := proxyv2.NewProxyServer(ac)
ws.ProxyServer = srv.(*proxyv2.ProxyServer)
ws.ProxyServer = srv
ac.Server = srv
l.Debug("attempting to start outpost")
err := ac.StartBackgroundTasks()

View File

@ -1,90 +1,85 @@
---
services:
postgresql:
image: docker.io/library/postgres:16-alpine
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
start_period: 20s
interval: 30s
retries: 5
timeout: 5s
volumes:
- database:/var/lib/postgresql/data
env_file:
- .env
environment:
POSTGRES_DB: ${PG_DB:-authentik}
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
POSTGRES_DB: ${PG_DB:-authentik}
env_file:
- .env
redis:
image: docker.io/library/redis:alpine
command: --save 60 1 --loglevel warning
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.0}
start_period: 20s
test:
- CMD-SHELL
- pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}
timeout: 5s
image: docker.io/library/postgres:16-alpine
restart: unless-stopped
command: server
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
volumes:
- ./media:/media
- ./custom-templates:/templates
env_file:
- .env
ports:
- "${COMPOSE_PORT_HTTP:-9000}:9000"
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
- database:/var/lib/postgresql/data
redis:
command: --save 60 1 --loglevel warning
healthcheck:
interval: 30s
retries: 5
start_period: 20s
test:
- CMD-SHELL
- redis-cli ping | grep PONG
timeout: 3s
image: docker.io/library/redis:alpine
restart: unless-stopped
volumes:
- redis:/data
server:
command: server
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.0}
restart: unless-stopped
command: worker
env_file:
- .env
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
# `user: root` and the docker socket volume are optional.
# See more for the docker socket integration here:
# https://goauthentik.io/docs/outposts/integrations/docker
# Removing `user: root` also prevents the worker from fixing the permissions
# on the mounted folders, so when removing this make sure the folders have the correct UID/GID
# (1000:1000 by default)
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.0}
ports:
- ${COMPOSE_PORT_HTTP:-9000}:9000
- ${COMPOSE_PORT_HTTPS:-9443}:9443
restart: unless-stopped
volumes:
- ./media:/media
- ./custom-templates:/templates
worker:
command: worker
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
env_file:
- .env
environment:
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.0}
restart: unless-stopped
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
env_file:
- .env
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
volumes:
database:
driver: local

3
go.mod
View File

@ -16,7 +16,6 @@ require (
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/gorilla/websocket v1.5.3
github.com/grafana/pyroscope-go v1.2.2
github.com/jellydator/ttlcache/v3 v3.3.0
github.com/mitchellh/mapstructure v1.5.0
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
@ -59,10 +58,8 @@ require (
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid v1.3.1 // indirect

6
go.sum
View File

@ -178,10 +178,6 @@ github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2e
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE=
github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -266,8 +262,6 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=

View File

@ -1,17 +0,0 @@
package common
import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func PreRun(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
}

View File

@ -0,0 +1 @@
2025.4.1

View File

@ -1,10 +1,14 @@
package constants
import (
_ "embed"
"fmt"
"os"
)
//go:embed VERSION
var version string
func BUILD(def string) string {
build := os.Getenv("GIT_BUILD_HASH")
if build == "" {
@ -13,12 +17,15 @@ func BUILD(def string) string {
return build
}
func VERSION() string {
return version
}
func FullVersion() string {
ver := VERSION
if b := BUILD(""); b != "" {
return fmt.Sprintf("%s+%s", ver, b)
return fmt.Sprintf("%s+%s", version, b)
}
return ver
return version
}
func UserAgentOutpost() string {
@ -32,5 +39,3 @@ func UserAgentIPC() string {
func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2025.6.0"

View File

@ -5,24 +5,19 @@ import (
"fmt"
"net/http"
"net/http/pprof"
"os"
"runtime"
"github.com/gorilla/mux"
"github.com/grafana/pyroscope-go"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/config"
"goauthentik.io/internal/utils/web"
)
var l = log.WithField("logger", "authentik.debugger.go")
func EnableDebugServer(appName string) {
func EnableDebugServer() {
l := log.WithField("logger", "authentik.go_debugger")
if !config.Get().Debug {
return
}
h := mux.NewRouter()
enablePyroscope(appName)
h.HandleFunc("/debug/pprof/", pprof.Index)
h.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
h.HandleFunc("/debug/pprof/profile", pprof.Profile)
@ -59,38 +54,3 @@ func EnableDebugServer(appName string) {
}
}()
}
func enablePyroscope(appName string) {
p, pok := os.LookupEnv("AUTHENTIK_PYROSCOPE_HOST")
if !pok {
return
}
l.Debug("Enabling pyroscope")
runtime.SetMutexProfileFraction(5)
runtime.SetBlockProfileRate(5)
hostname, err := os.Hostname()
if err != nil {
panic(err)
}
_, err = pyroscope.Start(pyroscope.Config{
ApplicationName: appName,
ServerAddress: p,
Logger: pyroscope.StandardLogger,
Tags: map[string]string{"hostname": hostname},
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileAllocSpace,
pyroscope.ProfileInuseObjects,
pyroscope.ProfileInuseSpace,
pyroscope.ProfileGoroutines,
pyroscope.ProfileMutexCount,
pyroscope.ProfileMutexDuration,
pyroscope.ProfileBlockCount,
pyroscope.ProfileBlockDuration,
},
})
if err != nil {
panic(err)
}
}

View File

@ -135,10 +135,6 @@ func NewAPIController(akURL url.URL, token string) *APIController {
return ac
}
func (a *APIController) Log() *log.Entry {
return a.logger
}
// Start Starts all handlers, non-blocking
func (a *APIController) Start() error {
err := a.Server.Refresh()
@ -202,7 +198,7 @@ func (a *APIController) OnRefresh() error {
func (a *APIController) getWebsocketPingArgs() map[string]interface{} {
args := map[string]interface{}{
"version": constants.VERSION,
"version": constants.VERSION(),
"buildHash": constants.BUILD(""),
"uuid": a.instanceUUID.String(),
"golangVersion": runtime.Version(),
@ -222,7 +218,7 @@ func (a *APIController) StartBackgroundTasks() error {
"outpost_name": a.Outpost.Name,
"outpost_type": a.Server.Type(),
"uuid": a.instanceUUID.String(),
"version": constants.VERSION,
"version": constants.VERSION(),
"build": constants.BUILD(""),
}).Set(1)
go func() {

View File

@ -160,7 +160,7 @@ func (ac *APIController) startWSHandler() {
"outpost_name": ac.Outpost.Name,
"outpost_type": ac.Server.Type(),
"uuid": ac.instanceUUID.String(),
"version": constants.VERSION,
"version": constants.VERSION(),
"build": constants.BUILD(""),
}).SetToCurrentTime()
}
@ -222,7 +222,7 @@ func (ac *APIController) startIntervalUpdater() {
"outpost_name": ac.Outpost.Name,
"outpost_type": ac.Server.Type(),
"uuid": ac.instanceUUID.String(),
"version": constants.VERSION,
"version": constants.VERSION(),
"build": constants.BUILD(""),
}).SetToCurrentTime()
}

View File

@ -1,51 +0,0 @@
package entrypoint
import (
"errors"
"net/url"
"os"
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
)
func OutpostMain(appName string, server func(ac *ak.APIController) ak.Outpost) error {
debug.EnableDebugServer(appName)
akURL := config.Get().AuthentikHost
if akURL == "" {
return errors.New("environment variable `AUTHENTIK_HOST` not set")
}
akToken := config.Get().AuthentikToken
if akToken == "" {
return errors.New("environment variable `AUTHENTIK_TOKEN` not set")
}
akURLActual, err := url.Parse(akURL)
if err != nil {
return err
}
ex := common.Init()
defer common.Defer()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = server(ac)
err = ac.Start()
if err != nil {
ac.Log().WithError(err).Panic("Failed to run server")
return err
}
for {
<-ex
return nil
}
}

View File

@ -48,25 +48,25 @@ func doGlobalSetup(outpost api.Outpost, globalConfig *api.Config) {
if globalConfig.ErrorReporting.Enabled {
if !initialSetup {
l.WithField("env", globalConfig.ErrorReporting.Environment).Debug("Error reporting enabled")
err := sentry.Init(sentry.ClientOptions{
Dsn: globalConfig.ErrorReporting.SentryDsn,
Environment: globalConfig.ErrorReporting.Environment,
EnableTracing: true,
TracesSampler: sentryutils.SamplerFunc(float64(globalConfig.ErrorReporting.TracesSampleRate)),
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
HTTPTransport: webutils.NewUserAgentTransport(constants.UserAgentOutpost(), http.DefaultTransport),
IgnoreErrors: []string{
http.ErrAbortHandler.Error(),
},
})
if err != nil {
l.WithField("env", globalConfig.ErrorReporting.Environment).WithError(err).Warning("Failed to initialise sentry")
}
}
err := sentry.Init(sentry.ClientOptions{
Dsn: globalConfig.ErrorReporting.SentryDsn,
Environment: globalConfig.ErrorReporting.Environment,
EnableTracing: true,
TracesSampler: sentryutils.SamplerFunc(float64(globalConfig.ErrorReporting.TracesSampleRate)),
Release: fmt.Sprintf("authentik@%s", constants.VERSION()),
HTTPTransport: webutils.NewUserAgentTransport(constants.UserAgentOutpost(), http.DefaultTransport),
IgnoreErrors: []string{
http.ErrAbortHandler.Error(),
},
})
if err != nil {
l.WithField("env", globalConfig.ErrorReporting.Environment).WithError(err).Warning("Failed to initialise sentry")
}
}
if !initialSetup {
l.WithField("hash", constants.BUILD("tagged")).WithField("version", constants.VERSION).Info("Starting authentik outpost")
l.WithField("hash", constants.BUILD("tagged")).WithField("version", constants.VERSION()).Info("Starting authentik outpost")
initialSetup = true
}
}

View File

@ -26,7 +26,7 @@ type LDAPServer struct {
providers []*ProviderInstance
}
func NewServer(ac *ak.APIController) ak.Outpost {
func NewServer(ac *ak.APIController) *LDAPServer {
ls := &LDAPServer{
log: log.WithField("logger", "authentik.outpost.ldap"),
ac: ac,

View File

@ -3,7 +3,6 @@ package application
type ProxyClaims struct {
UserAttributes map[string]interface{} `json:"user_attributes"`
BackendOverride string `json:"backend_override"`
HostHeader string `json:"host_header"`
IsSuperuser bool `json:"is_superuser"`
}

View File

@ -74,18 +74,13 @@ func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) {
r.URL.Scheme = ou.Scheme
r.URL.Host = ou.Host
claims := a.getClaimsFromSession(r)
if claims != nil && claims.Proxy != nil {
if claims.Proxy.BackendOverride != "" {
u, err := url.Parse(claims.Proxy.BackendOverride)
if err != nil {
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
} else {
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
}
}
if claims.Proxy.HostHeader != "" {
r.Host = claims.Proxy.HostHeader
if claims != nil && claims.Proxy != nil && claims.Proxy.BackendOverride != "" {
u, err := url.Parse(claims.Proxy.BackendOverride)
if err != nil {
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
} else {
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
}
}
a.log.WithField("upstream_url", r.URL.String()).Trace("final upstream url")

View File

@ -35,7 +35,7 @@ type ProxyServer struct {
akAPI *ak.APIController
}
func NewProxyServer(ac *ak.APIController) ak.Outpost {
func NewProxyServer(ac *ak.APIController) *ProxyServer {
l := log.WithField("logger", "authentik.outpost.proxyv2")
defaultCert, err := crypto.GenerateSelfSignedCert()
if err != nil {

View File

@ -23,7 +23,7 @@ type RACServer struct {
conns map[string]connection.Connection
}
func NewServer(ac *ak.APIController) ak.Outpost {
func NewServer(ac *ak.APIController) *RACServer {
rs := &RACServer{
log: log.WithField("logger", "authentik.outpost.rac"),
ac: ac,

View File

@ -34,7 +34,7 @@ type RadiusServer struct {
providers []*ProviderInstance
}
func NewServer(ac *ak.APIController) ak.Outpost {
func NewServer(ac *ak.APIController) *RadiusServer {
rs := &RadiusServer{
log: log.WithField("logger", "authentik.outpost.radius"),
ac: ac,

View File

@ -2,6 +2,7 @@ package web
import (
"context"
"fmt"
"net/http"
"github.com/getsentry/sentry-go"
@ -19,7 +20,7 @@ func NewTracingTransport(ctx context.Context, inner http.RoundTripper) *tracingT
func (tt *tracingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
span := sentry.StartSpan(tt.ctx, "authentik.go.http_request")
r.Header.Set("sentry-trace", span.ToSentryTrace())
span.Description = r.Method + " " + r.URL.String()
span.Description = fmt.Sprintf("%s %s", r.Method, r.URL.String())
span.SetTag("url", r.URL.String())
span.SetTag("method", r.Method)
defer span.Finish()

View File

@ -31,6 +31,8 @@ func (ws *WebServer) configureStatic() {
return h
}
helpHandler := http.FileServer(http.Dir("./website/help/"))
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/dist/").Handler(pathStripper(
distFs,
"static/dist/",
@ -76,6 +78,13 @@ func (ws *WebServer) configureStatic() {
))
}
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/help/").Handler(pathStripper(
helpHandler,
config.Get().Web.Path,
"/if/help/",
))
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/help").Handler(http.RedirectHandler(fmt.Sprintf("%sif/help/", config.Get().Web.Path), http.StatusMovedPermanently))
staticRouter.PathPrefix(config.Get().Web.Path).Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header()["Content-Type"] = []string{"text/plain"}
rw.WriteHeader(200)
@ -98,7 +107,7 @@ func (ws *WebServer) staticHeaderMiddleware(h http.Handler) http.Handler {
etagHandler := etag.Handler(h, false)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "public, no-transform")
w.Header().Set("X-authentik-version", constants.VERSION)
w.Header().Set("X-authentik-version", constants.VERSION())
w.Header().Set("Vary", "X-authentik-version, Etag")
etagHandler.ServeHTTP(w, r)
})

View File

@ -34,7 +34,7 @@ from aws_cdk import (
)
from constructs import Construct
from authentik import __version__
from authentik import authentik_version as ak_version
class AuthentikStack(Stack):
@ -88,7 +88,7 @@ class AuthentikStack(Stack):
self,
"AuthentikVersion",
type="String",
default=__version__,
default=ak_version(),
description="authentik Docker image tag",
)

View File

@ -11,7 +11,7 @@ from cryptography.hazmat.backends.openssl.backend import backend
from defusedxml import defuse_stdlib
from prometheus_client.values import MultiProcessValue
from authentik import get_full_version
from authentik import authentik_full_version
from authentik.lib.config import CONFIG
from authentik.lib.debug import start_debug_server
from authentik.lib.logging import get_logger_config
@ -132,9 +132,9 @@ if not CONFIG.get_bool("disable_startup_analytics", False):
json={
"domain": "authentik",
"name": "pageview",
"referrer": get_full_version(),
"referrer": authentik_full_version(),
"url": (
f"http://localhost/{env}?utm_source={get_full_version()}&utm_medium={env}"
f"http://localhost/{env}?utm_source={authentik_full_version()}&utm_medium={env}"
),
},
headers={

View File

@ -2,7 +2,7 @@
from lifecycle.migrate import BaseMigration
from datetime import datetime
from authentik import __version__, get_build_hash
from authentik import authentik_version, authentik_build_hash
class Migration(BaseMigration):
@ -14,7 +14,7 @@ class Migration(BaseMigration):
ORDER BY "timestamp" DESC
LIMIT 1
""",
(__version__, get_build_hash()),
(authentik_version(), authentik_build_hash()),
)
return not bool(self.cur.rowcount)
@ -24,7 +24,7 @@ class Migration(BaseMigration):
INSERT INTO authentik_version_history ("timestamp", version, build)
VALUES (%s, %s, %s)
""",
(datetime.now(), __version__, get_build_hash()),
(datetime.now(), authentik_version(), authentik_build_hash()),
)
self.cur.execute(
"""

Binary file not shown.

Binary file not shown.

View File

@ -13,7 +13,7 @@ dependencies = [
"dacite==1.9.2",
"deepmerge==2.0",
"defusedxml==0.7.1",
"django==5.1.10",
"django==5.1.9",
"django-countries==7.6.1",
"django-cte==1.3.3",
"django-filter==25.1",
@ -79,7 +79,6 @@ dev = [
"aws-cdk-lib==2.188.0",
"bandit==1.8.3",
"black==25.1.0",
"bump2version==1.0.1",
"channels[daphne]==4.2.2",
"codespell==2.4.1",
"colorama==0.4.6",

View File

@ -0,0 +1,92 @@
from yaml import safe_dump
from authentik import authentik_version
authentik_image = (
f"${{AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}}:${{AUTHENTIK_TAG:-{authentik_version()}}}"
)
base = {
"services": {
"postgresql": {
"env_file": [".env"],
"environment": {
"POSTGRES_DB": "${PG_DB:-authentik}",
"POSTGRES_PASSWORD": "${PG_PASS:?database " "password " "required}",
"POSTGRES_USER": "${PG_USER:-authentik}",
},
"healthcheck": {
"interval": "30s",
"retries": 5,
"start_period": "20s",
"test": ["CMD-SHELL", "pg_isready -d " "$${POSTGRES_DB} -U " "$${POSTGRES_USER}"],
"timeout": "5s",
},
"image": "docker.io/library/postgres:16-alpine",
"restart": "unless-stopped",
"volumes": ["database:/var/lib/postgresql/data"],
},
"redis": {
"command": "--save 60 1 --loglevel warning",
"healthcheck": {
"interval": "30s",
"retries": 5,
"start_period": "20s",
"test": ["CMD-SHELL", "redis-cli ping | grep PONG"],
"timeout": "3s",
},
"image": "docker.io/library/redis:alpine",
"restart": "unless-stopped",
"volumes": ["redis:/data"],
},
"server": {
"command": "server",
"depends_on": {
"postgresql": {"condition": "service_healthy"},
"redis": {"condition": "service_healthy"},
},
"env_file": [".env"],
"environment": {
"AUTHENTIK_POSTGRESQL__HOST": "postgresql",
"AUTHENTIK_POSTGRESQL__NAME": "${PG_DB:-authentik}",
"AUTHENTIK_POSTGRESQL__PASSWORD": "${PG_PASS}",
"AUTHENTIK_POSTGRESQL__USER": "${PG_USER:-authentik}",
"AUTHENTIK_REDIS__HOST": "redis",
"AUTHENTIK_SECRET_KEY": "${AUTHENTIK_SECRET_KEY:?secret " "key " "required}",
},
"image": authentik_image,
"ports": ["${COMPOSE_PORT_HTTP:-9000}:9000", "${COMPOSE_PORT_HTTPS:-9443}:9443"],
"restart": "unless-stopped",
"volumes": ["./media:/media", "./custom-templates:/templates"],
},
"worker": {
"command": "worker",
"depends_on": {
"postgresql": {"condition": "service_healthy"},
"redis": {"condition": "service_healthy"},
},
"env_file": [".env"],
"environment": {
"AUTHENTIK_POSTGRESQL__HOST": "postgresql",
"AUTHENTIK_POSTGRESQL__NAME": "${PG_DB:-authentik}",
"AUTHENTIK_POSTGRESQL__PASSWORD": "${PG_PASS}",
"AUTHENTIK_POSTGRESQL__USER": "${PG_USER:-authentik}",
"AUTHENTIK_REDIS__HOST": "redis",
"AUTHENTIK_SECRET_KEY": "${AUTHENTIK_SECRET_KEY:?secret " "key " "required}",
},
"image": authentik_image,
"restart": "unless-stopped",
"user": "root",
"volumes": [
"/var/run/docker.sock:/var/run/docker.sock",
"./media:/media",
"./certs:/certs",
"./custom-templates:/templates",
],
},
},
"volumes": {"database": {"driver": "local"}, "redis": {"driver": "local"}},
}
with open("docker-compose.yml", "w") as _compose:
safe_dump(base, _compose)

View File

@ -5,7 +5,7 @@ Generates a Semantic Versioning identifier, suffixed with a timestamp.
from time import time
from authentik import __version__ as package_version
from authentik import authentik_version as package_version
"""
See: https://semver.org/#spec-item-9 (Pre-release spec)

View File

@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://json.schemastore.org/traefik-v3.json
# yaml-language-server: $schema=https://json.schemastore.org/traefik-v2.json
api:
insecure: true
debug: true

View File

@ -1,97 +0,0 @@
"""test flow with WebAuthn Stage"""
from selenium.webdriver.common.virtual_authenticator import (
Protocol,
Transport,
VirtualAuthenticatorOptions,
)
from authentik.blueprints.tests import apply_blueprint
from authentik.stages.authenticator_webauthn.models import (
AuthenticatorWebAuthnStage,
WebAuthnDevice,
)
from tests.e2e.test_flows_login_sfe import login_sfe
from tests.e2e.utils import SeleniumTestCase, retry
class TestFlowsAuthenticatorWebAuthn(SeleniumTestCase):
"""test flow with WebAuthn Stage"""
host = "localhost"
def register(self):
options = VirtualAuthenticatorOptions(
protocol=Protocol.CTAP2,
transport=Transport.INTERNAL,
has_resident_key=True,
has_user_verification=True,
is_user_verified=True,
)
self.driver.add_virtual_authenticator(options)
self.driver.get(self.url("authentik_core:if-flow", flow_slug="default-authentication-flow"))
self.login()
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)
self.driver.get(
self.url(
"authentik_flows:configure",
stage_uuid=AuthenticatorWebAuthnStage.objects.first().stage_uuid,
)
)
self.wait_for_url(self.if_user_url("/library"))
self.assertTrue(WebAuthnDevice.objects.filter(user=self.user, confirmed=True).exists())
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint("default/flow-default-authenticator-webauthn-setup.yaml")
def test_webauthn_setup(self):
"""Test WebAuthn setup"""
self.register()
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint("default/flow-default-authenticator-webauthn-setup.yaml")
def test_webauthn_authenticate(self):
"""Test WebAuthn authentication"""
self.register()
self.driver.delete_all_cookies()
self.driver.get(self.url("authentik_core:if-flow", flow_slug="default-authentication-flow"))
self.login()
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint("default/flow-default-authenticator-webauthn-setup.yaml")
def test_webauthn_authenticate_sfe(self):
"""Test WebAuthn authentication (SFE)"""
self.register()
self.driver.delete_all_cookies()
self.driver.get(
self.url(
"authentik_core:if-flow",
flow_slug="default-authentication-flow",
query={"sfe": True},
)
)
login_sfe(self.driver, self.user)
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)

View File

@ -4,35 +4,34 @@ from time import sleep
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webdriver import WebDriver
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import User
from tests.e2e.utils import SeleniumTestCase, retry
def login_sfe(driver: WebDriver, user: User):
"""Do entire login flow adjusted for SFE"""
flow_executor = driver.find_element(By.ID, "flow-sfe-container")
identification_stage = flow_executor.find_element(By.ID, "ident-form")
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").click()
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
user.username
)
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
Keys.ENTER
)
password_stage = flow_executor.find_element(By.ID, "password-form")
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(user.username)
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
sleep(1)
class TestFlowsLoginSFE(SeleniumTestCase):
"""test default login flow"""
def login(self):
"""Do entire login flow adjusted for SFE"""
flow_executor = self.driver.find_element(By.ID, "flow-sfe-container")
identification_stage = flow_executor.find_element(By.ID, "ident-form")
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").click()
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
self.user.username
)
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
Keys.ENTER
)
password_stage = flow_executor.find_element(By.ID, "password-form")
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
self.user.username
)
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
sleep(1)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
@ -47,6 +46,6 @@ class TestFlowsLoginSFE(SeleniumTestCase):
query={"sfe": True},
)
)
login_sfe(self.driver, self.user)
self.login()
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)

19
uv.lock generated
View File

@ -242,7 +242,6 @@ dev = [
{ name = "aws-cdk-lib" },
{ name = "bandit" },
{ name = "black" },
{ name = "bump2version" },
{ name = "channels", extra = ["daphne"] },
{ name = "codespell" },
{ name = "colorama" },
@ -274,7 +273,7 @@ requires-dist = [
{ name = "dacite", specifier = "==1.9.2" },
{ name = "deepmerge", specifier = "==2.0" },
{ name = "defusedxml", specifier = "==0.7.1" },
{ name = "django", specifier = "==5.1.10" },
{ name = "django", specifier = "==5.1.9" },
{ name = "django-countries", specifier = "==7.6.1" },
{ name = "django-cte", specifier = "==1.3.3" },
{ name = "django-filter", specifier = "==25.1" },
@ -340,7 +339,6 @@ dev = [
{ name = "aws-cdk-lib", specifier = "==2.188.0" },
{ name = "bandit", specifier = "==1.8.3" },
{ name = "black", specifier = "==25.1.0" },
{ name = "bump2version", specifier = "==1.0.1" },
{ name = "channels", extras = ["daphne"], specifier = "==4.2.2" },
{ name = "codespell", specifier = "==2.4.1" },
{ name = "colorama", specifier = "==0.4.6" },
@ -598,15 +596,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/7a/03482cd3c00008d9be646c8e1520611d6202ce447db725ac40b07f9b088a/botocore-1.38.29-py3-none-any.whl", hash = "sha256:4d623f54326eb66d1a633f0c1780992c80f3db317a91c9afe31d5c700290621e", size = 13588258, upload-time = "2025-06-03T19:22:45.14Z" },
]
[[package]]
name = "bump2version"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/29/2a/688aca6eeebfe8941235be53f4da780c6edee05dbbea5d7abaa3aab6fad2/bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6", size = 36236, upload-time = "2020-10-07T18:38:40.119Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/e3/fa60c47d7c344533142eb3af0b73234ef8ea3fb2da742ab976b947e717df/bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", size = 22030, upload-time = "2020-10-07T18:38:38.148Z" },
]
[[package]]
name = "cachetools"
version = "5.5.2"
@ -968,16 +957,16 @@ wheels = [
[[package]]
name = "django"
version = "5.1.10"
version = "5.1.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/73/ca/1c724be89e603eb8b5587ea24c63a8c30094c8ff4d990780b5033ee15c40/django-5.1.10.tar.gz", hash = "sha256:73e5d191421d177803dbd5495d94bc7d06d156df9561f4eea9e11b4994c07137", size = 10714538, upload-time = "2025-06-04T13:53:18.805Z" }
sdist = { url = "https://files.pythonhosted.org/packages/10/08/2e6f05494b3fc0a3c53736846034f882b82ee6351791a7815bbb45715d79/django-5.1.9.tar.gz", hash = "sha256:565881bdd0eb67da36442e9ac788bda90275386b549070d70aee86327781a4fc", size = 10710887, upload-time = "2025-05-07T14:06:45.257Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/fc/80dc741ba0acb3241aac1213d7272c573d52d8a62ec2c69e9b3bef1547f2/django-5.1.10-py3-none-any.whl", hash = "sha256:19c9b771e9cf4de91101861aadd2daaa159bcf10698ca909c5755c88e70ccb84", size = 8277457, upload-time = "2025-06-04T13:53:07.676Z" },
{ url = "https://files.pythonhosted.org/packages/e1/d1/d8b6b8250b84380d5a123e099ad3298a49407d81598faa13b43a2c6d96d7/django-5.1.9-py3-none-any.whl", hash = "sha256:2fd1d4a0a66a5ba702699eb692e75b0d828b73cc2f4e1fc4b6a854a918967411", size = 8277363, upload-time = "2025-05-07T14:06:37.426Z" },
]
[[package]]

View File

@ -403,9 +403,6 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
}
render() {
if (this.challenge.deviceChallenges.length === 1) {
this.deviceChallenge = this.challenge.deviceChallenges[0];
}
if (!this.deviceChallenge) {
return this.renderChallengePicker();
}
@ -434,7 +431,9 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
${
challenges.length > 0
? "<p>Select an authentication method.</p>"
: `<p>No compatible authentication method available</p>`
: `
<p>No compatible authentication method available</p>
`
}
${challenges
.map((challenge) => {

View File

@ -56,14 +56,18 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
protected readonly ws: WebsocketClient;
@property({ type: Object, attribute: false })
@property({
type: Object,
attribute: false,
reflect: false,
})
public user?: SessionUser;
@query("ak-about-modal")
public aboutModal?: AboutModal;
@property({ type: Boolean, reflect: true })
public sidebarOpen = false;
public sidebarOpen: boolean;
@eventOptions({ passive: true })
protected sidebarListener(event: CustomEvent<SidebarToggleEventDetail>) {

View File

@ -67,17 +67,6 @@ export class DebugPage extends AKElement {
>
POST System
</button>
<button
class="pf-c-button pf-m-primary"
@click=${() => {
showMessage({
level: MessageLevel.info,
message: `lorem ipsum ${Date.now()}`,
});
}}
>
Message
</button>
</div>
</div>
</div>

View File

@ -88,6 +88,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
return html`<ak-page-header
header=${this.user ? msg(str`Welcome, ${username || ""}.`) : msg("Welcome.")}
description=${msg("General system status")}
?hasIcon=${false}
>
</ak-page-header>
<section class="pf-c-page__main-section">

View File

@ -72,7 +72,7 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
name="avatars"
label=${msg("Avatars")}
value="${ifDefined(this._settings?.avatars)}"
input-hint="code"
inputHint="code"
.bighelp=${html`
<p class="pf-c-form__helper-text">
${msg(
@ -159,7 +159,7 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
<ak-text-input
name="eventRetention"
label=${msg("Event retention")}
input-hint="code"
inputHint="code"
required
value="${ifDefined(this._settings?.eventRetention)}"
.bighelp=${html`<p class="pf-c-form__helper-text">
@ -237,7 +237,7 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
<ak-text-input
name="defaultTokenDuration"
label=${msg("Default token duration")}
input-hint="code"
inputHint="code"
required
value="${ifDefined(this._settings?.defaultTokenDuration)}"
.bighelp=${html`<p class="pf-c-form__helper-text">

View File

@ -93,7 +93,11 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
}
renderForm(): TemplateResult {
return html`<ak-form-element-horizontal label=${msg("User")} required name="forUser">
return html`<ak-form-element-horizontal
label=${msg("User")}
?required=${true}
name="forUser"
>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {

View File

@ -136,7 +136,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
label=${msg("Slug")}
required
help=${msg("Internal application name used in URLs.")}
input-hint="code"
inputHint="code"
></ak-text-input>
<ak-text-input
name="group"
@ -145,7 +145,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
help=${msg(
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
)}
input-hint="code"
inputHint="code"
></ak-text-input>
<ak-provider-search-input
name="provider"
@ -186,7 +186,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
help=${msg(
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
)}
input-hint="code"
inputHint="code"
></ak-text-input>
<ak-switch-input
name="openInNewTab"

View File

@ -99,6 +99,7 @@ export class ApplicationViewPage extends AKElement {
return html`<ak-page-header
header=${this.application?.name || msg("Loading")}
description=${ifDefined(this.application?.metaPublisher)}
.iconImage=${true}
>
<ak-app-icon
size=${PFSize.Medium}

View File

@ -20,7 +20,7 @@ export class ProviderSelectModal extends TableModal<Provider> {
}
@property({ type: Boolean })
backchannel = false;
backchannel?: boolean;
@property()
confirm!: (selectedItems: Provider[]) => Promise<unknown>;

View File

@ -57,7 +57,7 @@ export class AkBackchannelProvidersInput extends AKElement {
render() {
const renderOneChip = (provider: Provider) =>
html`<ak-chip
removable
.removable=${true}
value=${ifDefined(provider.pk)}
@remove=${this.remover(provider)}
>${provider.name}</ak-chip
@ -66,7 +66,7 @@ export class AkBackchannelProvidersInput extends AKElement {
return html`
<ak-form-element-horizontal label=${this.label} name=${this.name}>
<div class="pf-c-input-group">
<ak-provider-select-table backchannel .confirm=${this.confirm}>
<ak-provider-select-table ?backchannel=${true} .confirm=${this.confirm}>
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
${this.tooltip ? this.tooltip : nothing}
<i class="fas fa-plus" aria-hidden="true"></i>

View File

@ -54,7 +54,7 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} required name="name">
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${this.instance?.name ?? ""}"
@ -62,7 +62,11 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
<ak-form-element-horizontal
label=${msg("Attributes")}
?required=${false}
name="attributes"
>
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(this.instance?.attributes ?? {})}"

View File

@ -128,7 +128,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
?invalid=${errors.slug ?? this.errors.has("slug")}
.errorMessages=${this.errorMessages("slug")}
help=${msg("Internal application name used in URLs.")}
input-hint="code"
inputHint="code"
></ak-slug-input>
<ak-text-input
name="group"
@ -138,7 +138,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
help=${msg(
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
)}
input-hint="code"
inputHint="code"
></ak-text-input>
<ak-radio-input
label=${msg("Policy engine mode")}
@ -161,7 +161,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
help=${msg(
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
)}
input-hint="code"
inputHint="code"
></ak-text-input>
<ak-switch-input
name="openInNewTab"

View File

@ -37,7 +37,7 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
required
?required=${true}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
@ -57,10 +57,10 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF
help=${msg(
"Determines how long a session lasts before being disconnected and requiring re-authorization.",
)}
input-hint="code"
inputHint="code"
></ak-text-input>
<ak-form-group expanded>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal

View File

@ -65,7 +65,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} required name="name">
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
@ -133,7 +133,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
.selected=${(item: BlueprintFile): boolean => {
return this.instance?.path === item.path;
}}
blankable
?blankable=${true}
>
</ak-search-select>
</ak-form-element-horizontal>`

View File

@ -134,7 +134,7 @@ export class BrandForm extends ModelForm<Brand, string> {
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Default flow background")}
required
?required=${true}
name="brandingDefaultFlowBackground"
>
<input

View File

@ -95,7 +95,7 @@ export class CoreGroupSearch extends CustomListenerElement(AKElement) {
.value=${renderValue}
.selected=${this.selected}
@ak-change=${this.handleSearchUpdate}
blankable
?blankable=${true}
>
</ak-search-select>
`;

View File

@ -120,7 +120,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
.value=${renderValue}
.selected=${this.selected}
@ak-change=${this.handleSearchUpdate}
blankable
?blankable=${true}
>
</ak-search-select>
`;

View File

@ -110,7 +110,7 @@ export const Default = () =>
container(
html` <ak-form-element-horizontal
label=${msg("Authorization flow")}
required
?required=${true}
name="authorizationFlow"
>
<ak-flow-search
@ -124,7 +124,7 @@ export const WithInitialValue = () =>
container(
html` <ak-form-element-horizontal
label=${msg("Authorization flow")}
required
?required=${true}
name="authorizationFlow"
>
<ak-flow-search

View File

@ -29,7 +29,7 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
return html`<ak-form-element-horizontal
label=${msg("Common Name")}
name="commonName"
required
?required=${true}
>
<input type="text" class="pf-c-form-control" required />
</ak-form-element-horizontal>
@ -39,10 +39,18 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
${msg("Optional, comma-separated SubjectAlt Names.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Validity days")} name="validityDays" required>
<ak-form-element-horizontal
label=${msg("Validity days")}
name="validityDays"
?required=${true}
>
<input class="pf-c-form-control" type="number" value="365" />
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Private key Algorithm")} required name="alg">
<ak-form-element-horizontal
label=${msg("Private key Algorithm")}
?required=${true}
name="alg"
>
<ak-radio
.options=${[
{

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-private-textarea-input.js";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -38,7 +37,7 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} name="name" required>
return html` <ak-form-element-horizontal label=${msg("Name")} name="name" ?required=${true}>
<input
type="text"
value="${ifDefined(this.instance?.name)}"
@ -46,24 +45,37 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
required
/>
</ak-form-element-horizontal>
<ak-private-textarea-input
<ak-form-element-horizontal
label=${msg("Certificate")}
name="certificateData"
input-hint="code"
placeholder="-----BEGIN CERTIFICATE-----"
required
?revealed=${this.instance === undefined}
help=${msg("PEM-encoded Certificate data.")}
></ak-private-textarea-input>
<ak-private-textarea-input
label=${msg("Private Key")}
?writeOnly=${this.instance !== undefined}
?required=${true}
>
<textarea
autocomplete="off"
spellcheck="false"
class="pf-c-form-control pf-m-monospace"
placeholder="-----BEGIN CERTIFICATE-----"
required
></textarea>
<p class="pf-c-form__helper-text">${msg("PEM-encoded Certificate data.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
name="keyData"
input-hint="code"
?revealed=${this.instance === undefined}
help=${msg(
"Optional Private Key. If this is set, you can use this keypair for encryption.",
)}
></ak-private-textarea-input>`;
?writeOnly=${this.instance !== undefined}
label=${msg("Private Key")}
>
<textarea
autocomplete="off"
class="pf-c-form-control pf-m-monospace"
spellcheck="false"
></textarea>
<p class="pf-c-form__helper-text">
${msg(
"Optional Private Key. If this is set, you can use this keypair for encryption.",
)}
</p>
</ak-form-element-horizontal>`;
}
}

View File

@ -1,6 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
import "@goauthentik/components/ak-private-textarea-input.js";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -62,13 +61,17 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
value="${ifDefined(this.installID)}"
/>
</ak-form-element-horizontal>
<ak-private-textarea-input
<ak-form-element-horizontal
name="key"
?revealed=${this.instance === undefined}
?writeOnly=${this.instance !== undefined}
label=${msg("License key")}
input-hint="code"
>
</ak-private-textarea-input>`;
<textarea
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
></textarea>
</ak-form-element-horizontal>`;
}
}

View File

@ -58,7 +58,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} required name="name">
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
@ -88,7 +88,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
.selected=${(group: Group): boolean => {
return group.pk === this.instance?.group;
}}
blankable
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
@ -97,7 +97,11 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Transports")} required name="transports">
<ak-form-element-horizontal
label=${msg("Transports")}
?required=${true}
name="transports"
>
<ak-dual-select-dynamic-selected
.provider=${eventTransportsProvider}
.selector=${eventTransportsSelector(this.instance?.transports)}
@ -110,7 +114,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Severity")} required name="severity">
<ak-form-element-horizontal label=${msg("Severity")} ?required=${true} name="severity">
<ak-radio
.options=${[
{

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