Compare commits

..

13 Commits

Author SHA1 Message Date
67b1ae7add re-migrate
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-26 18:06:52 +01:00
9fd40a0a3d format
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-26 17:18:40 +01:00
cdfd92c49e sources/ldap: implement test 2025-03-24 20:49:29 +01:00
f6a3105fa5 Add a test for memberof attribute 2025-03-24 20:49:29 +01:00
49067f8cdc sources/ldap: add missing spaces in docstrings
Follows suggestions from @jorhett
2025-03-24 20:49:29 +01:00
badd76998a website/docs: simplify wording of attribute documentation
Follows suggestions from @jorhett
2025-03-24 20:49:29 +01:00
17d93dcb38 Update website/docs/users-sources/sources/directory-sync/active-directory/index.md
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Amélie Krejčí <amelie@krejci.vip>
2025-03-24 20:49:29 +01:00
ade22c810f website/docs: add note about lookups to AD docs 2025-03-24 20:49:29 +01:00
383f9ecdb0 sources/ldap: lint changed files 2025-03-24 20:49:29 +01:00
9471b1d9df sources/ldap: add group membership toggle ui element 2025-03-24 20:49:28 +01:00
4119f93b54 sources/ldap: add schema changes 2025-03-24 20:49:28 +01:00
9dfa792757 sources/ldap: implement working membership lookups 2025-03-24 20:49:28 +01:00
269a557c58 sources/ldap: add support for group lookups from user 2025-03-24 20:49:27 +01:00
416 changed files with 3768 additions and 5602 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2025.2.3
current_version = 2025.2.2
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
@ -17,8 +17,6 @@ optional_value = final
[bumpversion:file:pyproject.toml]
[bumpversion:file:uv.lock]
[bumpversion:file:package.json]
[bumpversion:file:docker-compose.yml]

View File

@ -1,22 +0,0 @@
---
name: Documentation issue
about: Suggest an improvement or report a problem
title: ""
labels: documentation
assignees: ""
---
**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.**
A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...]
**Provide the URL or link to the exact page in the documentation to which you are referring.**
If there are multiple pages, list them all, and be sure to state the header or section where the content is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the documentation issue here.
**Consider opening a PR!**
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).

View File

@ -44,6 +44,7 @@ if is_release:
]
if not prerelease:
image_tags += [
f"{name}:latest",
f"{name}:{version_family}",
]
else:

View File

@ -29,7 +29,7 @@ jobs:
- name: Generate API
run: make gen-client-go
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout 5000s --verbose

View File

@ -43,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder
ARG TARGETOS
ARG TARGETARCH
@ -76,7 +76,7 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/authentik ./cmd/server
# Stage 4: MaxMind GeoIP
@ -94,9 +94,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.6.11 AS uv
FROM ghcr.io/astral-sh/uv:0.6.9 AS uv
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \

View File

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

View File

@ -46,7 +46,7 @@ LOGGER = get_logger()
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
"""Cache key where application list for user is saved"""
key = f"{CACHE_PREFIX}app_access/{user_pk}"
key = f"{CACHE_PREFIX}/app_access/{user_pk}"
if page_number:
key += f"/{page_number}"
return key

View File

@ -1,14 +1,13 @@
"""User API Views"""
from datetime import timedelta
from importlib import import_module
from json import loads
from typing import Any
from django.conf import settings
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission
from django.contrib.sessions.backends.base import SessionBase
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.db.models.functions import ExtractHour
from django.db.transaction import atomic
from django.db.utils import IntegrityError
@ -92,7 +91,6 @@ from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
class UserGroupSerializer(ModelSerializer):
@ -375,7 +373,7 @@ class UsersFilter(FilterSet):
method="filter_attributes",
)
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
uuid = UUIDFilter(field_name="uuid")
path = CharFilter(field_name="path")
@ -393,11 +391,6 @@ class UsersFilter(FilterSet):
queryset=Group.objects.all().order_by("name"),
)
def filter_is_superuser(self, queryset, name, value):
if value:
return queryset.filter(ak_groups__is_superuser=True).distinct()
return queryset.exclude(ak_groups__is_superuser=True).distinct()
def filter_attributes(self, queryset, name, value):
"""Filter attributes by query args"""
try:
@ -776,8 +769,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not instance.is_active:
sessions = AuthenticatedSession.objects.filter(user=instance)
session_ids = sessions.values_list("session_key", flat=True)
for session in session_ids:
SessionStore(session).delete()
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
sessions.delete()
LOGGER.debug("Deleted user's sessions", user=instance.username)
return response

View File

@ -761,17 +761,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
@property
def component(self) -> str:
"""Return component used to edit this object"""
if self.managed == self.MANAGED_INBUILT:
return ""
raise NotImplementedError
@property
def property_mapping_type(self) -> "type[PropertyMapping]":
"""Return property mapping type used by this object"""
if self.managed == self.MANAGED_INBUILT:
from authentik.core.models import PropertyMapping
return PropertyMapping
raise NotImplementedError
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
@ -786,14 +780,10 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a user to build final properties upon."""
if self.managed == self.MANAGED_INBUILT:
return {}
raise NotImplementedError
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a group to build final properties upon."""
if self.managed == self.MANAGED_INBUILT:
return {}
raise NotImplementedError
def __str__(self):

View File

@ -1,10 +1,7 @@
"""authentik core signals"""
from importlib import import_module
from django.conf import settings
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.base import SessionBase
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
@ -28,7 +25,6 @@ password_changed = Signal()
login_failed = Signal()
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
@receiver(post_save, sender=Application)
@ -64,7 +60,8 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
@receiver(pre_delete, sender=AuthenticatedSession)
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
"""Delete session when authenticated session is deleted"""
SessionStore(instance.session_key).delete()
cache_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key)
@receiver(pre_save)

View File

@ -36,7 +36,6 @@ from authentik.flows.planner import (
)
from authentik.flows.stage import StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
from authentik.lib.utils.urls import is_url_absolute
from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_values
@ -49,7 +48,6 @@ LOGGER = get_logger()
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
SESSION_KEY_SOURCE_FLOW_CONTEXT = "authentik/flows/source_flow_context"
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
@ -210,8 +208,6 @@ class SourceFlowManager:
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
if not is_url_absolute(final_redirect):
final_redirect = "authentik_core:if-user"
flow_context.update(
{
# Since we authenticate the user by their token, they have no backend set
@ -265,7 +261,6 @@ class SourceFlowManager:
plan.append_stage(stage)
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
plan.append_stage(stage)
plan.context.update(self.request.session.get(SESSION_KEY_SOURCE_FLOW_CONTEXT, {}))
return plan.to_redirect(self.request, flow)
def handle_auth(

View File

@ -1,19 +0,0 @@
from django.apps import apps
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user
class TestSourceAPI(APITestCase):
def setUp(self) -> None:
self.user = create_test_admin_user()
self.client.force_login(self.user)
def test_builtin_source_used_by(self):
"""Test Providers's types endpoint"""
apps.get_app_config("authentik_core").source_inbuilt()
response = self.client.get(
reverse("authentik_api:source-used-by", kwargs={"slug": "authentik-built-in"}),
)
self.assertEqual(response.status_code, 200)

View File

@ -1,7 +1,6 @@
"""Test Users API"""
from datetime import datetime
from json import loads
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
@ -16,12 +15,7 @@ from authentik.core.models import (
User,
UserTypes,
)
from authentik.core.tests.utils import (
create_test_admin_user,
create_test_brand,
create_test_flow,
create_test_user,
)
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.flows.models import FlowDesignation
from authentik.lib.generators import generate_id, generate_key
from authentik.stages.email.models import EmailStage
@ -32,7 +26,7 @@ class TestUsersAPI(APITestCase):
def setUp(self) -> None:
self.admin = create_test_admin_user()
self.user = create_test_user()
self.user = User.objects.create(username="test-user")
def test_filter_type(self):
"""Test API filtering by type"""
@ -47,35 +41,6 @@ class TestUsersAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
def test_filter_is_superuser(self):
"""Test API filtering by superuser status"""
User.objects.all().delete()
admin = create_test_admin_user()
self.client.force_login(admin)
# Test superuser
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"is_superuser": True,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 1)
self.assertEqual(body["results"][0]["username"], admin.username)
# Test non-superuser
user = create_test_user()
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"is_superuser": False,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 1, body)
self.assertEqual(body["results"][0]["username"], user.username)
def test_list_with_groups(self):
"""Test listing with groups"""
self.client.force_login(self.admin)
@ -134,8 +99,6 @@ class TestUsersAPI(APITestCase):
def test_recovery_email_no_flow(self):
"""Test user recovery link (no recovery flow set)"""
self.client.force_login(self.admin)
self.user.email = ""
self.user.save()
response = self.client.post(
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
)

View File

@ -11,14 +11,13 @@ from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Source, User
from authentik.core.sources.flow_manager import (
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
SESSION_KEY_SOURCE_FLOW_CONTEXT,
SESSION_KEY_SOURCE_FLOW_STAGES,
)
from authentik.core.types import UILoginButton
from authentik.enterprise.stages.source.models import SourceStage
from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.models import FlowToken, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_IS_REDIRECTED, PLAN_CONTEXT_IS_RESTORED
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
from authentik.flows.stage import ChallengeStageView, StageView
from authentik.lib.utils.time import timedelta_from_string
@ -54,9 +53,6 @@ class SourceStageView(ChallengeStageView):
resume_token = self.create_flow_token()
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
self.request.session[SESSION_KEY_SOURCE_FLOW_CONTEXT] = {
PLAN_CONTEXT_IS_REDIRECTED: self.executor.flow,
}
return self.login_button.challenge
def create_flow_token(self) -> FlowToken:

View File

@ -69,7 +69,6 @@ SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
SESSION_KEY_GET = "authentik/flows/get"
SESSION_KEY_POST = "authentik/flows/post"
SESSION_KEY_HISTORY = "authentik/flows/history"
SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started"
QS_KEY_TOKEN = "flow_token" # nosec
QS_QUERY = "query"
@ -454,7 +453,6 @@ class FlowExecutorView(APIView):
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN,
SESSION_KEY_GET,
SESSION_KEY_AUTH_STARTED,
# We might need the initial POST payloads for later requests
# SESSION_KEY_POST,
# We don't delete the history on purpose, as a user might

View File

@ -6,22 +6,14 @@ from django.shortcuts import get_object_or_404
from ua_parser.user_agent_parser import Parse
from authentik.core.views.interface import InterfaceView
from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED
from authentik.flows.models import Flow
class FlowInterfaceView(InterfaceView):
"""Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["flow"] = flow
if (
not self.request.user.is_authenticated
and flow.designation == FlowDesignation.AUTHENTICATION
):
self.request.session[SESSION_KEY_AUTH_STARTED] = True
self.request.session.save()
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs)

View File

@ -1,20 +1,5 @@
# authentik configuration
#
# https://docs.goauthentik.io/docs/install-config/configuration/
#
# To override the settings in this file, run the following command from the repository root:
#
# ```shell
# make gen-dev-config
# ```
#
# You may edit the generated file to override the configuration below.
#
# When making modifying the default configuration file,
# ensure that the corresponding documentation is updated to match.
#
# @see {@link ../../website/docs/install-config/configuration/configuration.mdx Configuration documentation} for more information.
# update website/docs/install-config/configuration/configuration.mdx
# This is the default configuration file
postgresql:
host: localhost
name: authentik

View File

@ -18,15 +18,6 @@ class SerializerModel(models.Model):
@property
def serializer(self) -> type[BaseSerializer]:
"""Get serializer for this model"""
# Special handling for built-in source
if (
hasattr(self, "managed")
and hasattr(self, "MANAGED_INBUILT")
and self.managed == self.MANAGED_INBUILT
):
from authentik.core.api.sources import SourceSerializer
return SourceSerializer
raise NotImplementedError

View File

@ -13,7 +13,6 @@ from paramiko.ssh_exception import SSHException
from structlog.stdlib import get_logger
from yaml import safe_dump
from authentik import __version__
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
@ -185,7 +184,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}:latest"
self.client.images.pull(image)
return image

View File

@ -35,4 +35,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
label = "authentik_policies"
verbose_name = "authentik Policies"
default = True
mountpoint = "policy/"

View File

@ -1,89 +0,0 @@
{% extends 'login/base_full.html' %}
{% load static %}
{% load i18n %}
{% block head %}
{{ block.super }}
<script>
let redirecting = false;
const checkAuth = async () => {
if (redirecting) return true;
const url = "{{ check_auth_url }}";
console.debug("authentik/policies/buffer: Checking authentication...");
try {
const result = await fetch(url, {
method: "HEAD",
});
if (result.status >= 400) {
return false
}
console.debug("authentik/policies/buffer: Continuing");
redirecting = true;
if ("{{ auth_req_method }}" === "post") {
document.querySelector("form").submit();
} else {
window.location.assign("{{ continue_url|escapejs }}");
}
} catch {
return false;
}
};
let timeout = 100;
let offset = 20;
let attempt = 0;
const main = async () => {
attempt += 1;
await checkAuth();
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
setTimeout(main, timeout);
timeout += (offset * attempt);
if (timeout >= 2000) {
timeout = 2000;
}
}
document.addEventListener("visibilitychange", async () => {
if (document.hidden) return;
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
await checkAuth();
});
main();
</script>
{% endblock %}
{% block title %}
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
{% endblock %}
{% block card_title %}
{% trans 'Waiting for authentication...' %}
{% endblock %}
{% block card %}
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
{% if auth_req_method == "post" %}
{% for key, value in auth_req_body.items %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
{% endif %}
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<div class="pf-c-empty-state__icon">
<span class="pf-c-spinner pf-m-xl" role="progressbar">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
<h1 class="pf-c-title pf-m-lg">
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
</h1>
</div>
</div>
<div class="pf-c-form__group pf-m-action">
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
{% trans "Authenticate in this tab" %}
</a>
</div>
</form>
{% endblock %}

View File

@ -1,121 +0,0 @@
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.http import HttpResponse
from django.test import RequestFactory, TestCase
from django.urls import reverse
from authentik.core.models import Application, Provider
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.flows.models import FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import dummy_get_response
from authentik.policies.views import (
QS_BUFFER_ID,
SESSION_KEY_BUFFER,
BufferedPolicyAccessView,
BufferView,
PolicyAccessView,
)
class TestPolicyViews(TestCase):
"""Test PolicyAccessView"""
def setUp(self):
super().setUp()
self.factory = RequestFactory()
self.user = create_test_user()
def test_pav(self):
"""Test simple policy access view"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
class TestView(PolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/")
req.user = self.user
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.content, b"foo")
def test_pav_buffer(self):
"""Test simple policy access view"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
class TestView(BufferedPolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
req.session.save()
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 302)
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
def test_pav_buffer_skip(self):
"""Test simple policy access view (skip buffer)"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
class TestView(BufferedPolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/?skip_buffer=true")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
req.session.save()
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 302)
self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication")))
def test_buffer(self):
"""Test buffer view"""
uid = generate_id()
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
ts = generate_id()
req.session[SESSION_KEY_BUFFER % uid] = {
"method": "get",
"body": {},
"url": f"/{ts}",
}
req.session.save()
res = BufferView.as_view()(req)
self.assertEqual(res.status_code, 200)
self.assertIn(ts, res.render().content.decode())

View File

@ -1,14 +1,7 @@
"""API URLs"""
from django.urls import path
from authentik.policies.api.bindings import PolicyBindingViewSet
from authentik.policies.api.policies import PolicyViewSet
from authentik.policies.views import BufferView
urlpatterns = [
path("buffer", BufferView.as_view(), name="buffer"),
]
api_urlpatterns = [
("policies/all", PolicyViewSet),

View File

@ -1,37 +1,23 @@
"""authentik access helper classes"""
from typing import Any
from uuid import uuid4
from django.contrib import messages
from django.contrib.auth.mixins import AccessMixin
from django.contrib.auth.views import redirect_to_login
from django.http import HttpRequest, HttpResponse, QueryDict
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import urlencode
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from django.views.generic.base import TemplateView, View
from django.views.generic.base import View
from structlog.stdlib import get_logger
from authentik.core.models import Application, Provider, User
from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import (
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_AUTH_STARTED,
SESSION_KEY_PLAN,
SESSION_KEY_POST,
)
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger()
QS_BUFFER_ID = "af_bf_id"
QS_SKIP_BUFFER = "skip_buffer"
SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s"
class RequestValidationError(SentryIgnoredException):
@ -139,65 +125,3 @@ class PolicyAccessView(AccessMixin, View):
for message in result.messages:
messages.error(self.request, _(message))
return result
def url_with_qs(url: str, **kwargs):
"""Update/set querystring of `url` with the parameters in `kwargs`. Original query string
parameters are retained"""
if "?" not in url:
return url + f"?{urlencode(kwargs)}"
url, _, qs = url.partition("?")
qs = QueryDict(qs, mutable=True)
qs.update(kwargs)
return url + f"?{urlencode(qs.items())}"
class BufferView(TemplateView):
"""Buffer view"""
template_name = "policies/buffer.html"
def get_context_data(self, **kwargs):
buf_id = self.request.GET.get(QS_BUFFER_ID)
buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id)
kwargs["auth_req_method"] = buffer["method"]
kwargs["auth_req_body"] = buffer["body"]
kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True})
kwargs["check_auth_url"] = reverse("authentik_api:user-me")
kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id})
return super().get_context_data(**kwargs)
class BufferedPolicyAccessView(PolicyAccessView):
"""PolicyAccessView which buffers access requests in case the user is not logged in"""
def handle_no_permission(self):
plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN)
authenticating = self.request.session.get(SESSION_KEY_AUTH_STARTED)
if plan:
flow = Flow.objects.filter(pk=plan.flow_pk).first()
if not flow or flow.designation != FlowDesignation.AUTHENTICATION:
LOGGER.debug("Not buffering request, no flow or flow not for authentication")
return super().handle_no_permission()
if not plan and authenticating is None:
LOGGER.debug("Not buffering request, no flow plan active")
return super().handle_no_permission()
if self.request.GET.get(QS_SKIP_BUFFER):
LOGGER.debug("Not buffering request, explicit skip")
return super().handle_no_permission()
buffer_id = str(uuid4())
LOGGER.debug("Buffering access request", bf_id=buffer_id)
self.request.session[SESSION_KEY_BUFFER % buffer_id] = {
"body": self.request.POST,
"url": self.request.build_absolute_uri(self.request.get_full_path()),
"method": self.request.method.lower(),
}
return redirect(
url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id})
)
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if QS_BUFFER_ID in self.request.GET:
self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None)
return response

View File

@ -30,7 +30,7 @@ from authentik.flows.stage import StageView
from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.views import bad_request_message
from authentik.policies.types import PolicyRequest
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError
from authentik.policies.views import PolicyAccessView, RequestValidationError
from authentik.providers.oauth2.constants import (
PKCE_METHOD_PLAIN,
PKCE_METHOD_S256,
@ -328,7 +328,7 @@ class OAuthAuthorizationParams:
return code
class AuthorizationFlowInitView(BufferedPolicyAccessView):
class AuthorizationFlowInitView(PolicyAccessView):
"""OAuth2 Flow initializer, checks access to application and starts flow"""
params: OAuthAuthorizationParams

View File

@ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStage
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine
from authentik.policies.views import BufferedPolicyAccessView
from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
class RACStartView(BufferedPolicyAccessView):
class RACStartView(PolicyAccessView):
"""Start a RAC connection by checking access and creating a connection token"""
endpoint: Endpoint

View File

@ -1,22 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-31 13:50
import authentik.lib.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0017_samlprovider_authn_context_class_ref_mapping"),
]
operations = [
migrations.AlterField(
model_name="samlprovider",
name="acs_url",
field=models.TextField(
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="ACS URL",
),
),
]

View File

@ -10,7 +10,6 @@ from structlog.stdlib import get_logger
from authentik.core.api.object_types import CreatableType
from authentik.core.models import PropertyMapping, Provider
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator
from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import (
DSA_SHA1,
@ -41,9 +40,7 @@ class SAMLBindings(models.TextChoices):
class SAMLProvider(Provider):
"""SAML 2.0 Endpoint for applications which support SAML."""
acs_url = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))], verbose_name=_("ACS URL")
)
acs_url = models.URLField(verbose_name=_("ACS URL"))
audience = models.TextField(
default="",
blank=True,

View File

@ -15,7 +15,7 @@ from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_POST
from authentik.lib.views import bad_request_message
from authentik.policies.views import BufferedPolicyAccessView
from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
@ -35,7 +35,7 @@ from authentik.stages.consent.stage import (
LOGGER = get_logger()
class SAMLSSOView(BufferedPolicyAccessView):
class SAMLSSOView(PolicyAccessView):
"""SAML SSO Base View, which plans a flow and injects our final stage.
Calls get/post handler."""
@ -83,7 +83,7 @@ class SAMLSSOView(BufferedPolicyAccessView):
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""GET and POST use the same handler, but we can't
override .dispatch easily because BufferedPolicyAccessView's dispatch"""
override .dispatch easily because PolicyAccessView's dispatch"""
return self.get(request, application_slug)

View File

@ -99,6 +99,7 @@ class LDAPSourceSerializer(SourceSerializer):
"sync_groups",
"sync_parent_group",
"connectivity",
"lookup_groups_from_user",
]
extra_kwargs = {"bind_password": {"write_only": True}}
@ -134,6 +135,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"sync_parent_group",
"user_property_mappings",
"group_property_mappings",
"lookup_groups_from_user",
]
search_fields = ["name", "slug"]
ordering = ["name"]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.0.13 on 2025-03-26 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_sources_ldap",
"0006_rename_ldappropertymapping_ldapsourcepropertymapping_and_more",
),
]
operations = [
migrations.AddField(
model_name="ldapsource",
name="lookup_groups_from_user",
field=models.BooleanField(
default=False,
help_text="Lookup group membership based on a user attribute instead of a group attribute. This allows nested group resolution on systems like FreeIPA and Active Directory",
),
),
]

View File

@ -123,6 +123,14 @@ class LDAPSource(Source):
Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT
)
lookup_groups_from_user = models.BooleanField(
default=False,
help_text=_(
"Lookup group membership based on a user attribute instead of a group attribute. "
"This allows nested group resolution on systems like FreeIPA and Active Directory"
),
)
@property
def component(self) -> str:
return "ak-source-ldap-form"

View File

@ -28,15 +28,17 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
if not self._source.sync_groups:
self.message("Group syncing is disabled for this Source")
return iter(())
# If we are looking up groups from users, we don't need to fetch the group membership field
attributes = [self._source.object_uniqueness_field, LDAP_DISTINGUISHED_NAME]
if not self._source.lookup_groups_from_user:
attributes.append(self._source.group_membership_field)
return self.search_paginator(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=SUBTREE,
attributes=[
self._source.group_membership_field,
self._source.object_uniqueness_field,
LDAP_DISTINGUISHED_NAME,
],
attributes=attributes,
**kwargs,
)
@ -47,9 +49,24 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
return -1
membership_count = 0
for group in page_data:
if "attributes" not in group:
continue
members = group.get("attributes", {}).get(self._source.group_membership_field, [])
if self._source.lookup_groups_from_user:
group_dn = group.get("dn", {})
group_filter = f"({self._source.group_membership_field}={group_dn})"
group_members = self._source.connection().extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=group_filter,
search_scope=SUBTREE,
attributes=[self._source.object_uniqueness_field],
)
members = []
for group_member in group_members:
group_member_dn = group_member.get("dn", {})
members.append(group_member_dn)
else:
if "attributes" not in group:
continue
members = group.get("attributes", {}).get(self._source.group_membership_field, [])
ak_group = self.get_group(group)
if not ak_group:
continue
@ -68,7 +85,7 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
"ak_groups__in": [ak_group],
}
)
)
).distinct()
membership_count += 1
membership_count += users.count()
ak_group.users.set(users)

View File

@ -96,6 +96,26 @@ def mock_freeipa_connection(password: str) -> Connection:
"objectClass": "posixAccount",
},
)
# User with groups in memberOf attribute
connection.strategy.add_entry(
"cn=user4,ou=users,dc=goauthentik,dc=io",
{
"name": "user4_sn",
"uid": "user4_sn",
"objectClass": "person",
"memberOf": [
"cn=reverse-lookup-group,ou=groups,dc=goauthentik,dc=io",
],
},
)
connection.strategy.add_entry(
"cn=reverse-lookup-group,ou=groups,dc=goauthentik,dc=io",
{
"cn": "reverse-lookup-group",
"uid": "reverse-lookup-group",
"objectClass": "groupOfNames",
},
)
# Locked out user
connection.strategy.add_entry(
"cn=user-nsaccountlock,ou=users,dc=goauthentik,dc=io",

View File

@ -162,6 +162,43 @@ class LDAPSyncTests(TestCase):
self.assertFalse(User.objects.filter(username="user1_sn").exists())
self.assertFalse(User.objects.get(username="user-nsaccountlock").is_active)
def test_sync_groups_freeipa_memberOf(self):
"""Test group sync when membership is derived from memberOf user attribute"""
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.lookup_groups_from_user = True
self.source.group_membership_field = "memberOf"
self.source.user_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default")
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
)
)
self.source.group_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter(
managed="goauthentik.io/sources/ldap/openldap-cn"
)
)
connection = MagicMock(return_value=mock_freeipa_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
user_sync = UserLDAPSynchronizer(self.source)
user_sync.sync_full()
group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync_full()
membership_sync = MembershipLDAPSynchronizer(self.source)
membership_sync.sync_full()
self.assertTrue(
User.objects.filter(username="user4_sn").exists(), "User does not exist"
)
# Test if membership mapping based on memberOf works.
memberof_group = Group.objects.filter(name="reverse-lookup-group")
self.assertTrue(memberof_group.exists(), "Group does not exist")
self.assertTrue(
memberof_group.first().users.filter(username="user4_sn").exists(),
"User not a member of the group",
)
def test_sync_groups_ad(self):
"""Test group sync"""
self.source.user_property_mappings.set(

View File

@ -1,35 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-31 13:53
import authentik.lib.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_saml", "0017_fix_x509subjectname"),
]
operations = [
migrations.AlterField(
model_name="samlsource",
name="slo_url",
field=models.TextField(
blank=True,
default=None,
help_text="Optional URL if your IDP supports Single-Logout.",
null=True,
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="SLO URL",
),
),
migrations.AlterField(
model_name="samlsource",
name="sso_url",
field=models.TextField(
help_text="URL that the initial Login request is sent to.",
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="SSO URL",
),
),
]

View File

@ -20,7 +20,6 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.models import Flow
from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.models import DomainlessURLValidator
from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import (
DSA_SHA1,
@ -92,13 +91,11 @@ class SAMLSource(Source):
help_text=_("Also known as Entity ID. Defaults the Metadata URL."),
)
sso_url = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))],
sso_url = models.URLField(
verbose_name=_("SSO URL"),
help_text=_("URL that the initial Login request is sent to."),
)
slo_url = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))],
slo_url = models.URLField(
default=None,
blank=True,
null=True,

View File

@ -33,7 +33,6 @@ from authentik.flows.planner import (
)
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import is_url_absolute
from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
@ -74,8 +73,6 @@ class InitiateView(View):
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
if not is_url_absolute(final_redirect):
final_redirect = "authentik_core:if-user"
kwargs.update(
{
PLAN_CONTEXT_SSO: True,

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@ from django.core.mail.backends.locmem import EmailBackend
from django.urls import reverse
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding
@ -67,36 +67,6 @@ class TestEmailStageSending(FlowTestCase):
self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"])
self.assertEqual(event.context["from_email"], "system@authentik.local")
def test_newlines_long_name(self):
"""Test with pending user"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
long_user = create_test_user()
long_user.name = "Test User\r\n Many Words\r\n"
long_user.save()
plan.context[PLAN_CONTEXT_PENDING_USER] = long_user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
Event.objects.filter(action=EventAction.EMAIL_SENT).delete()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
with patch(
"authentik.stages.email.models.EmailStage.backend_class",
PropertyMock(return_value=EmailBackend),
):
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
},
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik")
self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"])
def test_pending_fake_user(self):
"""Test with pending (fake) user"""
self.flow.designation = FlowDesignation.RECOVERY

View File

@ -32,14 +32,7 @@ class TemplateEmailMessage(EmailMultiAlternatives):
sanitized_to = []
# Ensure that all recipients are valid
for recipient_name, recipient_email in to:
# Remove any newline characters from name and email before sanitizing
clean_name = (
recipient_name.replace("\n", " ").replace("\r", " ") if recipient_name else ""
)
clean_email = (
recipient_email.replace("\n", "").replace("\r", "") if recipient_email else ""
)
sanitized_to.append(sanitize_address((clean_name, clean_email), "utf-8"))
sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8"))
super().__init__(to=sanitized_to, **kwargs)
if not template_name:
return

View File

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

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2025.2.3 Blueprint schema",
"title": "authentik 2025.2.2 Blueprint schema",
"required": [
"version",
"entries"
@ -6423,6 +6423,8 @@
},
"acs_url": {
"type": "string",
"format": "uri",
"maxLength": 200,
"minLength": 1,
"title": "ACS URL"
},
@ -7885,6 +7887,11 @@
"type": "string",
"format": "uuid",
"title": "Sync parent group"
},
"lookup_groups_from_user": {
"type": "boolean",
"title": "Lookup groups from user",
"description": "Lookup group membership based on a user attribute instead of a group attribute. This allows nested group resolution on systems like FreeIPA and Active Directory"
}
},
"required": []
@ -8731,6 +8738,8 @@
},
"sso_url": {
"type": "string",
"format": "uri",
"maxLength": 200,
"minLength": 1,
"title": "SSO URL",
"description": "URL that the initial Login request is sent to."
@ -8740,6 +8749,8 @@
"string",
"null"
],
"format": "uri",
"maxLength": 200,
"title": "SLO URL",
"description": "Optional URL if your IDP supports Single-Logout."
},

View File

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

7
go.mod
View File

@ -1,6 +1,9 @@
module goauthentik.io
go 1.24.0
go 1.23.0
toolchain go1.24.0
require (
beryju.io/ldap v0.1.0
github.com/coreos/go-oidc/v3 v3.13.0
@ -26,7 +29,7 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025023.2
goauthentik.io/api/v3 v3.2025022.6
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0

4
go.sum
View File

@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025023.2 h1:4XHlnykN5jQH78liQ4cp2Jf8eigvQImIJp+A+bsq1nA=
goauthentik.io/api/v3 v3.2025023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2025022.6 h1:M5M8Cd/1N7E8KLkvYYh7VdcdKz5nfzjKPFLK+YOtOVg=
goauthentik.io/api/v3 v3.2025022.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -162,14 +162,13 @@ func (c *Config) parseScheme(rawVal string) string {
if err != nil {
return rawVal
}
switch u.Scheme {
case "env":
if u.Scheme == "env" {
e, ok := os.LookupEnv(u.Host)
if ok {
return e
}
return u.RawQuery
case "file":
} else if u.Scheme == "file" {
d, err := os.ReadFile(u.Path)
if err != nil {
return u.RawQuery

View File

@ -10,7 +10,7 @@ import (
)
func TestConfigEnv(t *testing.T) {
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "bar"))
os.Setenv("AUTHENTIK_SECRET_KEY", "bar")
cfg = nil
if err := Get().fromEnv(); err != nil {
panic(err)
@ -19,8 +19,8 @@ func TestConfigEnv(t *testing.T) {
}
func TestConfigEnv_Scheme(t *testing.T) {
assert.NoError(t, os.Setenv("foo", "bar"))
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo"))
os.Setenv("foo", "bar")
os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo")
cfg = nil
if err := Get().fromEnv(); err != nil {
panic(err)
@ -33,15 +33,13 @@ func TestConfigEnv_File(t *testing.T) {
if err != nil {
log.Fatal(err)
}
defer func() {
assert.NoError(t, os.Remove(file.Name()))
}()
defer os.Remove(file.Name())
_, err = file.Write([]byte("bar"))
if err != nil {
panic(err)
}
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name())))
os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name()))
cfg = nil
if err := Get().fromEnv(); err != nil {
panic(err)

View File

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

View File

@ -0,0 +1,5 @@
//go:build requirefips
package backend
var FipsEnabled = true

View File

@ -0,0 +1,5 @@
//go:build !requirefips
package backend
var FipsEnabled = false

View File

@ -35,7 +35,7 @@ func EnableDebugServer() {
if err != nil {
return nil
}
_, err = fmt.Fprintf(w, "<a href='%[1]s'>%[1]s</a><br>", tpl)
_, err = w.Write([]byte(fmt.Sprintf("<a href='%[1]s'>%[1]s</a><br>", tpl)))
if err != nil {
l.WithError(err).Warning("failed to write index")
return nil

View File

@ -44,11 +44,10 @@ func New(healthcheck func() bool) *GoUnicorn {
signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2)
go func() {
for sig := range c {
switch sig {
case syscall.SIGHUP:
if sig == syscall.SIGHUP {
g.log.Info("SIGHUP received, forwarding to gunicorn")
g.Reload()
case syscall.SIGUSR2:
} else if sig == syscall.SIGUSR2 {
g.log.Info("SIGUSR2 received, restarting gunicorn")
g.Restart()
}

View File

@ -2,7 +2,6 @@ package ak
import (
"context"
"crypto/fips140"
"fmt"
"math/rand"
"net/http"
@ -204,7 +203,7 @@ func (a *APIController) getWebsocketPingArgs() map[string]interface{} {
"golangVersion": runtime.Version(),
"opensslEnabled": cryptobackend.OpensslEnabled,
"opensslVersion": cryptobackend.OpensslVersion(),
"fipsEnabled": fips140.Enabled(),
"fipsEnabled": cryptobackend.FipsEnabled,
}
hostname, err := os.Hostname()
if err == nil {

View File

@ -35,19 +35,13 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
req PaginatorRequest[Treq, Tres],
opts PaginatorOptions,
) ([]Tobj, error) {
if opts.Logger == nil {
opts.Logger = log.NewEntry(log.StandardLogger())
}
var bfreq, cfreq interface{}
fetchOffset := func(page int32) (Tres, error) {
bfreq = req.Page(page)
cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
res, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
if err != nil {
opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
if hres != nil && hres.StatusCode >= 400 && hres.StatusCode < 500 {
return res, err
}
}
return res, err
}
@ -57,9 +51,6 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
for {
apiObjects, err := fetchOffset(page)
if err != nil {
if page == 1 {
return objects, err
}
errs = append(errs, err)
continue
}

View File

@ -1,64 +1,5 @@
package ak
import (
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"goauthentik.io/api/v3"
)
type fakeAPIType struct{}
type fakeAPIResponse struct {
results []fakeAPIType
pagination api.Pagination
}
func (fapi *fakeAPIResponse) GetResults() []fakeAPIType { return fapi.results }
func (fapi *fakeAPIResponse) GetPagination() api.Pagination { return fapi.pagination }
type fakeAPIRequest struct {
res *fakeAPIResponse
http *http.Response
err error
}
func (fapi *fakeAPIRequest) Page(page int32) *fakeAPIRequest { return fapi }
func (fapi *fakeAPIRequest) PageSize(size int32) *fakeAPIRequest { return fapi }
func (fapi *fakeAPIRequest) Execute() (*fakeAPIResponse, *http.Response, error) {
return fapi.res, fapi.http, fapi.err
}
func Test_Simple(t *testing.T) {
req := &fakeAPIRequest{
res: &fakeAPIResponse{
results: []fakeAPIType{
{},
},
pagination: api.Pagination{
TotalPages: 1,
},
},
}
res, err := Paginator(req, PaginatorOptions{})
assert.NoError(t, err)
assert.Len(t, res, 1)
}
func Test_BadRequest(t *testing.T) {
req := &fakeAPIRequest{
http: &http.Response{
StatusCode: 400,
},
err: errors.New("foo"),
}
res, err := Paginator(req, PaginatorOptions{})
assert.Error(t, err)
assert.Equal(t, []fakeAPIType{}, res)
}
// func Test_PaginatorCompile(t *testing.T) {
// req := api.ApiCoreUsersListRequest{}
// Paginator(req, PaginatorOptions{

View File

@ -148,8 +148,7 @@ func (ac *APIController) startWSHandler() {
"outpost_type": ac.Server.Type(),
"uuid": ac.instanceUUID.String(),
}).Set(1)
switch wsMsg.Instruction {
case WebsocketInstructionTriggerUpdate:
if wsMsg.Instruction == WebsocketInstructionTriggerUpdate {
time.Sleep(ac.reloadOffset)
logger.Debug("Got update trigger...")
err := ac.OnRefresh()
@ -164,7 +163,7 @@ func (ac *APIController) startWSHandler() {
"build": constants.BUILD(""),
}).SetToCurrentTime()
}
case WebsocketInstructionProviderSpecific:
} else if wsMsg.Instruction == WebsocketInstructionProviderSpecific {
for _, h := range ac.wsHandlers {
h(context.Background(), wsMsg.Args)
}

View File

@ -66,12 +66,7 @@ func (ls *LDAPServer) StartLDAPServer() error {
return err
}
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() {
err := proxyListener.Close()
if err != nil {
ls.log.WithError(err).Warning("failed to close proxy listener")
}
}()
defer proxyListener.Close()
ls.log.WithField("listen", listen).Info("Starting LDAP server")
err = ls.s.Serve(proxyListener)

View File

@ -49,12 +49,7 @@ func (ls *LDAPServer) StartLDAPTLSServer() error {
}
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() {
err := proxyListener.Close()
if err != nil {
ls.log.WithError(err).Warning("failed to close proxy listener")
}
}()
defer proxyListener.Close()
tln := tls.NewListener(proxyListener, tlsConfig)

View File

@ -98,7 +98,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
entries := make([]*ldap.Entry, 0)
scope := req.Scope
scope := req.SearchRequest.Scope
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {

View File

@ -56,7 +56,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo
if !embedded && hostBrowser == "" {
return ep
}
var newHost = aku
var newHost *url.URL = aku
var newBrowserHost *url.URL
if embedded {
if authentikHost == "" {

View File

@ -130,12 +130,7 @@ func (ps *ProxyServer) ServeHTTP() {
return
}
proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() {
err := proxyListener.Close()
if err != nil {
ps.log.WithError(err).Warning("failed to close proxy listener")
}
}()
defer proxyListener.Close()
ps.log.WithField("listen", listenAddress).Info("Starting HTTP server")
ps.serve(proxyListener)
@ -154,12 +149,7 @@ func (ps *ProxyServer) ServeHTTPS() {
return
}
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() {
err := proxyListener.Close()
if err != nil {
ps.log.WithError(err).Warning("failed to close proxy listener")
}
}()
defer proxyListener.Close()
tlsListener := tls.NewListener(proxyListener, tlsConfig)
ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server")

View File

@ -72,13 +72,11 @@ func (s *RedisStore) New(r *http.Request, name string) (*sessions.Session, error
session.ID = c.Value
err = s.load(r.Context(), session)
if err != nil {
if errors.Is(err, redis.Nil) {
return session, nil
}
return session, err
if err == nil {
session.IsNew = false
} else if err == redis.Nil {
err = nil // no data stored
}
session.IsNew = false
return session, err
}

View File

@ -156,12 +156,7 @@ func (ws *WebServer) listenPlain() {
return
}
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() {
err := proxyListener.Close()
if err != nil {
ws.log.WithError(err).Warning("failed to close proxy listener")
}
}()
defer proxyListener.Close()
ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server")
ws.serve(proxyListener)

View File

@ -46,12 +46,7 @@ func (ws *WebServer) listenTLS() {
return
}
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() {
err := proxyListener.Close()
if err != nil {
ws.log.WithError(err).Warning("failed to close proxy listener")
}
}()
defer proxyListener.Close()
tlsListener := tls.NewListener(proxyListener, tlsConfig)
ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Starting HTTPS server")

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
ARG TARGETOS
ARG TARGETARCH
@ -27,7 +27,7 @@ COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/ldap ./cmd/ldap
# Stage 2: Run

View File

@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "MIT",
"devDependencies": {
"aws-cdk": "^2.1006.0",
"aws-cdk": "^2.1005.0",
"cross-env": "^7.0.3"
},
"engines": {
@ -17,9 +17,9 @@
}
},
"node_modules/aws-cdk": {
"version": "2.1006.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1006.0.tgz",
"integrity": "sha512-6qYnCt4mBN+3i/5F+FC2yMETkDHY/IL7gt3EuqKVPcaAO4jU7oXfVSlR60CYRkZWL4fnAurUV14RkJuJyVG/IA==",
"version": "2.1005.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz",
"integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

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

View File

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

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -1220,20 +1220,6 @@ msgstr ""
msgid "Reputation Scores"
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html
msgid "Permission denied"
msgstr ""

View File

@ -10,8 +10,8 @@
# Manuel Viens, 2023
# Mordecai, 2023
# nerdinator <florian.dupret@gmail.com>, 2024
# Tina, 2024
# Charles Leclerc, 2025
# Tina, 2025
# Marc Schmitt, 2025
#
#, fuzzy
@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -1347,22 +1347,6 @@ msgstr "Score de Réputation"
msgid "Reputation Scores"
msgstr "Scores de Réputation"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr "En attente de l'authentification..."
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
"Vous êtes déjà en cours d'authentification dans un autre onglet. Cette page "
"se rafraîchira lorsque l'authentification sera terminée."
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr "S'authentifier dans cet onglet"
#: authentik/policies/templates/policies/denied.html
msgid "Permission denied"
msgstr "Permission refusée"

Binary file not shown.

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -1234,20 +1234,6 @@ msgstr "信誉分数"
msgid "Reputation Scores"
msgstr "信誉分数"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr "正在等待身份验证…"
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr "您正在另一个标签页中验证身份。身份验证完成后,此页面会刷新。"
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr "在此标签页中验证身份"
#: authentik/policies/templates/policies/denied.html
msgid "Permission denied"
msgstr "权限被拒绝"

View File

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

View File

@ -17,7 +17,7 @@ COPY web .
RUN npm run build-proxy
# Stage 2: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
ARG TARGETOS
ARG TARGETARCH
@ -43,7 +43,7 @@ COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/proxy ./cmd/proxy
# Stage 3: Run

View File

@ -1,6 +1,6 @@
[project]
name = "authentik"
version = "2025.2.3"
version = "2025.2.2"
description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.12.*"
@ -103,7 +103,7 @@ dev = [
[tool.uv.sources]
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" }
opencontainers = { git = "https://github.com/BeryJu/oci-python", rev = "c791b19056769cd67957322806809ab70f5bead8" }
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf" }
[project.scripts]
ak = "lifecycle.ak:main"

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
ARG TARGETOS
ARG TARGETARCH
@ -27,7 +27,7 @@ COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/rac ./cmd/rac
# Stage 2: Run

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
ARG TARGETOS
ARG TARGETARCH
@ -27,7 +27,7 @@ COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/radius ./cmd/radius
# Stage 2: Run

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2025.2.3
version: 2025.2.2
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -27348,6 +27348,10 @@ paths:
format: uuid
explode: true
style: form
- in: query
name: lookup_groups_from_user
schema:
type: boolean
- in: query
name: name
schema:
@ -45967,6 +45971,11 @@ components:
nullable: true
description: Get cached source connectivity
readOnly: true
lookup_groups_from_user:
type: boolean
description: Lookup group membership based on a user attribute instead of
a group attribute. This allows nested group resolution on systems like
FreeIPA and Active Directory
required:
- base_dn
- component
@ -46163,6 +46172,11 @@ components:
type: string
format: uuid
nullable: true
lookup_groups_from_user:
type: boolean
description: Lookup group membership based on a user attribute instead of
a group attribute. This allows nested group resolution on systems like
FreeIPA and Active Directory
required:
- base_dn
- name
@ -51261,6 +51275,11 @@ components:
type: string
format: uuid
nullable: true
lookup_groups_from_user:
type: boolean
description: Lookup group membership based on a user attribute instead of
a group attribute. This allows nested group resolution on systems like
FreeIPA and Active Directory
PatchedLicenseRequest:
type: object
description: License Serializer
@ -52245,8 +52264,9 @@ components:
format: uuid
acs_url:
type: string
minLength: 1
format: uri
minLength: 1
maxLength: 200
audience:
type: string
description: Value of the audience restriction field of the assertion. When
@ -52403,14 +52423,16 @@ components:
description: Also known as Entity ID. Defaults the Metadata URL.
sso_url:
type: string
format: uri
minLength: 1
description: URL that the initial Login request is sent to.
format: uri
maxLength: 200
slo_url:
type: string
format: uri
nullable: true
description: Optional URL if your IDP supports Single-Logout.
format: uri
maxLength: 200
allow_idp_initiated:
type: boolean
description: Allows authentication flows initiated by the IdP. This can
@ -55211,6 +55233,7 @@ components:
acs_url:
type: string
format: uri
maxLength: 200
audience:
type: string
description: Value of the audience restriction field of the assertion. When
@ -55377,8 +55400,9 @@ components:
format: uuid
acs_url:
type: string
minLength: 1
format: uri
minLength: 1
maxLength: 200
audience:
type: string
description: Value of the audience restriction field of the assertion. When
@ -55551,13 +55575,15 @@ components:
description: Also known as Entity ID. Defaults the Metadata URL.
sso_url:
type: string
description: URL that the initial Login request is sent to.
format: uri
description: URL that the initial Login request is sent to.
maxLength: 200
slo_url:
type: string
format: uri
nullable: true
description: Optional URL if your IDP supports Single-Logout.
format: uri
maxLength: 200
allow_idp_initiated:
type: boolean
description: Allows authentication flows initiated by the IdP. This can
@ -55740,14 +55766,16 @@ components:
description: Also known as Entity ID. Defaults the Metadata URL.
sso_url:
type: string
format: uri
minLength: 1
description: URL that the initial Login request is sent to.
format: uri
maxLength: 200
slo_url:
type: string
format: uri
nullable: true
description: Optional URL if your IDP supports Single-Logout.
format: uri
maxLength: 200
allow_idp_initiated:
type: boolean
description: Allows authentication flows initiated by the IdP. This can

View File

@ -5,85 +5,45 @@ from yaml import safe_dump
from authentik.lib.generators import generate_id
def generate_local_config():
"""Generate a local development configuration"""
# TODO: This should be generated and validated against a schema, such as Pydantic.
return {
"debug": True,
"log_level": "debug",
"secret_key": generate_id(),
"postgresql": {
"user": "postgres",
},
"outposts": {
"container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s",
"disable_embedded_outpost": False,
},
"blueprints_dir": "./blueprints",
"cert_discovery_dir": "./certs",
"events": {
"processors": {
"geoip": "tests/GeoLite2-City-Test.mmdb",
"asn": "tests/GeoLite2-ASN-Test.mmdb",
}
},
"storage": {
"media": {
"backend": "file",
"s3": {
"endpoint": "http://localhost:8020",
"access_key": "accessKey1",
"secret_key": "secretKey1",
"bucket_name": "authentik-media",
"custom_domain": "localhost:8020/authentik-media",
"secure_urls": False,
with open("local.env.yml", "w", encoding="utf-8") as _config:
safe_dump(
{
"debug": True,
"log_level": "debug",
"secret_key": generate_id(),
"postgresql": {
"user": "postgres",
},
"outposts": {
"container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s",
"disable_embedded_outpost": False,
},
"blueprints_dir": "./blueprints",
"cert_discovery_dir": "./certs",
"events": {
"processors": {
"geoip": "tests/GeoLite2-City-Test.mmdb",
"asn": "tests/GeoLite2-ASN-Test.mmdb",
}
},
"storage": {
"media": {
"backend": "file",
"s3": {
"endpoint": "http://localhost:8020",
"access_key": "accessKey1",
"secret_key": "secretKey1",
"bucket_name": "authentik-media",
"custom_domain": "localhost:8020/authentik-media",
"secure_urls": False,
},
},
},
"tenants": {
"enabled": False,
"api_key": generate_id(),
},
},
"tenants": {
"enabled": False,
"api_key": generate_id(),
},
}
if __name__ == "__main__":
config_file_name = "local.env.yml"
with open(config_file_name, "w", encoding="utf-8") as _config:
_config.write(
"""
# Local authentik configuration overrides
#
# https://docs.goauthentik.io/docs/install-config/configuration/
#
# To regenerate this file, run the following command from the repository root:
#
# ```shell
# make gen-dev-config
# ```
"""
)
safe_dump(
generate_local_config(),
_config,
default_flow_style=False,
)
print(
f"""
---
Generated configuration file: {config_file_name}
For more information on how to use this configuration, see:
https://docs.goauthentik.io/docs/install-config/configuration/
---
"""
_config,
default_flow_style=False,
)

View File

@ -410,77 +410,3 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied",
)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint("default/flow-default-provider-authorization-implicit-consent.yaml")
@apply_blueprint("system/providers-oauth2.yaml")
@reconcile_app("authentik_crypto")
def test_authorization_consent_implied_parallel(self):
"""test OpenID Provider flow (default authorization flow with implied consent)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
redirect_uris=[
RedirectURI(
RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
)
],
authorization_flow=authorization_flow,
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
scope_name__in=[
SCOPE_OPENID,
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
SCOPE_OFFLINE_ACCESS,
]
)
)
Application.objects.create(
name=generate_id(),
slug=self.app_slug,
provider=provider,
)
self.driver.get(self.live_server_url)
login_window = self.driver.current_window_handle
self.driver.switch_to.new_window("tab")
grafana_window = self.driver.current_window_handle
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.switch_to.window(login_window)
self.login()
self.driver.switch_to.window(grafana_window)
self.wait_for_url("http://localhost:3000/?orgId=1")
self.driver.get("http://localhost:3000/profile")
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
self.user.name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute("value"),
self.user.name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=email]").get_attribute("value"),
self.user.email,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=login]").get_attribute("value"),
self.user.email,
)

View File

@ -20,7 +20,7 @@ from tests.e2e.utils import SeleniumTestCase, retry
class TestProviderSAML(SeleniumTestCase):
"""test SAML Provider flow"""
def setup_client(self, provider: SAMLProvider, force_post: bool = False, **kwargs):
def setup_client(self, provider: SAMLProvider, force_post: bool = False):
"""Setup client saml-sp container which we test SAML against"""
metadata_url = (
self.url(
@ -40,7 +40,6 @@ class TestProviderSAML(SeleniumTestCase):
"SP_ENTITY_ID": provider.issuer,
"SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
"SP_METADATA_URL": metadata_url,
**kwargs,
},
)
@ -112,74 +111,6 @@ class TestProviderSAML(SeleniumTestCase):
[self.user.email],
)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_sp_initiated_implicit_post(self):
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
acs_url="http://localhost:9009/saml/acs",
audience="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
)
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
Application.objects.create(
name="SAML",
slug="authentik-saml",
provider=provider,
)
self.setup_client(provider, True)
self.driver.get("http://localhost:9009")
self.login()
self.wait_for_url("http://localhost:9009/")
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
[self.user.name],
)
self.assertEqual(
body["attr"][
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
[str(self.user.pk)],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
[self.user.email],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
[self.user.email],
)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
@ -519,81 +450,3 @@ class TestProviderSAML(SeleniumTestCase):
lambda driver: driver.current_url.startswith(should_url),
f"URL {self.driver.current_url} doesn't match expected URL {should_url}",
)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_sp_initiated_implicit_post_buffer(self):
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
acs_url=f"http://{self.host}:9009/saml/acs",
audience="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
)
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
Application.objects.create(
name="SAML",
slug="authentik-saml",
provider=provider,
)
self.setup_client(provider, True, SP_ROOT_URL=f"http://{self.host}:9009")
self.driver.get(self.live_server_url)
login_window = self.driver.current_window_handle
self.driver.switch_to.new_window("tab")
client_window = self.driver.current_window_handle
# We need to access the SP on the same host as the IdP for SameSite cookies
self.driver.get(f"http://{self.host}:9009")
self.driver.switch_to.window(login_window)
self.login()
self.driver.switch_to.window(client_window)
self.wait_for_url(f"http://{self.host}:9009/")
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
[self.user.name],
)
self.assertEqual(
body["attr"][
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
[str(self.user.pk)],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
[self.user.email],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
[self.user.email],
)

13
uv.lock generated
View File

@ -162,7 +162,7 @@ wheels = [
[[package]]
name = "authentik"
version = "2025.2.3"
version = "2025.2.2"
source = { editable = "." }
dependencies = [
{ name = "argon2-cffi" },
@ -302,7 +302,7 @@ requires-dist = [
{ name = "ldap3" },
{ name = "lxml" },
{ name = "msgraph-sdk" },
{ name = "opencontainers", git = "https://github.com/BeryJu/oci-python?rev=c791b19056769cd67957322806809ab70f5bead8" },
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=20d69d9cc50a0fef31605b46f06da0c94f1ec3cf" },
{ name = "packaging" },
{ name = "paramiko" },
{ name = "psycopg", extras = ["c"] },
@ -1216,12 +1216,9 @@ wheels = [
[[package]]
name = "durationpy"
version = "0.9"
version = "0.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/31/e9/f49c4e7fccb77fa5c43c2480e09a857a78b41e7331a75e128ed5df45c56b/durationpy-0.9.tar.gz", hash = "sha256:fd3feb0a69a0057d582ef643c355c40d2fa1c942191f914d12203b1a01ac722a", size = 3186 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/a3/ac312faeceffd2d8f86bc6dcb5c401188ba5a01bc88e69bed97578a0dfcd/durationpy-0.9-py3-none-any.whl", hash = "sha256:e65359a7af5cedad07fb77a2dd3f390f8eb0b74cb845589fa6c057086834dd38", size = 3461 },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/b8/074abdcc251bec87da6c5b19b88d7898ec7996c6780d40c6ac5000d3dd47/durationpy-0.7.tar.gz", hash = "sha256:8447c43df4f1a0b434e70c15a38d77f5c9bd17284bfc1ff1d430f233d5083732", size = 3168 }
[[package]]
name = "email-validator"
@ -2078,7 +2075,7 @@ wheels = [
[[package]]
name = "opencontainers"
version = "0.0.14"
source = { git = "https://github.com/BeryJu/oci-python?rev=c791b19056769cd67957322806809ab70f5bead8#c791b19056769cd67957322806809ab70f5bead8" }
source = { git = "https://github.com/vsoch/oci-python?rev=20d69d9cc50a0fef31605b46f06da0c94f1ec3cf#20d69d9cc50a0fef31605b46f06da0c94f1ec3cf" }
[[package]]
name = "opentelemetry-api"

114
web/package-lock.json generated
View File

@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.3-1743464496",
"@goauthentik/api": "^2025.2.2-1742585853",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
@ -1835,9 +1835,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2025.2.3-1743464496",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.3-1743464496.tgz",
"integrity": "sha512-35+SqFNoBZ+WNpyG2Xv/VKYKIIxjwRmIbgX5WZSpc9IlJVv7yyckUYvLpU2F0hZVUMDnxAUE5bsiNn7K4EQslw=="
"version": "2025.2.2-1742585853",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.2-1742585853.tgz",
"integrity": "sha512-bg/816ljAuUixLxi8tZd3W7sEcHgG5aYl0IMkbTsFYOAuiOdl/5wqSWaVM8g8O9SQ9feP3v6xDLOGncMoJxh4g=="
},
"node_modules/@goauthentik/web": {
"resolved": "",
@ -8815,80 +8815,50 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/bare-events": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz",
"integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==",
"dev": true,
"license": "Apache-2.0",
"optional": true
},
"node_modules/bare-fs": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz",
"integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==",
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
"integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
"bare-events": "^2.0.0",
"bare-path": "^2.0.0",
"bare-stream": "^2.0.0"
}
},
"node_modules/bare-os": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
"integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"engines": {
"bare": ">=1.14.0"
}
"optional": true
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
"integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-os": "^3.0.1"
"bare-os": "^2.1.0"
}
},
"node_modules/bare-stream": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.0.tgz",
"integrity": "sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"streamx": "^2.21.0"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
"b4a": "^1.6.6",
"streamx": "^2.20.0"
}
},
"node_modules/base64-arraybuffer": {
@ -20200,10 +20170,9 @@
}
},
"node_modules/prebuild-install/node_modules/tar-fs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
"license": "MIT",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
"optional": true,
"dependencies": {
"chownr": "^1.1.1",
@ -22785,13 +22754,13 @@
}
},
"node_modules/streamx": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz",
"integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-fifo": "^1.3.2",
"queue-tick": "^1.0.1",
"text-decoder": "^1.1.0"
},
"optionalDependencies": {
@ -23246,18 +23215,17 @@
}
},
"node_modules/tar-fs": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
"integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
"bare-fs": "^2.1.1",
"bare-path": "^2.1.0"
}
},
"node_modules/tar-stream": {
@ -24792,9 +24760,9 @@
}
},
"node_modules/vite": {
"version": "5.4.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz",
"integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==",
"version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -12,7 +12,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.3-1743464496",
"@goauthentik/api": "^2025.2.2-1742585853",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",

View File

@ -13,7 +13,6 @@ const MAX_PARAMS = 5;
// const MAX_COGNITIVE_COMPLEXITY = 9;
const rules = {
"no-param-reassign": "error",
"accessor-pairs": "error",
"array-callback-return": "error",
"block-scoped-var": "error",
@ -85,6 +84,7 @@ const rules = {
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-param-reassign": "error",
"no-proto": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
@ -134,7 +134,6 @@ const rules = {
// "sonarjs/cognitive-complexity": ["off", MAX_COGNITIVE_COMPLEXITY],
// "sonarjs/no-duplicate-string": "off",
// "sonarjs/no-nested-template-literals": "off",
" @typescript-eslint/no-namespace": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",

View File

@ -48,7 +48,6 @@ export default [
// "sonarjs/no-duplicate-string": "off",
// "sonarjs/no-nested-template-literals": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{

View File

@ -19,7 +19,7 @@ import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthent
@customElement("ak-about-modal")
export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) {
static get styles() {
return ModalButton.styles.concat(
return super.styles.concat(
PFAbout,
css`
.pf-c-about-modal-box__hero {
@ -59,7 +59,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
renderModal() {
let product = globalAK().brand.brandingTitle || DefaultBrand.brandingTitle;
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`;
}
return html`<div

View File

@ -46,7 +46,7 @@ export class SystemStatusCard extends AdminStatusCard<SystemInfo> {
return;
}
const outpost = outposts.results[0];
outpost.config.authentik_host = window.location.origin;
outpost.config["authentik_host"] = window.location.origin;
await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesUpdate({
uuid: outpost.pk,
outpostRequest: outpost,

View File

@ -28,18 +28,16 @@ export class WorkersStatusCard extends AdminStatusCard<Worker[]> {
icon: "fa fa-times-circle pf-m-danger",
message: html`${msg("No workers connected. Background tasks will not run.")}`,
});
}
if (value.filter((w) => !w.versionMatching).length > 0) {
} else if (value.filter((w) => !w.versionMatching).length > 0) {
return Promise.resolve<AdminStatus>({
icon: "fa fa-times-circle pf-m-danger",
message: html`${msg("Worker with incorrect version connected.")}`,
});
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
});
}
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
});
}
renderValue() {

View File

@ -127,7 +127,7 @@ export class SyncStatusChart extends AKChart<SummarizedSyncStatus[]> {
msg("LDAP Source"),
),
];
this.centerText = statuses.reduce((total, el) => total + el.total, 0).toString();
this.centerText = statuses.reduce((total, el) => (total += el.total), 0).toString();
return statuses;
}

View File

@ -6,26 +6,26 @@ import { html } from "lit";
import "../AdminSettingsFooterLinks.js";
describe("ak-admin-settings-footer-link", () => {
afterEach(() => {
return browser.execute(() => {
document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if ("_$litPart$" in document.body) {
delete document.body._$litPart$;
afterEach(async () => {
await browser.execute(async () => {
await document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if (document.body["_$litPart$"]) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
await delete document.body["_$litPart$"];
}
});
});
it("should render an empty control", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = $("ak-admin-settings-footer-link");
const link = await $("ak-admin-settings-footer-link");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" });
});
it("should not be valid if just a name is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = $("ak-admin-settings-footer-link");
const link = await $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "foo", href: "" });
@ -33,7 +33,7 @@ describe("ak-admin-settings-footer-link", () => {
it("should be valid if just a URL is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = $("ak-admin-settings-footer-link");
const link = await $("ak-admin-settings-footer-link");
await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({
@ -44,7 +44,7 @@ describe("ak-admin-settings-footer-link", () => {
it("should be valid if both are filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = $("ak-admin-settings-footer-link");
const link = await $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
@ -56,7 +56,7 @@ describe("ak-admin-settings-footer-link", () => {
it("should not be valid if the URL is not valid", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = $("ak-admin-settings-footer-link");
const link = await $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("never://foo.com");
await expect(await link.getProperty("toJson")).toEqual({

View File

@ -79,7 +79,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
});
}
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles().metaIcon;
const icon = this.getFormFiles()["metaIcon"];
if (icon || this.clearIcon) {
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
slug: app.slug,
@ -117,7 +117,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
if (!(ev instanceof InputEvent) || !ev.target) {
return;
}
this.clearIcon = Boolean((ev.target as HTMLInputElement).checked);
this.clearIcon = !!(ev.target as HTMLInputElement).checked;
}
renderForm(): TemplateResult {

View File

@ -71,7 +71,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
}
static get styles(): CSSResult[] {
return TablePage.styles.concat(PFCard, applicationListStyle);
return super.styles.concat(PFCard, applicationListStyle);
}
columns(): TableColumn[] {

View File

@ -6,7 +6,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import * as YAML from "yaml";
import YAML from "yaml";
import { msg } from "@lit/localize";
import { CSSResult } from "lit";
@ -31,8 +31,9 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
getSuccessMessage(): string {
if (this.instance?.pbmUuid) {
return msg("Successfully updated entitlement.");
} else {
return msg("Successfully created entitlement.");
}
return msg("Successfully created entitlement.");
}
static get styles(): CSSResult[] {
@ -48,10 +49,11 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
pbmUuid: this.instance.pbmUuid || "",
applicationEntitlementRequest: data,
});
} else {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
}
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
}
renderForm(): TemplateResult {

View File

@ -15,6 +15,7 @@ import {
ProviderModelEnum,
ProxyMode,
ProxyProvider,
RACProvider,
RadiusProvider,
RedirectURI,
SAMLProvider,
@ -50,8 +51,9 @@ function renderRadiusOverview(rawProvider: OneOfProvider) {
]);
}
function renderRACOverview(_rawProvider: OneOfProvider) {
// const _provider = rawProvider as RACProvider;
function renderRACOverview(rawProvider: OneOfProvider) {
// @ts-expect-error TS6133
const _provider = rawProvider as RACProvider;
}
function formatRedirectUris(uris: RedirectURI[] = []) {

View File

@ -1,13 +1,14 @@
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { isSlug, isURLInput } from "@goauthentik/common/utils.js";
import { isSlug } from "@goauthentik/common/utils.js";
import { camelToSnake } from "@goauthentik/common/utils.js";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-slug-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -20,25 +21,13 @@ import { type ApplicationRequest } from "@goauthentik/api";
import { ApplicationWizardStateUpdate, ValidationRecord } from "../types";
/**
* Plucks the specified keys from an object, trimming their values if they are strings.
*
* @template T - The type of the input object.
* @template K - The keys to be plucked from the input object.
*
* @param {T} input - The input object.
* @param {Array<K>} keys - The keys to be plucked from the input object.
*/
function trimMany<T extends object, K extends keyof T>(input: T, keys: Array<K>): Pick<T, K> {
const result: Partial<T> = {};
const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
for (const key of keys) {
const value = input[key];
result[key] = (typeof value === "string" ? value.trim() : value) as T[K];
}
const trimMany = (o: KeyUnknown, vs: string[]) =>
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
return result as Pick<T, K>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isStr = (v: any): v is string => typeof v === "string";
@customElement("ak-application-wizard-application-step")
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
@ -48,7 +37,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
errors = new Map<string, string>();
@query("form#applicationform")
declare form: HTMLFormElement;
form!: HTMLFormElement;
constructor() {
super();
@ -65,34 +54,27 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
}
get buttons(): WizardButton[] {
return [
// ---
{ kind: "next", destination: "provider-choice" },
{ kind: "cancel" },
];
return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }];
}
get valid() {
this.errors = new Map();
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
const trimmed = trimMany((this.formValues || {}) as Partial<ApplicationRequest>, [
"name",
"slug",
"metaLaunchUrl",
]);
if (!trimmed.name) {
if (values["name"] === "") {
this.errors.set("name", msg("An application name is required"));
}
if (!isURLInput(trimmed.metaLaunchUrl)) {
if (
!(
isStr(values["metaLaunchUrl"]) &&
(values["metaLaunchUrl"] === "" || URL.canParse(values["metaLaunchUrl"]))
)
) {
this.errors.set("metaLaunchUrl", msg("Not a valid URL"));
}
if (!isSlug(trimmed.slug)) {
if (!(isStr(values["slug"]) && values["slug"] !== "" && isSlug(values["slug"]))) {
this.errors.set("slug", msg("Not a valid slug"));
}
return this.errors.size === 0;
}
@ -100,39 +82,27 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
if (button.kind === "next") {
if (!this.valid) {
this.handleEnabling({
disabled: [
// ---
"provider-choice",
"provider",
"bindings",
"submit",
],
disabled: ["provider-choice", "provider", "bindings", "submit"],
});
return;
}
const app: Partial<ApplicationRequest> = this.formValues as Partial<ApplicationRequest>;
let payload: ApplicationWizardStateUpdate = {
app: this.formValues,
errors: this.removeErrors("app"),
};
if (app.name && (this.wizard.provider?.name ?? "").trim() === "") {
payload = {
...payload,
provider: { name: `Provider for ${app.name}` },
};
}
this.handleUpdate(payload, button.destination, {
enable: "provider-choice",
});
return;
}
super.handleButton(button);
}
@ -211,7 +181,6 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
if (!(this.wizard.app && this.wizard.errors)) {
throw new Error("Application Step received uninitialized wizard context.");
}
return this.renderForm(
this.wizard.app as ApplicationRequest,
this.wizard.errors?.app ?? {},

View File

@ -45,7 +45,7 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
hide = true;
@query("form#bindingform")
declare form: HTMLFormElement;
form!: HTMLFormElement;
@query(".policy-search-select")
searchSelect!: SearchSelectBase<Policy> | SearchSelectBase<Group> | SearchSelectBase<User>;

View File

@ -22,7 +22,7 @@ export class ApplicationWizardProviderSamlForm extends ApplicationWizardProvider
const setHasSigningKp = (ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = Boolean(target.selectedKeypair);
this.hasSigningKp = !!target.selectedKeypair;
};
return html` <ak-wizard-title>${this.label}</ak-wizard-title>

View File

@ -8,7 +8,7 @@ import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
import * as YAML from "yaml";
import YAML from "yaml";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
@ -59,10 +59,11 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
instanceUuid: this.instance.pk,
blueprintInstanceRequest: data,
});
} else {
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsCreate({
blueprintInstanceRequest: data,
});
}
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsCreate({
blueprintInstanceRequest: data,
});
}
renderForm(): TemplateResult {

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