Compare commits

..

2 Commits

Author SHA1 Message Date
29a3117a94 web: Clean up Sentry usage. Reduce API calls.
web: Format.

web: Disable sentry via server context.
2025-05-03 02:26:43 +02:00
66e40720c5 web: Clean up usage of server injected values. 2025-05-03 02:25:55 +02:00
215 changed files with 4317 additions and 9948 deletions

View File

@ -200,7 +200,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: web/dist path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**') }}
- name: prepare web ui - name: prepare web ui
if: steps.cache-web.outputs.cache-hit != 'true' if: steps.cache-web.outputs.cache-hit != 'true'
working-directory: web working-directory: web
@ -208,7 +208,6 @@ jobs:
npm ci npm ci
make -C .. gen-client-ts make -C .. gen-client-ts
npm run build npm run build
npm run build:sfe
- name: run e2e - name: run e2e
run: | run: |
uv run coverage run manage.py test ${{ matrix.job.glob }} uv run coverage run manage.py test ${{ matrix.job.glob }}

View File

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

View File

@ -40,8 +40,7 @@ COPY ./web /work/web/
COPY ./website /work/website/ COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build && \ RUN npm run build
npm run build:sfe
# Stage 3: Build go proxy # Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
@ -86,17 +85,18 @@ FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN" ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
ENV GEOIPUPDATE_VERBOSE="1" ENV GEOIPUPDATE_VERBOSE="1"
ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID" ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID"
ENV GEOIPUPDATE_LICENSE_KEY_FILE="/run/secrets/GEOIPUPDATE_LICENSE_KEY"
USER root USER root
RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
--mount=type=secret,id=GEOIPUPDATE_LICENSE_KEY \ --mount=type=secret,id=GEOIPUPDATE_LICENSE_KEY \
mkdir -p /usr/share/GeoIP && \ mkdir -p /usr/share/GeoIP && \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv # Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.7.3 AS uv FROM ghcr.io/astral-sh/uv:0.7.2 AS uv
# Stage 6: Base python image # Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base FROM ghcr.io/goauthentik/fips-python:3.12.10-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \ ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \ PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \

View File

@ -42,4 +42,4 @@ See [SECURITY.md](SECURITY.md)
## Adoption and Contributions ## Adoption and Contributions
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [contribution guide](https://docs.goauthentik.io/docs/developer-docs?utm_source=github). Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [CONTRIBUTING.md file](./CONTRIBUTING.md).

View File

@ -54,7 +54,7 @@ def create_component(generator: SchemaGenerator, name, schema, type_=ResolvedCom
return component return component
def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs): def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs): # noqa: W0613
"""Workaround to set a default response for endpoints. """Workaround to set a default response for endpoints.
Workaround suggested at Workaround suggested at
<https://github.com/tfranzel/drf-spectacular/issues/119#issuecomment-656970357> <https://github.com/tfranzel/drf-spectacular/issues/119#issuecomment-656970357>

View File

@ -164,7 +164,9 @@ class BlueprintEntry:
"""Get the blueprint model, with yaml tags resolved if present""" """Get the blueprint model, with yaml tags resolved if present"""
return str(self.tag_resolver(self.model, blueprint)) return str(self.tag_resolver(self.model, blueprint))
def get_permissions(self, blueprint: "Blueprint") -> Generator[BlueprintEntryPermission]: def get_permissions(
self, blueprint: "Blueprint"
) -> Generator[BlueprintEntryPermission, None, None]:
"""Get permissions of this entry, with all yaml tags resolved""" """Get permissions of this entry, with all yaml tags resolved"""
for perm in self.permissions: for perm in self.permissions:
yield BlueprintEntryPermission( yield BlueprintEntryPermission(

View File

@ -5,10 +5,10 @@ from typing import Any
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models import Value as V from django.db.models import Value as V
from django.http.request import HttpRequest from django.http.request import HttpRequest
from sentry_sdk import get_current_span
from authentik import get_full_version from authentik import get_full_version
from authentik.brands.models import Brand from authentik.brands.models import Brand
from authentik.lib.sentry import get_http_meta
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
_q_default = Q(default=True) _q_default = Q(default=True)
@ -32,9 +32,13 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects brand object into every template""" """Context Processor that injects brand object into every template"""
brand = getattr(request, "brand", DEFAULT_BRAND) brand = getattr(request, "brand", DEFAULT_BRAND)
tenant = getattr(request, "tenant", Tenant()) tenant = getattr(request, "tenant", Tenant())
trace = ""
span = get_current_span()
if span:
trace = span.to_traceparent()
return { return {
"brand": brand, "brand": brand,
"footer_links": tenant.footer_links, "footer_links": tenant.footer_links,
"html_meta": {**get_http_meta()}, "sentry_trace": trace,
"version": get_full_version(), "version": get_full_version(),
} }

View File

@ -16,12 +16,10 @@ from drf_spectacular.utils import (
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField, SerializerMethodField from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.permissions import SAFE_METHODS, BasePermission
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ValidationError from rest_framework.serializers import ListSerializer, ValidationError
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
from rest_framework.views import View
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
@ -87,6 +85,34 @@ class GroupSerializer(ModelSerializer):
raise ValidationError(_("Cannot set group as parent of itself.")) raise ValidationError(_("Cannot set group as parent of itself."))
return parent return parent
def validate_is_superuser(self, superuser: bool):
"""Ensure that the user creating this group has permissions to set the superuser flag"""
request: Request = self.context.get("request", None)
if not request:
return superuser
# If we're updating an instance, and the state hasn't changed, we don't need to check perms
if self.instance and superuser == self.instance.is_superuser:
return superuser
user: User = request.user
perm = (
"authentik_core.enable_group_superuser"
if superuser
else "authentik_core.disable_group_superuser"
)
has_perm = user.has_perm(perm)
if self.instance and not has_perm:
has_perm = user.has_perm(perm, self.instance)
if not has_perm:
raise ValidationError(
_(
(
"User does not have permission to set "
"superuser status to {superuser_status}."
).format_map({"superuser_status": superuser})
)
)
return superuser
class Meta: class Meta:
model = Group model = Group
fields = [ fields = [
@ -154,36 +180,6 @@ class GroupFilter(FilterSet):
fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"] fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"]
class SuperuserSetter(BasePermission):
"""Check for enable_group_superuser or disable_group_superuser permissions"""
message = _("User does not have permission to set the given superuser status.")
enable_perm = "authentik_core.enable_group_superuser"
disable_perm = "authentik_core.disable_group_superuser"
def has_permission(self, request: Request, view: View):
if request.method != "POST":
return True
is_superuser = request.data.get("is_superuser", False)
if not is_superuser:
return True
return request.user.has_perm(self.enable_perm)
def has_object_permission(self, request: Request, view: View, object: Group):
if request.method in SAFE_METHODS:
return True
new_value = request.data.get("is_superuser")
old_value = object.is_superuser
if new_value is None or new_value == old_value:
return True
perm = self.enable_perm if new_value else self.disable_perm
return request.user.has_perm(perm) or request.user.has_perm(perm, object)
class GroupViewSet(UsedByMixin, ModelViewSet): class GroupViewSet(UsedByMixin, ModelViewSet):
"""Group Viewset""" """Group Viewset"""
@ -196,7 +192,6 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
serializer_class = GroupSerializer serializer_class = GroupSerializer
search_fields = ["name", "is_superuser"] search_fields = ["name", "is_superuser"]
filterset_class = GroupFilter filterset_class = GroupFilter
permission_classes = [SuperuserSetter]
ordering = ["name"] ordering = ["name"]
def get_queryset(self): def get_queryset(self):

View File

@ -2,7 +2,6 @@
from django.apps import apps from django.apps import apps
from django.contrib.auth.management import create_permissions from django.contrib.auth.management import create_permissions
from django.core.management import call_command
from django.core.management.base import BaseCommand, no_translations from django.core.management.base import BaseCommand, no_translations
from guardian.management import create_anonymous_user from guardian.management import create_anonymous_user
@ -17,10 +16,6 @@ class Command(BaseCommand):
"""Check permissions for all apps""" """Check permissions for all apps"""
for tenant in Tenant.objects.filter(ready=True): for tenant in Tenant.objects.filter(ready=True):
with tenant: with tenant:
# See https://code.djangoproject.com/ticket/28417
# Remove potential lingering old permissions
call_command("remove_stale_contenttypes", "--no-input")
for app in apps.get_app_configs(): for app in apps.get_app_configs():
self.stdout.write(f"Checking app {app.name} ({app.label})\n") self.stdout.write(f"Checking app {app.name} ({app.label})\n")
create_permissions(app, verbosity=0) create_permissions(app, verbosity=0)

View File

@ -31,10 +31,7 @@ class PickleSerializer:
def loads(self, data): def loads(self, data):
"""Unpickle data to be loaded from redis""" """Unpickle data to be loaded from redis"""
try: return pickle.loads(data) # nosec
return pickle.loads(data) # nosec
except Exception:
return {}
def _migrate_session( def _migrate_session(

View File

@ -1,27 +0,0 @@
# Generated by Django 5.1.9 on 2025-05-14 11:15
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def remove_old_authenticated_session_content_type(
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
):
db_alias = schema_editor.connection.alias
ContentType = apps.get_model("contenttypes", "ContentType")
ContentType.objects.using(db_alias).filter(model="oldauthenticatedsession").delete()
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0047_delete_oldauthenticatedsession"),
]
operations = [
migrations.RunPython(
code=remove_old_authenticated_session_content_type,
),
]

View File

@ -1,5 +1,14 @@
{% load i18n %} {% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{{ config_json|json_script:":ak-config:" }}
{{ brand_json|json_script:":ak-brand:" }}
<meta name="ak-version-family" content="{{ version_family }}">
<meta name="ak-version-subdomain" content="{{ version_subdomain }}">
<meta name="ak-build" content="{{ build }}">
<meta name="ak-base-url" content="{{ base_url }}">
<meta name="ak-base-url-rel" content="{{ base_url_rel }}">
<script> <script>
window.authentik = { window.authentik = {

View File

@ -21,9 +21,7 @@
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}
{% for key, value in html_meta.items %} <meta name="sentry-trace" content="{{ sentry_trace }}" />
<meta name="{{key}}" content="{{ value }}" />
{% endfor %}
</head> </head>
<body> <body>
{% block body %} {% block body %}

View File

@ -118,25 +118,12 @@ class TestGroupsAPI(APITestCase):
reverse("authentik_api:group-list"), reverse("authentik_api:group-list"),
data={"name": generate_id(), "is_superuser": True}, data={"name": generate_id(), "is_superuser": True},
) )
self.assertEqual(res.status_code, 403) self.assertEqual(res.status_code, 400)
self.assertJSONEqual( self.assertJSONEqual(
res.content, res.content,
{"detail": "User does not have permission to set the given superuser status."}, {"is_superuser": ["User does not have permission to set superuser status to True."]},
) )
def test_superuser_update_object_perm(self):
"""Test updating a superuser group with object permission"""
group = Group.objects.create(name=generate_id(), is_superuser=False)
assign_perm("view_group", self.login_user, group)
assign_perm("change_group", self.login_user, group)
assign_perm("enable_group_superuser", self.login_user, group)
self.client.force_login(self.login_user)
res = self.client.patch(
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
data={"is_superuser": True},
)
self.assertEqual(res.status_code, 200)
def test_superuser_update_no_perm(self): def test_superuser_update_no_perm(self):
"""Test updating a superuser group without permission""" """Test updating a superuser group without permission"""
group = Group.objects.create(name=generate_id(), is_superuser=True) group = Group.objects.create(name=generate_id(), is_superuser=True)
@ -147,10 +134,10 @@ class TestGroupsAPI(APITestCase):
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}), reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
data={"is_superuser": False}, data={"is_superuser": False},
) )
self.assertEqual(res.status_code, 403) self.assertEqual(res.status_code, 400)
self.assertJSONEqual( self.assertJSONEqual(
res.content, res.content,
{"detail": "User does not have permission to set the given superuser status."}, {"is_superuser": ["User does not have permission to set superuser status to False."]},
) )
def test_superuser_update_no_change(self): def test_superuser_update_no_change(self):
@ -176,27 +163,3 @@ class TestGroupsAPI(APITestCase):
data={"name": generate_id(), "is_superuser": True}, data={"name": generate_id(), "is_superuser": True},
) )
self.assertEqual(res.status_code, 201) self.assertEqual(res.status_code, 201)
def test_superuser_create_no_perm(self):
"""Test creating a superuser group with no permission"""
assign_perm("authentik_core.add_group", self.login_user)
self.client.force_login(self.login_user)
res = self.client.post(
reverse("authentik_api:group-list"),
data={"name": generate_id(), "is_superuser": True},
)
self.assertEqual(res.status_code, 403)
self.assertJSONEqual(
res.content,
{"detail": "User does not have permission to set the given superuser status."},
)
def test_no_superuser_create_no_perm(self):
"""Test creating a non-superuser group with no permission"""
assign_perm("authentik_core.add_group", self.login_user)
self.client.force_login(self.login_user)
res = self.client.post(
reverse("authentik_api:group-list"),
data={"name": generate_id()},
)
self.assertEqual(res.status_code, 201)

View File

@ -1,6 +1,5 @@
"""Interface views""" """Interface views"""
from json import dumps
from typing import Any from typing import Any
from django.http import HttpRequest from django.http import HttpRequest
@ -46,14 +45,19 @@ class InterfaceView(TemplateView):
"""Base interface view""" """Base interface view"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]: def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data) """Add common context data to all interface views"""
kwargs["brand_json"] = dumps(CurrentBrandSerializer(self.request.brand).data)
kwargs["config_json"] = ConfigView(request=Request(self.request)).get_config().data
kwargs["brand_json"] = CurrentBrandSerializer(self.request.brand).data
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}" kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}" kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
kwargs["build"] = get_build_hash() kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs kwargs["url_kwargs"] = self.kwargs
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/")) kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
kwargs["base_url_rel"] = CONFIG.get("web.path", "/") kwargs["base_url_rel"] = CONFIG.get("web.path", "/")
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -132,14 +132,13 @@ class LicenseKey:
"""Get a summarized version of all (not expired) licenses""" """Get a summarized version of all (not expired) licenses"""
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0) total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
for lic in License.objects.all(): for lic in License.objects.all():
if lic.is_valid: total.internal_users += lic.internal_users
total.internal_users += lic.internal_users total.external_users += lic.external_users
total.external_users += lic.external_users
total.license_flags.extend(lic.status.license_flags)
exp_ts = int(mktime(lic.expiry.timetuple())) exp_ts = int(mktime(lic.expiry.timetuple()))
if total.exp == 0: if total.exp == 0:
total.exp = exp_ts total.exp = exp_ts
total.exp = max(total.exp, exp_ts) total.exp = max(total.exp, exp_ts)
total.license_flags.extend(lic.status.license_flags)
return total return total
@staticmethod @staticmethod

View File

@ -39,10 +39,6 @@ class License(SerializerModel):
internal_users = models.BigIntegerField() internal_users = models.BigIntegerField()
external_users = models.BigIntegerField() external_users = models.BigIntegerField()
@property
def is_valid(self) -> bool:
return self.expiry >= now()
@property @property
def serializer(self) -> type[BaseSerializer]: def serializer(self) -> type[BaseSerializer]:
from authentik.enterprise.api import LicenseSerializer from authentik.enterprise.api import LicenseSerializer

View File

@ -8,7 +8,6 @@ from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from authentik.core.models import User
from authentik.enterprise.license import LicenseKey from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import ( from authentik.enterprise.models import (
THRESHOLD_READ_ONLY_WEEKS, THRESHOLD_READ_ONLY_WEEKS,
@ -72,9 +71,9 @@ class TestEnterpriseLicense(TestCase):
) )
def test_valid_multiple(self): def test_valid_multiple(self):
"""Check license verification""" """Check license verification"""
lic = License.objects.create(key=generate_id(), expiry=expiry_valid) lic = License.objects.create(key=generate_id())
self.assertTrue(lic.status.status().is_valid) self.assertTrue(lic.status.status().is_valid)
lic2 = License.objects.create(key=generate_id(), expiry=expiry_valid) lic2 = License.objects.create(key=generate_id())
self.assertTrue(lic2.status.status().is_valid) self.assertTrue(lic2.status.status().is_valid)
total = LicenseKey.get_total() total = LicenseKey.get_total()
self.assertEqual(total.internal_users, 200) self.assertEqual(total.internal_users, 200)
@ -233,9 +232,7 @@ class TestEnterpriseLicense(TestCase):
) )
def test_expiry_expired(self): def test_expiry_expired(self):
"""Check license verification""" """Check license verification"""
User.objects.all().delete() License.objects.create(key=generate_id())
License.objects.all().delete()
License.objects.create(key=generate_id(), expiry=expiry_expired)
self.assertEqual(LicenseKey.get_total().summary().status, LicenseUsageStatus.EXPIRED) self.assertEqual(LicenseKey.get_total().summary().status, LicenseUsageStatus.EXPIRED)
@patch( @patch(

View File

@ -57,7 +57,7 @@ class LogEventSerializer(PassiveSerializer):
@contextmanager @contextmanager
def capture_logs(log_default_output=True) -> Generator[list[LogEvent]]: def capture_logs(log_default_output=True) -> Generator[list[LogEvent], None, None]:
"""Capture log entries created""" """Capture log entries created"""
logs = [] logs = []
cap = LogCapture() cap = LogCapture()

View File

@ -15,7 +15,6 @@
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">
<meta name="sentry-trace" content="{{ sentry_trace }}" /> <meta name="sentry-trace" content="{{ sentry_trace }}" />
<link rel="prefetch" href="{{ flow_background_url }}" />
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
<style> <style>
html, html,
@ -23,7 +22,7 @@
height: 100%; height: 100%;
} }
body { body {
background-image: url("{{ flow_background_url }}"); background-image: url("{{ flow.background_url }}");
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
} }

View File

@ -5,7 +5,7 @@
{% block head_before %} {% block head_before %}
{{ block.super }} {{ block.super }}
<link rel="prefetch" href="{{ flow_background_url }}" /> <link rel="prefetch" href="{{ flow.background_url }}" />
{% if flow.compatibility_mode and not inspector %} {% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: !navigator.webdriver };</script> <script>ShadyDOM = { force: !navigator.webdriver };</script>
{% endif %} {% endif %}
@ -21,7 +21,7 @@ window.authentik.flow = {
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
<style> <style>
:root { :root {
--ak-flow-background: url("{{ flow_background_url }}"); --ak-flow-background: url("{{ flow.background_url }}");
} }
</style> </style>
{% endblock %} {% endblock %}

View File

@ -13,9 +13,7 @@ class FlowInterfaceView(InterfaceView):
"""Flow interface""" """Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]: def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["flow"] = flow
kwargs["flow_background_url"] = flow.background_url(self.request)
kwargs["inspector"] = "inspector" in self.request.GET kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -363,9 +363,6 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True) pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True)
if not pool_options: if not pool_options:
pool_options = True pool_options = True
# FIXME: Temporarily force pool to be deactivated.
# See https://github.com/goauthentik/authentik/issues/14320
pool_options = False
db = { db = {
"default": { "default": {

View File

@ -17,7 +17,7 @@ from ldap3.core.exceptions import LDAPException
from redis.exceptions import ConnectionError as RedisConnectionError from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import RedisError, ResponseError from redis.exceptions import RedisError, ResponseError
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from sentry_sdk import HttpTransport, get_current_scope from sentry_sdk import HttpTransport
from sentry_sdk import init as sentry_sdk_init from sentry_sdk import init as sentry_sdk_init
from sentry_sdk.api import set_tag from sentry_sdk.api import set_tag
from sentry_sdk.integrations.argv import ArgvIntegration from sentry_sdk.integrations.argv import ArgvIntegration
@ -27,7 +27,6 @@ from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.socket import SocketIntegration from sentry_sdk.integrations.socket import SocketIntegration
from sentry_sdk.integrations.stdlib import StdlibIntegration from sentry_sdk.integrations.stdlib import StdlibIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration from sentry_sdk.integrations.threading import ThreadingIntegration
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from websockets.exceptions import WebSocketException from websockets.exceptions import WebSocketException
@ -96,8 +95,6 @@ def traces_sampler(sampling_context: dict) -> float:
return 0 return 0
if _type == "websocket": if _type == "websocket":
return 0 return 0
if CONFIG.get_bool("debug"):
return 1
return float(CONFIG.get("error_reporting.sample_rate", 0.1)) return float(CONFIG.get("error_reporting.sample_rate", 0.1))
@ -170,14 +167,3 @@ def before_send(event: dict, hint: dict) -> dict | None:
if settings.DEBUG: if settings.DEBUG:
return None return None
return event return event
def get_http_meta():
"""Get sentry-related meta key-values"""
scope = get_current_scope()
meta = {
SENTRY_TRACE_HEADER_NAME: scope.get_traceparent() or "",
}
if bag := scope.get_baggage():
meta[BAGGAGE_HEADER_NAME] = bag.serialize()
return meta

View File

@ -59,7 +59,7 @@ class PropertyMappingManager:
request: HttpRequest | None, request: HttpRequest | None,
return_mapping: bool = False, return_mapping: bool = False,
**kwargs, **kwargs,
) -> Generator[tuple[dict, PropertyMapping]]: ) -> Generator[tuple[dict, PropertyMapping], None]:
"""Iterate over all mappings that were pre-compiled and """Iterate over all mappings that were pre-compiled and
execute all of them with the given context""" execute all of them with the given context"""
if not self.__has_compiled: if not self.__has_compiled:

View File

@ -494,88 +494,86 @@ class TestConfig(TestCase):
}, },
) )
# FIXME: Temporarily force pool to be deactivated. def test_db_pool(self):
# See https://github.com/goauthentik/authentik/issues/14320 """Test DB Config with pool"""
# def test_db_pool(self): config = ConfigLoader()
# """Test DB Config with pool""" config.set("postgresql.host", "foo")
# config = ConfigLoader() config.set("postgresql.name", "foo")
# config.set("postgresql.host", "foo") config.set("postgresql.user", "foo")
# config.set("postgresql.name", "foo") config.set("postgresql.password", "foo")
# config.set("postgresql.user", "foo") config.set("postgresql.port", "foo")
# config.set("postgresql.password", "foo") config.set("postgresql.test.name", "foo")
# config.set("postgresql.port", "foo") config.set("postgresql.use_pool", True)
# config.set("postgresql.test.name", "foo") conf = django_db_config(config)
# config.set("postgresql.use_pool", True) self.assertEqual(
# conf = django_db_config(config) conf,
# self.assertEqual( {
# conf, "default": {
# { "ENGINE": "authentik.root.db",
# "default": { "HOST": "foo",
# "ENGINE": "authentik.root.db", "NAME": "foo",
# "HOST": "foo", "OPTIONS": {
# "NAME": "foo", "pool": True,
# "OPTIONS": { "sslcert": None,
# "pool": True, "sslkey": None,
# "sslcert": None, "sslmode": None,
# "sslkey": None, "sslrootcert": None,
# "sslmode": None, },
# "sslrootcert": None, "PASSWORD": "foo",
# }, "PORT": "foo",
# "PASSWORD": "foo", "TEST": {"NAME": "foo"},
# "PORT": "foo", "USER": "foo",
# "TEST": {"NAME": "foo"}, "CONN_MAX_AGE": 0,
# "USER": "foo", "CONN_HEALTH_CHECKS": False,
# "CONN_MAX_AGE": 0, "DISABLE_SERVER_SIDE_CURSORS": False,
# "CONN_HEALTH_CHECKS": False, }
# "DISABLE_SERVER_SIDE_CURSORS": False, },
# } )
# },
# )
# def test_db_pool_options(self): def test_db_pool_options(self):
# """Test DB Config with pool""" """Test DB Config with pool"""
# config = ConfigLoader() config = ConfigLoader()
# config.set("postgresql.host", "foo") config.set("postgresql.host", "foo")
# config.set("postgresql.name", "foo") config.set("postgresql.name", "foo")
# config.set("postgresql.user", "foo") config.set("postgresql.user", "foo")
# config.set("postgresql.password", "foo") config.set("postgresql.password", "foo")
# config.set("postgresql.port", "foo") config.set("postgresql.port", "foo")
# config.set("postgresql.test.name", "foo") config.set("postgresql.test.name", "foo")
# config.set("postgresql.use_pool", True) config.set("postgresql.use_pool", True)
# config.set( config.set(
# "postgresql.pool_options", "postgresql.pool_options",
# base64.b64encode( base64.b64encode(
# dumps( dumps(
# { {
# "max_size": 15, "max_size": 15,
# } }
# ).encode() ).encode()
# ).decode(), ).decode(),
# ) )
# conf = django_db_config(config) conf = django_db_config(config)
# self.assertEqual( self.assertEqual(
# conf, conf,
# { {
# "default": { "default": {
# "ENGINE": "authentik.root.db", "ENGINE": "authentik.root.db",
# "HOST": "foo", "HOST": "foo",
# "NAME": "foo", "NAME": "foo",
# "OPTIONS": { "OPTIONS": {
# "pool": { "pool": {
# "max_size": 15, "max_size": 15,
# }, },
# "sslcert": None, "sslcert": None,
# "sslkey": None, "sslkey": None,
# "sslmode": None, "sslmode": None,
# "sslrootcert": None, "sslrootcert": None,
# }, },
# "PASSWORD": "foo", "PASSWORD": "foo",
# "PORT": "foo", "PORT": "foo",
# "TEST": {"NAME": "foo"}, "TEST": {"NAME": "foo"},
# "USER": "foo", "USER": "foo",
# "CONN_MAX_AGE": 0, "CONN_MAX_AGE": 0,
# "CONN_HEALTH_CHECKS": False, "CONN_HEALTH_CHECKS": False,
# "DISABLE_SERVER_SIDE_CURSORS": False, "DISABLE_SERVER_SIDE_CURSORS": False,
# } }
# }, },
# ) )

View File

@ -199,7 +199,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
chunk_size = len(ops) chunk_size = len(ops)
if len(ops) < 1: if len(ops) < 1:
return return
for chunk in batched(ops, chunk_size, strict=False): for chunk in batched(ops, chunk_size):
req = PatchRequest(Operations=list(chunk)) req = PatchRequest(Operations=list(chunk))
self._request( self._request(
"PATCH", "PATCH",

8
go.mod
View File

@ -19,7 +19,7 @@ require (
github.com/jellydator/ttlcache/v3 v3.3.0 github.com/jellydator/ttlcache/v3 v3.3.0
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.8.1 github.com/pires/go-proxyproto v0.8.0
github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_golang v1.22.0
github.com/redis/go-redis/v9 v9.8.0 github.com/redis/go-redis/v9 v9.8.0
github.com/sethvargo/go-envconfig v1.3.0 github.com/sethvargo/go-envconfig v1.3.0
@ -29,8 +29,8 @@ require (
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025040.1 goauthentik.io/api/v3 v3.2025040.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.29.0
golang.org/x/sync v0.14.0 golang.org/x/sync v0.13.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
) )
@ -75,7 +75,7 @@ require (
go.opentelemetry.io/otel/trace v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/crypto v0.36.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

20
go.sum
View File

@ -230,8 +230,8 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -358,16 +358,16 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -376,8 +376,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -412,8 +412,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -56,7 +56,6 @@ EXPOSE 3389 6636 9300
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/ldap"] ENTRYPOINT ["/ldap"]

View File

@ -97,7 +97,6 @@ elif [[ "$1" == "test-all" ]]; then
elif [[ "$1" == "healthcheck" ]]; then elif [[ "$1" == "healthcheck" ]]; then
run_authentik healthcheck $(cat $MODE_FILE) run_authentik healthcheck $(cat $MODE_FILE)
elif [[ "$1" == "dump_config" ]]; then elif [[ "$1" == "dump_config" ]]; then
shift
exec python -m authentik.lib.config $@ exec python -m authentik.lib.config $@
elif [[ "$1" == "debug" ]]; then elif [[ "$1" == "debug" ]]; then
exec sleep infinity exec sleep infinity

View File

@ -9,7 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1014.0", "aws-cdk": "^2.1013.0",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
}, },
"engines": { "engines": {
@ -17,9 +17,9 @@
} }
}, },
"node_modules/aws-cdk": { "node_modules/aws-cdk": {
"version": "2.1014.0", "version": "2.1013.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1014.0.tgz", "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1013.0.tgz",
"integrity": "sha512-es101rtRAClix9BncNL54iW90MiOyRv4iCC5tv/firGDnidS6pPinuK0IIFt0RO6w0+3heRxWBXg8HY+f9877w==", "integrity": "sha512-cbq4cOoEIZueMWenGgfI4RujS+AQ9GaMCTlW/3CnvEIhMD8j/tgZx7PTtgMuvwYrRoEeb/wTxgLPgUd5FhsoHA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View File

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

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
{ {
"name": "@goauthentik/docusaurus-config", "name": "@goauthentik/docusaurus-config",
"version": "1.0.6", "version": "1.0.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@goauthentik/docusaurus-config", "name": "@goauthentik/docusaurus-config",
"version": "1.0.6", "version": "1.0.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"deepmerge-ts": "^7.1.5", "deepmerge-ts": "^7.1.5",

View File

@ -1,6 +1,6 @@
{ {
"name": "@goauthentik/docusaurus-config", "name": "@goauthentik/docusaurus-config",
"version": "1.0.6", "version": "1.0.5",
"description": "authentik's Docusaurus config", "description": "authentik's Docusaurus config",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View File

@ -76,7 +76,6 @@ EXPOSE 9000 9300 9443
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/proxy"] ENTRYPOINT ["/proxy"]

View File

@ -3,114 +3,102 @@ name = "authentik"
version = "2025.4.0" version = "2025.4.0"
description = "" description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }] authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.13.*" requires-python = "==3.12.*"
dependencies = [ dependencies = [
"argon2-cffi==23.1.0", "argon2-cffi",
"celery==5.5.2", "celery",
"channels==4.2.2", "channels",
"channels-redis==4.2.1", "channels-redis",
"cryptography==44.0.3", "cryptography",
"dacite==1.9.2", "dacite",
"deepmerge==2.0", "deepmerge",
"defusedxml==0.7.1", "defusedxml",
"django==5.1.9", "django",
"django-countries==7.6.1", "django-countries",
"django-cte==1.3.3", "django-cte",
"django-filter==25.1", "django-filter",
"django-guardian<3.0.0", "django-guardian",
"django-model-utils==5.0.0", "django-model-utils",
"django-pglock==1.7.1", "django-pglock",
"django-prometheus==2.3.1", "django-prometheus",
"django-redis==5.4.0", "django-redis",
"django-storages[s3]==1.14.6", "django-storages[s3]",
"django-tenants==3.7.0", "django-tenants",
"djangorestframework==3.16.0", "djangorestframework",
"djangorestframework-guardian==0.3.0", "djangorestframework-guardian",
"docker==7.1.0", "docker",
"drf-orjson-renderer==1.7.3", "drf-orjson-renderer",
"drf-spectacular==0.28.0", "drf-spectacular",
"dumb-init==1.2.5.post1", "dumb-init",
"duo-client==5.5.0", "duo-client",
"fido2==1.2.0", "fido2",
"flower==2.0.1", "flower",
"geoip2==5.1.0", "geoip2",
"geopy==2.4.1", "geopy",
"google-api-python-client==2.169.0", "google-api-python-client",
"gssapi==1.9.0", "gssapi",
"gunicorn==23.0.0", "gunicorn",
"jsonpatch==1.33", "jsonpatch",
"jwcrypto==1.5.6", "jwcrypto",
"kubernetes==32.0.1", "kubernetes",
"ldap3==2.9.1", "ldap3",
"lxml==5.4.0", "lxml",
"msgraph-sdk==1.30.0", "msgraph-sdk",
"opencontainers==0.0.14", "opencontainers",
"packaging==25.0", "packaging",
"paramiko==3.5.1", "paramiko",
"psycopg[c,pool]==3.2.9", "psycopg[c, pool]",
"pydantic==2.11.4", "pydantic",
"pydantic-scim==0.0.8", "pydantic-scim",
"pyjwt==2.10.1", "pyjwt",
"pyrad==2.4", "pyrad",
"python-kadmin-rs==0.6.0", "python-kadmin-rs ==0.6.0",
"pyyaml==6.0.2", "pyyaml",
"requests-oauthlib==2.0.0", "requests-oauthlib",
"scim2-filter-parser==0.7.0", "scim2-filter-parser",
"sentry-sdk==2.28.0", "sentry-sdk",
"service-identity==24.2.0", "service_identity",
"setproctitle==1.3.6", "setproctitle",
"structlog==25.3.0", "structlog",
"swagger-spec-validator==3.0.4", "swagger-spec-validator",
"tenant-schemas-celery==4.0.1", "tenant-schemas-celery",
"twilio==9.6.1", "twilio",
"ua-parser==1.0.1", "ua-parser",
"unidecode==1.4.0", "unidecode",
"urllib3<3", "urllib3 <3",
"uvicorn[standard]==0.34.2", "uvicorn[standard]",
"watchdog==6.0.0", "watchdog",
"webauthn==2.5.2", "webauthn",
"wsproto==1.2.0", "wsproto",
"xmlsec==1.3.15", "xmlsec <= 1.3.14",
"zxcvbn==4.5.0", "zxcvbn",
] ]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"aws-cdk-lib==2.188.0", "aws-cdk-lib",
"bandit==1.8.3", "bandit",
"black==25.1.0", "black",
"bump2version==1.0.1", "bump2version",
"channels[daphne]==4.2.2", "channels[daphne]",
"codespell==2.4.1", "codespell",
"colorama==0.4.6", "colorama",
"constructs==10.4.2", "constructs",
"coverage[toml]==7.8.0", "coverage[toml]",
"debugpy==1.8.14", "debugpy",
"drf-jsonschema-serializer==3.0.0", "drf-jsonschema-serializer",
"freezegun==1.5.1", "freezegun",
"importlib-metadata==8.6.1", "importlib-metadata",
"k5test==0.10.4", "k5test",
"pdoc==15.0.3", "pdoc",
"pytest==8.3.5", "pytest",
"pytest-django==4.11.1", "pytest-django",
"pytest-github-actions-annotate-failures==0.3.0", "pytest-github-actions-annotate-failures",
"pytest-randomly==3.16.0", "pytest-randomly",
"pytest-timeout==2.4.0", "pytest-timeout",
"requests-mock==1.12.1", "requests-mock",
"ruff==0.11.9", "ruff",
"selenium==4.32.0", "selenium",
]
[tool.uv]
no-binary-package = [
# This differs from the no-binary packages in the Dockerfile. This is due to the fact
# that these packages are built from source for different reasons than cryptography and kadmin.
# These packages are built from source to link against the libxml2 on the system which is
# required for functionality and to stay up-to-date on both libraries.
# The other packages specified in the dockerfile are compiled from source to link against the
# correct FIPS OpenSSL libraries
"lxml",
"xmlsec",
] ]
[tool.uv.sources] [tool.uv.sources]
@ -155,12 +143,12 @@ ignore-words = ".github/codespell-words.txt"
[tool.black] [tool.black]
line-length = 100 line-length = 100
target-version = ['py313'] target-version = ['py312']
exclude = 'node_modules' exclude = 'node_modules'
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 100
target-version = "py313" target-version = "py312"
exclude = ["**/migrations/**", "**/node_modules/**"] exclude = ["**/migrations/**", "**/node_modules/**"]
[tool.ruff.lint] [tool.ruff.lint]

View File

@ -56,7 +56,6 @@ HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/rac", "healthch
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/rac"] ENTRYPOINT ["/rac"]

View File

@ -56,7 +56,6 @@ EXPOSE 1812/udp 9300
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/radius"] ENTRYPOINT ["/radius"]

View File

@ -1,12 +1,12 @@
services: services:
chrome: chrome:
image: docker.io/selenium/standalone-chrome:136.0 image: docker.io/selenium/standalone-chrome:122.0
volumes: volumes:
- /dev/shm:/dev/shm - /dev/shm:/dev/shm
network_mode: host network_mode: host
restart: always restart: always
mailpit: mailpit:
image: docker.io/axllent/mailpit:v1.24.2 image: docker.io/axllent/mailpit:v1.6.5
ports: ports:
- 1025:1025 - 1025:1025
- 8025:8025 - 8025:8025

View File

@ -1,51 +0,0 @@
"""test default login (using SFE interface) flow"""
from time import sleep
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from authentik.blueprints.tests import apply_blueprint
from tests.e2e.utils import SeleniumTestCase, retry
class TestFlowsLoginSFE(SeleniumTestCase):
"""test default login flow"""
def login(self):
"""Do entire login flow adjusted for SFE"""
flow_executor = self.driver.find_element(By.ID, "flow-sfe-container")
identification_stage = flow_executor.find_element(By.ID, "ident-form")
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").click()
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
self.user.username
)
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
Keys.ENTER
)
password_stage = flow_executor.find_element(By.ID, "password-form")
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
self.user.username
)
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
sleep(1)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
def test_login(self):
"""test default login flow"""
self.driver.get(
self.url(
"authentik_core:if-flow",
flow_slug="default-authentication-flow",
query={"sfe": True},
)
)
self.login()
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)

View File

@ -26,7 +26,6 @@ from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.command import Command
from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.wait import WebDriverWait
@ -198,12 +197,7 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
super().tearDown() super().tearDown()
if IS_CI: if IS_CI:
print("::group::Browser logs") print("::group::Browser logs")
# Very verbose way to get browser logs for line in self.driver.get_log("browser"):
# https://github.com/SeleniumHQ/selenium/pull/15641
# for some reason this removes the `get_log` API from Remote Webdriver
# and only keeps it on the local Chrome web driver, even when using
# a remote chrome driver...? (nvm the fact this was released as a minor version)
for line in self.driver.execute(Command.GET_LOG, {"type": "browser"})["value"]:
print(line["message"]) print(line["message"])
if IS_CI: if IS_CI:
print("::endgroup::") print("::endgroup::")
@ -241,7 +235,7 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
return element return element
def login(self): def login(self):
"""Do entire login flow""" """Do entire login flow and check user afterwards"""
flow_executor = self.get_shadow_root("ak-flow-executor") flow_executor = self.get_shadow_root("ak-flow-executor")
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor) identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)

2180
uv.lock generated

File diff suppressed because it is too large Load Diff

10
web/package-lock.json generated
View File

@ -7,6 +7,7 @@
"": { "": {
"name": "@goauthentik/web", "name": "@goauthentik/web",
"version": "0.0.0", "version": "0.0.0",
"hasInstallScript": true,
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
".", ".",
@ -9471,9 +9472,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001716", "version": "1.0.30001667",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
"integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==", "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -9488,8 +9489,7 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ]
"license": "CC-BY-4.0"
}, },
"node_modules/ccount": { "node_modules/ccount": {
"version": "2.0.1", "version": "2.0.1",

View File

@ -19,6 +19,7 @@
"lint:precommit": "wireit", "lint:precommit": "wireit",
"lint:types": "wireit", "lint:types": "wireit",
"lit-analyse": "wireit", "lit-analyse": "wireit",
"postinstall": "bash scripts/patch-spotlight.sh",
"precommit": "wireit", "precommit": "wireit",
"prettier": "wireit", "prettier": "wireit",
"prettier-check": "wireit", "prettier-check": "wireit",

View File

@ -6,7 +6,7 @@
* @import { Message as ESBuildMessage } from "esbuild"; * @import { Message as ESBuildMessage } from "esbuild";
*/ */
const logPrefix = "authentik/dev/web: "; const logPrefix = "👷 [ESBuild]";
const log = console.debug.bind(console, logPrefix); const log = console.debug.bind(console, logPrefix);
/** /**
@ -76,7 +76,7 @@ export class ESBuildObserver extends EventSource {
*/ */
#startListener = () => { #startListener = () => {
this.#trackActivity(); this.#trackActivity();
log("⏰ Build started..."); log("⏰ Build started...");
}; };
#internalErrorListener = () => { #internalErrorListener = () => {
@ -86,7 +86,7 @@ export class ESBuildObserver extends EventSource {
clearTimeout(this.#keepAliveInterval); clearTimeout(this.#keepAliveInterval);
this.close(); this.close();
log("⛔️ Closing connection"); log("⛔️ Closing connection");
} }
}; };
@ -126,13 +126,13 @@ export class ESBuildObserver extends EventSource {
this.#trackActivity(); this.#trackActivity();
if (!this.online) { if (!this.online) {
log("🚫 Build finished while offline."); log("🚫 Build finished while offline.");
this.deferredReload = true; this.deferredReload = true;
return; return;
} }
log("🛎️ Build completed! Reloading..."); log("🛎️ Build completed! Reloading...");
// We use an animation frame to keep the reload from happening before the // We use an animation frame to keep the reload from happening before the
// event loop has a chance to process the message. // event loop has a chance to process the message.
@ -189,13 +189,13 @@ export class ESBuildObserver extends EventSource {
if (!this.deferredReload) return; if (!this.deferredReload) return;
log("🛎️ Reloading after offline build..."); log("🛎️ Reloading after offline build...");
this.deferredReload = false; this.deferredReload = false;
window.location.reload(); window.location.reload();
}); });
log("🛎️ Listening for build changes..."); log("🛎️ Listening for build changes...");
this.#keepAliveInterval = setInterval(() => { this.#keepAliveInterval = setInterval(() => {
const now = Date.now(); const now = Date.now();
@ -203,7 +203,7 @@ export class ESBuildObserver extends EventSource {
if (now - this.lastUpdatedAt < 10_000) return; if (now - this.lastUpdatedAt < 10_000) return;
this.alive = false; this.alive = false;
log("👋 Waiting for build to start..."); log("👋 Waiting for build to start...");
}, 15_000); }, 15_000);
} }

View File

@ -47,16 +47,7 @@ class SimpleFlowExecutor {
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`; return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
} }
loading() {
this.container.innerHTML = `<div class="d-flex justify-content-center">
<div class="spinner-border spinner-border-md" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>`;
}
start() { start() {
this.loading();
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: this.apiURL, url: this.apiURL,
@ -210,9 +201,6 @@ class PasswordStage extends Stage<PasswordChallenge> {
<form id="password-form"> <form id="password-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt=""> <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1> <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="form-label-group my-3">
<input type="text" readonly class="form-control-plaintext" value="Welcome, ${this.challenge?.pendingUser}.">
</div>
<div class="form-label-group my-3 has-validation"> <div class="form-label-group my-3 has-validation">
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password"> <input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
${this.renderInputError("password")} ${this.renderInputError("password")}

View File

@ -0,0 +1,33 @@
#!/usr/bin/env bash
TARGET="./node_modules/@spotlightjs/overlay/dist/index-"[0-9a-f]*.js
if [[ $(grep -L "QX2" "$TARGET" > /dev/null 2> /dev/null) ]]; then
patch --forward -V none --no-backup-if-mismatch -p0 $TARGET <<EOF
TARGET=$(find "./node_modules/@spotlightjs/overlay/dist/" -name "index-[0-9a-f]*.js");
if ! grep -GL 'QX2 = ' "$TARGET" > /dev/null ; then
patch --forward --no-backup-if-mismatch -p0 "$TARGET" <<EOF
>>>>>>> main
--- a/index-5682ce90.js 2024-06-13 16:19:28
+++ b/index-5682ce90.js 2024-06-13 16:20:23
@@ -4958,11 +4958,10 @@
}
);
}
-const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m));
+const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m)), QX2 = () => {};
function Gp({
data: n,
- onUpdateData: a = () => {
- },
+ onUpdateData: a = QX2,
editingEnabled: s = !1,
clipboardEnabled: o = !1,
displayDataTypes: c = !1,
EOF
else
echo "spotlight overlay.js patch already applied"
fi

View File

@ -1,6 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants"; import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { DefaultBrand } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
@ -33,7 +33,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve(); const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
const version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve(); const version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
let build: string | TemplateResult = msg("Release"); let build: string | TemplateResult = msg("Release");
if (globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug)) { if (ServerContext.config.capabilities.includes(CapabilitiesEnum.CanDebug)) {
build = msg("Development"); build = msg("Development");
} else if (version.buildHash !== "") { } else if (version.buildHash !== "") {
build = html`<a build = html`<a
@ -58,10 +58,12 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
} }
renderModal() { renderModal() {
let product = globalAK().brand.brandingTitle || DefaultBrand.brandingTitle; let product = ServerContext.brand.brandingTitle || DefaultBrand.brandingTitle;
if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) { if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`; product += ` ${msg("Enterprise")}`;
} }
return html`<div return html`<div
class="pf-c-backdrop" class="pf-c-backdrop"
@click=${(e: PointerEvent) => { @click=${(e: PointerEvent) => {

View File

@ -6,7 +6,8 @@ import {
EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE,
EVENT_SIDEBAR_TOGGLE, EVENT_SIDEBAR_TOGGLE,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry"; import { setSentryPII, tryInitializeSentry } from "@goauthentik/common/sentry";
import { ServerContext } from "@goauthentik/common/server-context";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; import { AuthenticatedInterface } from "@goauthentik/elements/Interface";
@ -131,9 +132,9 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
//#region Lifecycle //#region Lifecycle
constructor() { constructor() {
configureSentry(true);
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)"); this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)");
this.sidebarOpen = this.#sidebarMatcher.matches; this.sidebarOpen = this.#sidebarMatcher.matches;
} }
@ -167,14 +168,21 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
} }
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
tryInitializeSentry(ServerContext.config);
this.user = await me(); this.user = await me();
setSentryPII(this.user.user);
const canAccessAdmin = const canAccessAdmin =
this.user.user.isSuperuser || this.user.user.isSuperuser ||
// TODO: somehow add `access_admin_interface` to the API schema // TODO: somehow add `access_admin_interface` to the API schema
this.user.user.systemPermissions.includes("access_admin_interface"); this.user.user.systemPermissions.includes("access_admin_interface");
if (!canAccessAdmin && this.user.user.pk > 0) { if (!canAccessAdmin && this.user.user.pk > 0) {
console.debug(
"authentik/admin: User does not have access to admin interface. Redirecting...",
);
window.location.assign("/if/user/"); window.location.assign("/if/user/");
} }
} }

View File

@ -113,7 +113,8 @@ export class ApplicationViewPage extends AKElement {
renderApp(): TemplateResult { renderApp(): TemplateResult {
if (!this.application) { if (!this.application) {
return html`<ak-empty-state loading header=${msg("Loading")}> </ak-empty-state>`; return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
</ak-empty-state>`;
} }
return html`<ak-tabs> return html`<ak-tabs>
${this.missingOutpost ${this.missingOutpost

View File

@ -1,5 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global"; import { docLink } from "@goauthentik/common/server-context";
import "@goauthentik/components/ak-toggle-group"; import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";

View File

@ -1,4 +1,4 @@
import { docLink } from "@goauthentik/common/global"; import { docLink } from "@goauthentik/common/server-context";
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton"; import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/TokenCopyButton"; import "@goauthentik/elements/buttons/TokenCopyButton";

View File

@ -1,5 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global"; import { docLink } from "@goauthentik/common/server-context";
import { groupBy } from "@goauthentik/common/utils"; import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";

View File

@ -1,6 +1,6 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm"; import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global"; import { docLink } from "@goauthentik/common/server-context";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";

View File

@ -1,4 +1,4 @@
import { docLink } from "@goauthentik/common/global"; import { docLink } from "@goauthentik/common/server-context";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";

View File

@ -1,6 +1,6 @@
import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm"; import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global"; import { docLink } from "@goauthentik/common/server-context";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";

View File

@ -42,7 +42,7 @@ export class ProviderViewPage extends AKElement {
renderProvider(): TemplateResult { renderProvider(): TemplateResult {
if (!this.provider) { if (!this.provider) {
return html`<ak-empty-state loading ?fullHeight=${true}></ak-empty-state>`; return html`<ak-empty-state ?loading=${true} ?fullHeight=${true}></ak-empty-state>`;
} }
switch (this.provider?.component) { switch (this.provider?.component) {
case "ak-provider-saml-form": case "ak-provider-saml-form":

View File

@ -432,7 +432,7 @@ export class OAuth2ProviderViewPage extends AKElement {
<div class="pf-c-card__body"> <div class="pf-c-card__body">
${this.preview ${this.preview
? html`<pre>${JSON.stringify(this.preview?.preview, null, 4)}</pre>` ? html`<pre>${JSON.stringify(this.preview?.preview, null, 4)}</pre>`
: html` <ak-empty-state loading></ak-empty-state> `} : html` <ak-empty-state ?loading=${true}></ak-empty-state> `}
</div> </div>
</div> </div>
</div>`; </div>`;

View File

@ -502,7 +502,7 @@ export class SAMLProviderViewPage extends AKElement {
renderTabPreview(): TemplateResult { renderTabPreview(): TemplateResult {
if (!this.preview) { if (!this.preview) {
return html`<ak-empty-state loading></ak-empty-state>`; return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
} }
return html` <div return html` <div
class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter" class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"

View File

@ -34,7 +34,7 @@ export class SourceViewPage extends AKElement {
renderSource(): TemplateResult { renderSource(): TemplateResult {
if (!this.source) { if (!this.source) {
return html`<ak-empty-state loading ?fullHeight=${true}></ak-empty-state>`; return html`<ak-empty-state ?loading=${true} ?fullHeight=${true}></ak-empty-state>`;
} }
switch (this.source?.component) { switch (this.source?.component) {
case "ak-source-kerberos-form": case "ak-source-kerberos-form":

View File

@ -5,7 +5,8 @@ import {
GroupMatchingModeToLabel, GroupMatchingModeToLabel,
UserMatchingModeToLabel, UserMatchingModeToLabel,
} from "@goauthentik/admin/sources/oauth/utils"; } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ServerContext } from "@goauthentik/common/server-context.js";
import "@goauthentik/components/ak-switch-input"; import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input"; import "@goauthentik/components/ak-textarea-input";
@ -60,8 +61,9 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
kerberosSourceRequest: data as unknown as KerberosSourceRequest, kerberosSourceRequest: data as unknown as KerberosSourceRequest,
}); });
} }
const c = await config(); const { capabilities } = ServerContext.config;
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
if (capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["icon"]; const icon = this.getFormFiles()["icon"];
if (icon || this.clearIcon) { if (icon || this.clearIcon) {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({ await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({

View File

@ -5,7 +5,8 @@ import {
GroupMatchingModeToLabel, GroupMatchingModeToLabel,
UserMatchingModeToLabel, UserMatchingModeToLabel,
} from "@goauthentik/admin/sources/oauth/utils"; } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ServerContext } from "@goauthentik/common/server-context.js";
import "@goauthentik/components/ak-radio-input"; import "@goauthentik/components/ak-radio-input";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
@ -85,8 +86,9 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
oAuthSourceRequest: data as unknown as OAuthSourceRequest, oAuthSourceRequest: data as unknown as OAuthSourceRequest,
}); });
} }
const c = await config(); const { capabilities } = ServerContext.config;
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
if (capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["icon"]; const icon = this.getFormFiles()["icon"];
if (icon || this.clearIcon) { if (icon || this.clearIcon) {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({ await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({

View File

@ -6,7 +6,8 @@ import {
GroupMatchingModeToLabel, GroupMatchingModeToLabel,
UserMatchingModeToLabel, UserMatchingModeToLabel,
} from "@goauthentik/admin/sources/oauth/utils"; } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ServerContext } from "@goauthentik/common/server-context.js";
import { import {
CapabilitiesEnum, CapabilitiesEnum,
WithCapabilitiesConfig, WithCapabilitiesConfig,
@ -61,8 +62,9 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
sAMLSourceRequest: data, sAMLSourceRequest: data,
}); });
} }
const c = await config(); const { capabilities } = ServerContext.config;
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
if (capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["icon"]; const icon = this.getFormFiles()["icon"];
if (icon || this.clearIcon) { if (icon || this.clearIcon) {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({ await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({

View File

@ -1,5 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
import { Form } from "@goauthentik/elements/forms/Form"; import { Form } from "@goauthentik/elements/forms/Form";
@ -21,7 +21,7 @@ export class UserImpersonateForm extends Form<ImpersonationRequest> {
impersonationRequest: data, impersonationRequest: data,
}) })
.then(() => { .then(() => {
window.location.href = globalAK().api.base; window.location.href = ServerContext.baseURL;
}); });
} }

View File

@ -5,23 +5,14 @@ import {
LoggingMiddleware, LoggingMiddleware,
} from "@goauthentik/common/api/middleware"; } from "@goauthentik/common/api/middleware";
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants"; import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import { SentryMiddleware } from "@goauthentik/common/sentry";
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api"; import { Configuration, CurrentBrand } from "@goauthentik/api";
// HACK: Workaround for ESBuild not being able to hoist import statement across entrypoints. // HACK: Workaround for ESBuild not being able to hoist import statement across entrypoints.
// This can be removed after ESBuild uses a single build context for all entrypoints. // This can be removed after ESBuild uses a single build context for all entrypoints.
export { CSRFHeaderName }; export { CSRFHeaderName };
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config);
export function config(): Promise<Config> {
if (!globalConfigPromise) {
globalConfigPromise = new RootApi(DEFAULT_CONFIG).rootConfigRetrieve();
}
return globalConfigPromise;
}
export function brandSetFavicon(brand: CurrentBrand) { export function brandSetFavicon(brand: CurrentBrand) {
/** /**
* <link rel="icon" href="/static/dist/assets/icons/icon.png"> * <link rel="icon" href="/static/dist/assets/icons/icon.png">
@ -53,27 +44,22 @@ export function brandSetLocale(brand: CurrentBrand) {
); );
} }
let globalBrandPromise: Promise<CurrentBrand> | undefined = Promise.resolve(globalAK().brand); export function getMetaContent(key: string): string {
export function brand(): Promise<CurrentBrand> { const metaEl = document.querySelector<HTMLMetaElement>(`meta[name=${key}]`);
if (!globalBrandPromise) { if (!metaEl) return "";
globalBrandPromise = new CoreApi(DEFAULT_CONFIG)
.coreBrandsCurrentRetrieve() return metaEl.content;
.then((brand) => {
brandSetFavicon(brand);
brandSetLocale(brand);
return brand;
});
}
return globalBrandPromise;
} }
export const DEFAULT_CONFIG = new Configuration({ export const DEFAULT_CONFIG = new Configuration({
basePath: `${globalAK().api.base}api/v3`, basePath: `${ServerContext.baseURL}api/v3`,
headers: {
"sentry-trace": ServerContext.sentryTrace,
},
middleware: [ middleware: [
new CSRFMiddleware(), new CSRFMiddleware(),
new EventMiddleware(), new EventMiddleware(),
new LoggingMiddleware(globalAK().brand), new LoggingMiddleware(ServerContext.brand),
new SentryMiddleware(),
], ],
}); });

View File

@ -1,59 +0,0 @@
import { Config, ConfigFromJSON, CurrentBrand, CurrentBrandFromJSON } from "@goauthentik/api";
export interface GlobalAuthentik {
_converted?: boolean;
locale?: string;
flow?: {
layout: string;
};
config: Config;
brand: CurrentBrand;
versionFamily: string;
versionSubdomain: string;
build: string;
api: {
base: string;
relBase: string;
};
}
export interface AuthentikWindow {
authentik: GlobalAuthentik;
}
export function globalAK(): GlobalAuthentik {
const ak = (window as unknown as AuthentikWindow).authentik;
if (ak && !ak._converted) {
ak._converted = true;
ak.brand = CurrentBrandFromJSON(ak.brand);
ak.config = ConfigFromJSON(ak.config);
}
const apiBase = new URL(process.env.AK_API_BASE_PATH || window.location.origin);
if (!ak) {
return {
config: ConfigFromJSON({
capabilities: [],
}),
brand: CurrentBrandFromJSON({
ui_footer_links: [],
}),
versionFamily: "",
versionSubdomain: "",
build: "",
api: {
base: apiBase.toString(),
relBase: apiBase.pathname,
},
};
}
return ak;
}
export function docLink(path: string): string {
const ak = globalAK();
// Default case or beta build which should always point to latest
if (!ak || ak.build !== "") {
return `https://goauthentik.io${path}`;
}
return `https://${ak.versionSubdomain}.goauthentik.io${path}`;
}

View File

@ -1,122 +1,131 @@
import { VERSION } from "@goauthentik/common/constants"; import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { RouteInterfaceName, readInterfaceRouteParam } from "@goauthentik/elements/router/utils";
import { me } from "@goauthentik/common/users"; import { BrowserOptions, browserTracingIntegration, init, setTag, setUser } from "@sentry/browser";
import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils";
import {
ErrorEvent,
EventHint,
browserTracingIntegration,
init,
setTag,
setUser,
} from "@sentry/browser";
import { getTraceData } from "@sentry/core";
import * as Spotlight from "@spotlightjs/spotlight";
import { import { CapabilitiesEnum, Config, ResponseError, UserSelf } from "@goauthentik/api";
CapabilitiesEnum,
FetchParams,
Middleware,
RequestContext,
ResponseError,
} from "@goauthentik/api";
/** /**
* A generic error that can be thrown without triggering Sentry's reporting. * A generic error that can be thrown without triggering Sentry's reporting.
*
* @category Sentry
*/ */
export class SentryIgnoredError extends Error {} export class SentryIgnoredError extends Error {}
export const TAG_SENTRY_COMPONENT = "authentik.component"; /**
export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities"; * Attempt initializing Spotlight.
*
* @see {@link https://spotlightjs.com/ Spotlight}
* @category Sentry
*/
export async function tryInitializingSpotlight() {
return import("@spotlightjs/spotlight").then((Spotlight) =>
Spotlight.init({ injectImmediately: true }),
);
}
let _sentryConfigured = false; /**
* Default Sentry options for the browser.
export function configureSentry(canDoPpi = false) { *
const cfg = globalAK().config; * @category Sentry
const debug = cfg.capabilities.includes(CapabilitiesEnum.CanDebug); */
if (!cfg.errorReporting.enabled && !debug) { const DEFAULT_SENTRY_BROWSER_OPTIONS = {
return cfg; ignoreErrors: [
} /network/gi,
init({ /fetch/gi,
dsn: cfg.errorReporting.sentryDsn, /module/gi,
ignoreErrors: [ // Error on edge on ios,
/network/gi, // https://stackoverflow.com/questions/69261499/what-is-instantsearchsdkjsbridgeclearhighlight
/fetch/gi, /instantSearchSDKJSBridgeClearHighlight/gi,
/module/gi, // Seems to be an issue in Safari and Firefox
// Error on edge on ios, /MutationObserver.observe/gi,
// https://stackoverflow.com/questions/69261499/what-is-instantsearchsdkjsbridgeclearhighlight /NS_ERROR_FAILURE/gi,
/instantSearchSDKJSBridgeClearHighlight/gi, ],
// Seems to be an issue in Safari and Firefox release: `authentik@${VERSION}`,
/MutationObserver.observe/gi, integrations: [
/NS_ERROR_FAILURE/gi, browserTracingIntegration({
], shouldCreateSpanForRequest: (url: string) => {
release: `authentik@${VERSION}`, return url.startsWith(window.location.host);
integrations: [ },
browserTracingIntegration({ }),
// https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#custom-routing ],
instrumentNavigation: false, beforeSend: (event, hint) => {
instrumentPageLoad: false, if (!hint) {
traceFetch: false,
}),
],
tracePropagationTargets: [window.location.origin],
tracesSampleRate: debug ? 1.0 : cfg.errorReporting.tracesSampleRate,
environment: cfg.errorReporting.environment,
beforeSend: (
event: ErrorEvent,
hint: EventHint,
): ErrorEvent | PromiseLike<ErrorEvent | null> | null => {
if (!hint) {
return event;
}
if (hint.originalException instanceof SentryIgnoredError) {
return null;
}
if (
hint.originalException instanceof ResponseError ||
hint.originalException instanceof DOMException
) {
return null;
}
return event; return event;
}, }
}); if (hint.originalException instanceof SentryIgnoredError) {
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(",")); return null;
if (window.location.pathname.includes("if/")) { }
setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`); if (
} hint.originalException instanceof ResponseError ||
if (debug) { hint.originalException instanceof DOMException
Spotlight.init({ ) {
injectImmediately: true, return null;
integrations: [ }
Spotlight.sentry({ return event;
injectIntoSDK: true, },
}), } as const satisfies BrowserOptions;
],
}); /**
console.debug("authentik/config: Enabled Sentry Spotlight"); * Include the given user in Sentry events.
} *
if (cfg.errorReporting.sendPii && canDoPpi) { * @category Sentry
me().then((user) => { */
setUser({ email: user.user.email }); export function setSentryPII(user: UserSelf): void {
console.debug("authentik/config: Sentry with PII enabled."); console.debug("authentik/sentry: PII enabled.");
});
} else { setUser({ email: user.email });
console.debug("authentik/config: Sentry enabled.");
}
_sentryConfigured = true;
} }
export class SentryMiddleware implements Middleware { /**
pre?(context: RequestContext): Promise<FetchParams | void> { * Include the given capabilities in Sentry events.
if (!_sentryConfigured) { *
return Promise.resolve(context); * @category Sentry
} */
const traceData = getTraceData(); export function setSentryCapabilities(capabilities: CapabilitiesEnum[]): void {
// @ts-ignore setTag("authentik.capabilities", capabilities.join(","));
context.init.headers["baggage"] = traceData["baggage"]; }
// @ts-ignore
context.init.headers["sentry-trace"] = traceData["sentry-trace"]; /**
return Promise.resolve(context); * Include the given route interface in Sentry events.
*
* @category Sentry
*/
export function setSentryInterface(interfaceName: RouteInterfaceName) {
setTag("authentik.component", `web/${interfaceName}}`);
}
/**
* Attempt to initialize Sentry with the given configuration.
*
* @see {@linkcode setSentryPII}
* @see {@linkcode setSentryCapabilities}
* @see {@linkcode setSentryInterface}
* @category Sentry
*/
export function tryInitializeSentry({ errorReporting, capabilities }: Config): void {
if (!errorReporting.enabled) return;
init({
...DEFAULT_SENTRY_BROWSER_OPTIONS,
dsn: errorReporting.sentryDsn,
tracesSampleRate: errorReporting.tracesSampleRate,
environment: errorReporting.environment,
enabled: process.env.NODE_ENV !== "development",
});
setSentryCapabilities(capabilities);
setSentryInterface(readInterfaceRouteParam());
if (
process.env.NODE_ENV === "development" &&
capabilities.includes(CapabilitiesEnum.CanDebug)
) {
tryInitializingSpotlight()
.then(() => {
console.debug("authentik/sentry: Sentry with Spotlight enabled.");
})
.catch((err) => {
console.warn("authentik/sentry: Failed to load Spotlight", err);
});
} }
} }

View File

@ -0,0 +1,116 @@
/**
* @file Server context singleton.
*/
import { Config, ConfigFromJSON, CurrentBrand, CurrentBrandFromJSON } from "@goauthentik/api";
function readMetaElement(key: string, fallback: string = ""): string {
const metaElement = document.querySelector<HTMLMetaElement>(`meta[name="${key}"]`);
const value = metaElement?.getAttribute("content") || fallback;
return value;
}
interface ServerContextValue {
/**
* Server-injected authentik configuration.
*/
config: Readonly<Config>;
/**
* Brand information used to customize the UI.
*/
brand: Readonly<CurrentBrand>;
/**
* A semantic versioning string representing the current version of authentik.
*/
versionFamily: string;
/**
* A subdomain-compatible version string representing the current version of authentik.
*/
versionSubdomain: string;
/**
* A build hash string representing the current build of authentik.
*/
build: string;
/**
* The base URL of the authentik instance.
*/
baseURL: string;
/**
* The relative base URL of the authentik instance.
*/
baseURLRelative: string;
/**
* The layout of the flow, if any.
*/
flowLayout: string;
/**
* The Sentry trace ID for the current request.
*/
sentryTrace: string;
}
/**
* Reads the server context from the DOM.
*/
export function refreshServerContext(): Readonly<ServerContextValue> {
const configElement = document.getElementById(":ak-config:");
const config = configElement?.textContent
? ConfigFromJSON(JSON.parse(configElement.textContent))
: ConfigFromJSON({
capabilities: [],
});
const brandElement = document.getElementById(":ak-brand:");
const brand = brandElement?.textContent
? CurrentBrandFromJSON(JSON.parse(brandElement.textContent))
: CurrentBrandFromJSON({
ui_footer_links: [],
});
const apiBaseURL = new URL(process.env.AK_API_BASE_PATH || window.location.origin);
const value: ServerContextValue = {
sentryTrace: readMetaElement("sentry-trace"),
baseURL: readMetaElement("ak-base-url") || apiBaseURL.toString(),
baseURLRelative: readMetaElement("ak-base-url-rel"),
versionFamily: readMetaElement("ak-version-family"),
versionSubdomain: readMetaElement("ak-version-subdomain"),
build: readMetaElement("ak-build"),
flowLayout: readMetaElement("ak-flow-layout"),
config,
brand,
};
return value;
}
/**
* Server injected values used to configure application.
*
* @singleton
*/
export const ServerContext = refreshServerContext();
export function docLink(path: string): string {
const { build, versionSubdomain } = ServerContext;
// Default case or beta build which should always point to latest
if (build) {
return new URL(path, "https://goauthentik.io").toString();
}
return new URL(path, `https://${versionSubdomain}.goauthentik.io`).toString();
}

View File

@ -1,6 +1,6 @@
import { EVENT_MESSAGE, EVENT_WS_MESSAGE } from "@goauthentik/common/constants"; import { EVENT_MESSAGE, EVENT_WS_MESSAGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
import { ServerContext } from "@goauthentik/common/server-context";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
@ -22,11 +22,14 @@ export class WebsocketClient {
connect(): void { connect(): void {
if (navigator.webdriver) return; if (navigator.webdriver) return;
const apiURL = new URL(globalAK().api.base);
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${apiURL.host}${apiURL.pathname}ws/client/`; const apiURL = new URL(ServerContext.baseURL);
this.messageSocket = new WebSocket(wsUrl);
const wsURL = `${window.location.protocol.replace("http", "ws")}//${apiURL.host}${apiURL.pathname}ws/client/`;
this.messageSocket = new WebSocket(wsURL);
this.messageSocket.addEventListener("open", () => { this.messageSocket.addEventListener("open", () => {
console.debug(`authentik/ws: connected to ${wsUrl}`); console.debug(`authentik/ws: connected to ${wsURL}`);
this.retryDelay = 200; this.retryDelay = 200;
}); });
this.messageSocket.addEventListener("close", (e) => { this.messageSocket.addEventListener("close", (e) => {

View File

@ -3,7 +3,7 @@ import {
EVENT_API_DRAWER_TOGGLE, EVENT_API_DRAWER_TOGGLE,
EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config"; import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
@ -146,7 +146,7 @@ export class NavigationButtons extends AKElement {
<a <a
class="pf-c-button pf-m-plain" class="pf-c-button pf-m-plain"
type="button" type="button"
href="${globalAK().api.base}if/user/#/settings" href="${ServerContext.baseURL}if/user/#/settings"
> >
<pf-tooltip position="top" content=${msg("Settings")}> <pf-tooltip position="top" content=${msg("Settings")}>
<i class="fas fa-cog" aria-hidden="true"></i> <i class="fas fa-cog" aria-hidden="true"></i>
@ -200,7 +200,7 @@ export class NavigationButtons extends AKElement {
${this.renderSettings()} ${this.renderSettings()}
<div class="pf-c-page__header-tools-item"> <div class="pf-c-page__header-tools-item">
<a <a
href="${globalAK().api.base}flows/-/default/invalidation/" href="${ServerContext.baseURL}flows/-/default/invalidation/"
class="pf-c-button pf-m-plain" class="pf-c-button pf-m-plain"
> >
<pf-tooltip position="top" content=${msg("Sign out")}> <pf-tooltip position="top" content=${msg("Sign out")}>

View File

@ -1,4 +1,4 @@
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import { import {
StyleSheetInit, StyleSheetInit,
StyleSheetParent, StyleSheetParent,
@ -82,7 +82,7 @@ export class AKElement extends LitElement implements ThemedElement {
constructor() { constructor() {
super(); super();
const { brand } = globalAK(); const { brand } = ServerContext;
this.#preferredColorScheme = formatColorScheme(brand.uiTheme); this.#preferredColorScheme = formatColorScheme(brand.uiTheme);
this.activeTheme = resolveUITheme(brand?.uiTheme); this.activeTheme = resolveUITheme(brand?.uiTheme);

View File

@ -83,7 +83,7 @@ export class Diagram extends AKElement {
} }
}); });
if (!this.diagram) { if (!this.diagram) {
return html`<ak-empty-state loading></ak-empty-state>`; return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
} }
return html`${until( return html`${until(
mermaid.render("graph", this.diagram).then((r) => { mermaid.render("graph", this.diagram).then((r) => {

View File

@ -1,6 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import { ThemedElement } from "@goauthentik/common/theme"; import { ThemedElement } from "@goauthentik/common/theme";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js"; import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
@ -23,8 +23,8 @@ export class ConfigContextController implements ReactiveController {
initialValue: undefined, initialValue: undefined,
}); });
// Pre-hydrate from template-embedded config // Pre-hydrate from template-embedded config
this.context.setValue(globalAK().config); this.context.setValue(ServerContext.config);
this.host.config = globalAK().config; this.host.config = ServerContext.config;
this.fetch = this.fetch.bind(this); this.fetch = this.fetch.bind(this);
this.fetch(); this.fetch();
} }

View File

@ -23,20 +23,9 @@ const configContext = Symbol("configContext");
const modalController = Symbol("modalController"); const modalController = Symbol("modalController");
const versionContext = Symbol("versionContext"); const versionContext = Symbol("versionContext");
export abstract class LightInterface extends AKElement implements ThemedElement { export abstract class Interface extends AKElement implements ThemedElement {
protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase); protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase);
constructor() {
super();
const styleParent = resolveStyleSheetParent(document);
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
appendStyleSheet(styleParent, Interface.PFBaseStyleSheet);
}
}
export abstract class Interface extends LightInterface implements ThemedElement {
[configContext]: ConfigContextController; [configContext]: ConfigContextController;
[modalController]: ModalOrchestrationController; [modalController]: ModalOrchestrationController;
@ -49,6 +38,12 @@ export abstract class Interface extends LightInterface implements ThemedElement
constructor() { constructor() {
super(); super();
const styleParent = resolveStyleSheetParent(document);
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
appendStyleSheet(styleParent, Interface.PFBaseStyleSheet);
this.addController(new BrandContextController(this)); this.addController(new BrandContextController(this));
this[configContext] = new ConfigContextController(this); this[configContext] = new ConfigContextController(this);
this[modalController] = new ModalOrchestrationController(this); this[modalController] = new ModalOrchestrationController(this);

View File

@ -1,4 +1,4 @@
import { AuthenticatedInterface, Interface, LightInterface } from "./Interface"; import { AuthenticatedInterface, Interface } from "./Interface";
export { Interface, AuthenticatedInterface, LightInterface }; export { Interface, AuthenticatedInterface };
export default Interface; export default Interface;

View File

@ -3,9 +3,10 @@ import {
EVENT_WS_MESSAGE, EVENT_WS_MESSAGE,
TITLE_DEFAULT, TITLE_DEFAULT,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config"; import { getConfigForUser } from "@goauthentik/common/ui/config";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { DefaultBrand } from "@goauthentik/common/ui/config";
import { UIConfig, UserDisplay } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import "@goauthentik/components/ak-nav-buttons"; import "@goauthentik/components/ak-nav-buttons";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
@ -404,7 +405,7 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}> <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}>
<a <a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
href="${globalAK().api.base}if/user/" href="${ServerContext.baseURL}if/user/"
slot="extra" slot="extra"
> >
${msg("User interface")} ${msg("User interface")}

View File

@ -1,5 +1,3 @@
import { globalAK } from "@goauthentik/common/global";
import { LOCALES as RAW_LOCALES, enLocale } from "./definitions"; import { LOCALES as RAW_LOCALES, enLocale } from "./definitions";
import { AkLocale } from "./types"; import { AkLocale } from "./types";
@ -51,7 +49,7 @@ export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): s
userReq, userReq,
window.navigator?.language ?? TOMBSTONE, window.navigator?.language ?? TOMBSTONE,
brandReq, brandReq,
globalAK()?.locale ?? TOMBSTONE, document.documentElement.getAttribute("lang") ?? TOMBSTONE,
DEFAULT_LOCALE, DEFAULT_LOCALE,
].filter(isLocaleCandidate); ].filter(isLocaleCandidate);

View File

@ -1,4 +1,4 @@
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider"; import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
@ -76,7 +76,7 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
: "pf-m-gold"}" : "pf-m-gold"}"
> >
${message} ${message}
<a href="${globalAK().api.base}if/admin/#/enterprise/licenses" <a href="${ServerContext.baseURL}if/admin/#/enterprise/licenses"
>${msg("Click here for more info.")}</a >${msg("Click here for more info.")}</a
> >
</div>`; </div>`;

View File

@ -230,7 +230,9 @@ export abstract class AKChart<T> extends AKElement {
<p slot="body">${pluckErrorDetail(this.error)}</p> <p slot="body">${pluckErrorDetail(this.error)}</p>
</ak-empty-state> </ak-empty-state>
` `
: html`${this.chart ? html`` : html`<ak-empty-state loading></ak-empty-state>`}`} : html`${this.chart
? html``
: html`<ak-empty-state ?loading="${true}"></ak-empty-state>`}`}
${this.centerText ? html` <span>${this.centerText}</span> ` : html``} ${this.centerText ? html` <span>${this.centerText}</span> ` : html``}
<canvas style="${this.chart === undefined ? "display: none;" : ""}"></canvas> <canvas style="${this.chart === undefined ? "display: none;" : ""}"></canvas>
</div> </div>

View File

@ -71,7 +71,7 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
renderVisible(): TemplateResult { renderVisible(): TemplateResult {
if ((this._instancePk && !this.instance) || !this._initialDataLoad) { if ((this._instancePk && !this.instance) || !this._initialDataLoad) {
return html`<ak-empty-state loading></ak-empty-state>`; return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
} }
return super.renderVisible(); return super.renderVisible();
} }

View File

@ -1,6 +1,6 @@
import { RequestInfo } from "@goauthentik/common/api/middleware"; import { RequestInfo } from "@goauthentik/common/api/middleware";
import { EVENT_API_DRAWER_TOGGLE, EVENT_REQUEST_POST } from "@goauthentik/common/constants"; import { EVENT_API_DRAWER_TOGGLE, EVENT_REQUEST_POST } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import { formatElapsedTime } from "@goauthentik/common/temporal"; import { formatElapsedTime } from "@goauthentik/common/temporal";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
@ -92,7 +92,7 @@ export class APIDrawer extends AKElement {
<h1 class="pf-c-notification-drawer__header-title"> <h1 class="pf-c-notification-drawer__header-title">
${msg("API Requests")} ${msg("API Requests")}
</h1> </h1>
<a href="${globalAK().api.base}api/v3/" target="_blank" <a href="${ServerContext.baseURL}api/v3/" target="_blank"
>${msg("Open API Browser")}</a >${msg("Open API Browser")}</a
> >
</div> </div>

View File

@ -1,8 +1,8 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_REFRESH } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { actionToLabel } from "@goauthentik/common/labels"; import { actionToLabel } from "@goauthentik/common/labels";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
import { ServerContext } from "@goauthentik/common/server-context";
import { formatElapsedTime } from "@goauthentik/common/temporal"; import { formatElapsedTime } from "@goauthentik/common/temporal";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
@ -99,7 +99,7 @@ export class NotificationDrawer extends AKElement {
html` html`
<a <a
class="pf-c-dropdown__toggle pf-m-plain" class="pf-c-dropdown__toggle pf-m-plain"
href="${globalAK().api.base}if/admin/#/events/log/${item.event?.pk}" href="${ServerContext.baseURL}if/admin/#/events/log/${item.event?.pk}"
> >
<pf-tooltip position="top" content=${msg("Show details")}> <pf-tooltip position="top" content=${msg("Show details")}>
<i class="fas fa-share-square"></i> <i class="fas fa-share-square"></i>

View File

@ -51,7 +51,7 @@ export class Route {
if (this.callback) { if (this.callback) {
return html`${until( return html`${until(
this.callback(args), this.callback(args),
html`<ak-empty-state loading></ak-empty-state>`, html`<ak-empty-state ?loading=${true}></ak-empty-state>`,
)}`; )}`;
} }
if (this.element) { if (this.element) {

View File

@ -6,35 +6,19 @@ import { TemplateResult } from "lit";
export class RouteMatch { export class RouteMatch {
route: Route; route: Route;
arguments: { [key: string]: string }; arguments: { [key: string]: string };
fullURL: string; fullUrl?: string;
constructor(route: Route, fullUrl: string) { constructor(route: Route) {
this.route = route; this.route = route;
this.arguments = {}; this.arguments = {};
this.fullURL = fullUrl;
} }
render(): TemplateResult { render(): TemplateResult {
return this.route.render(this.arguments); return this.route.render(this.arguments);
} }
/**
* Convert the matched Route's URL regex to a sanitized, readable URL by replacing
* all regex values with placeholders according to the name of their regex group.
*
* @returns The sanitized URL for logging/tracing.
*/
sanitizedURL() {
let cleanedURL = this.fullURL;
for (const match of Object.keys(this.arguments)) {
const value = this.arguments[match];
cleanedURL = cleanedURL?.replace(value, `:${match}`);
}
return cleanedURL;
}
toString(): string { toString(): string {
return `<RouteMatch url=${this.sanitizedURL()} route=${this.route} arguments=${JSON.stringify( return `<RouteMatch url=${this.fullUrl} route=${this.route} arguments=${JSON.stringify(
this.arguments, this.arguments,
)}>`; )}>`;
} }

View File

@ -3,15 +3,8 @@ import { AKElement } from "@goauthentik/elements/Base";
import { Route } from "@goauthentik/elements/router/Route"; import { Route } from "@goauthentik/elements/router/Route";
import { RouteMatch } from "@goauthentik/elements/router/RouteMatch"; import { RouteMatch } from "@goauthentik/elements/router/RouteMatch";
import "@goauthentik/elements/router/Router404"; import "@goauthentik/elements/router/Router404";
import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
getClient,
startBrowserTracingNavigationSpan,
startBrowserTracingPageLoadSpan,
} from "@sentry/browser";
import { Client, Span } from "@sentry/types";
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
// Poliyfill for hashchange.newURL, // Poliyfill for hashchange.newURL,
@ -60,9 +53,6 @@ export class RouterOutlet extends AKElement {
@property({ attribute: false }) @property({ attribute: false })
routes: Route[] = []; routes: Route[] = [];
private sentryClient?: Client;
private pageLoadSpan?: Span;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
css` css`
@ -79,15 +69,6 @@ export class RouterOutlet extends AKElement {
constructor() { constructor() {
super(); super();
window.addEventListener("hashchange", (ev: HashChangeEvent) => this.navigate(ev)); window.addEventListener("hashchange", (ev: HashChangeEvent) => this.navigate(ev));
this.sentryClient = getClient();
if (this.sentryClient) {
this.pageLoadSpan = startBrowserTracingPageLoadSpan(this.sentryClient, {
name: window.location.pathname,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "url",
},
});
}
} }
firstUpdated(): void { firstUpdated(): void {
@ -111,8 +92,9 @@ export class RouterOutlet extends AKElement {
this.routes.some((route) => { this.routes.some((route) => {
const match = route.url.exec(activeUrl); const match = route.url.exec(activeUrl);
if (match !== null) { if (match !== null) {
matchedRoute = new RouteMatch(route, activeUrl); matchedRoute = new RouteMatch(route);
matchedRoute.arguments = match.groups || {}; matchedRoute.arguments = match.groups || {};
matchedRoute.fullUrl = activeUrl;
console.debug("authentik/router: found match ", matchedRoute); console.debug("authentik/router: found match ", matchedRoute);
return true; return true;
} }
@ -125,31 +107,13 @@ export class RouterOutlet extends AKElement {
<ak-router-404 url=${activeUrl}></ak-router-404> <ak-router-404 url=${activeUrl}></ak-router-404>
</div>`; </div>`;
}); });
matchedRoute = new RouteMatch(route, activeUrl); matchedRoute = new RouteMatch(route);
matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {}; matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {};
matchedRoute.fullUrl = activeUrl;
} }
this.current = matchedRoute; this.current = matchedRoute;
} }
updated(changedProperties: PropertyValues<this>): void {
if (!changedProperties.has("current") || !this.current) return;
if (!this.sentryClient) return;
// https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#custom-routing
if (this.pageLoadSpan) {
this.pageLoadSpan.updateName(this.current.sanitizedURL());
this.pageLoadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, "route");
this.pageLoadSpan = undefined;
} else {
startBrowserTracingNavigationSpan(this.sentryClient, {
op: "navigation",
name: this.current.sanitizedURL(),
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "route",
},
});
}
}
render(): TemplateResult | undefined { render(): TemplateResult | undefined {
return this.current?.render(); return this.current?.render();
} }

View File

@ -1,5 +1,5 @@
import type { AdminInterface } from "@goauthentik/admin/AdminInterface/index.entrypoint.js"; import type { AdminInterface } from "@goauthentik/admin/AdminInterface/index.entrypoint.js";
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { DefaultBrand } from "@goauthentik/common/ui/config";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider"; import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
@ -45,7 +45,7 @@ export class SidebarVersion extends WithLicenseSummary(WithVersion(AKElement)) {
if (!this.version || !this.licenseSummary) { if (!this.version || !this.licenseSummary) {
return nothing; return nothing;
} }
let product = globalAK().brand.brandingTitle || DefaultBrand.brandingTitle; let product = ServerContext.brand.brandingTitle || DefaultBrand.brandingTitle;
if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) { if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`; product += ` ${msg("Enterprise")}`;
} }

View File

@ -121,7 +121,7 @@ export class SyncStatusCard extends AKElement {
renderSyncStatus(): TemplateResult { renderSyncStatus(): TemplateResult {
if (this.loading) { if (this.loading) {
return html`<ak-empty-state loading></ak-empty-state>`; return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
} }
if (!this.syncState) { if (!this.syncState) {
return html`${msg("No sync status.")}`; return html`${msg("No sync status.")}`;

View File

@ -19,7 +19,7 @@ describe("ak-empty-state", () => {
}); });
it("should render the default loader", async () => { it("should render the default loader", async () => {
render(html`<ak-empty-state loading header=${msg("Loading")}> </ak-empty-state>`); render(html`<ak-empty-state ?loading=${true} header=${msg("Loading")}> </ak-empty-state>`);
const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon"); const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon");
await expect(empty).toExist(); await expect(empty).toExist();

View File

@ -139,7 +139,8 @@ export class UserSourceSettingsPage extends AKElement {
})} })}
`} `}
` `
: html`<ak-empty-state loading header=${msg("Loading")}> </ak-empty-state>`} : html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
</ak-empty-state>`}
</ul>`; </ul>`;
} }
} }

View File

@ -4,8 +4,8 @@ import {
EVENT_FLOW_INSPECTOR_TOGGLE, EVENT_FLOW_INSPECTOR_TOGGLE,
TITLE_DEFAULT, TITLE_DEFAULT,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { tryInitializeSentry } from "@goauthentik/common/sentry";
import { configureSentry } from "@goauthentik/common/sentry"; import { ServerContext } from "@goauthentik/common/server-context";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { DefaultBrand } from "@goauthentik/common/ui/config";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Interface"; import { Interface } from "@goauthentik/elements/Interface";
@ -171,7 +171,6 @@ export class FlowExecutor extends Interface implements StageHost {
} }
constructor() { constructor() {
configureSentry();
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
const inspector = new URL(window.location.toString()).searchParams.get("inspector"); const inspector = new URL(window.location.toString()).searchParams.get("inspector");
@ -238,9 +237,12 @@ export class FlowExecutor extends Interface implements StageHost {
} }
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
tryInitializeSentry(ServerContext.config);
if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) { if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) {
this.inspectorAvailable = true; this.inspectorAvailable = true;
} }
this.loading = true; this.loading = true;
try { try {
const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({ const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({
@ -481,7 +483,8 @@ export class FlowExecutor extends Interface implements StageHost {
} }
getLayout(): string { getLayout(): string {
const prefilledFlow = globalAK()?.flow?.layout || FlowLayoutEnum.Stacked; const prefilledFlow = ServerContext.flowLayout || FlowLayoutEnum.Stacked;
if (this.challenge) { if (this.challenge) {
return this.challenge?.flowInfo?.layout || prefilledFlow; return this.challenge?.flowInfo?.layout || prefilledFlow;
} }
@ -521,7 +524,7 @@ export class FlowExecutor extends Interface implements StageHost {
<img <img
src="${themeImage( src="${themeImage(
this.brand?.brandingLogo ?? this.brand?.brandingLogo ??
globalAK()?.brand.brandingLogo ?? ServerContext.brand.brandingLogo ??
DefaultBrand.brandingLogo, DefaultBrand.brandingLogo,
)}" )}"
alt="${msg("authentik Logo")}" alt="${msg("authentik Logo")}"

View File

@ -1,4 +1,4 @@
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import "@goauthentik/flow/FormStatic"; import "@goauthentik/flow/FormStatic";
import { BaseStage } from "@goauthentik/flow/stages/base"; import { BaseStage } from "@goauthentik/flow/stages/base";
@ -24,7 +24,8 @@ export class SessionEnd extends BaseStage<SessionEndChallenge, unknown> {
render(): TemplateResult { render(): TemplateResult {
if (!this.challenge) { if (!this.challenge) {
return html`<ak-empty-state loading header=${msg("Loading")}> </ak-empty-state>`; return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
</ak-empty-state>`;
} }
return html`<header class="pf-c-login__main-header"> return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1> <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
@ -47,7 +48,7 @@ export class SessionEnd extends BaseStage<SessionEndChallenge, unknown> {
str`You've logged out of ${this.challenge.applicationName}. You can go back to the overview to launch another application, or log out of your authentik account.`, str`You've logged out of ${this.challenge.applicationName}. You can go back to the overview to launch another application, or log out of your authentik account.`,
)} )}
</p> </p>
<a href="${globalAK().api.base}" class="pf-c-button pf-m-primary"> <a href="${ServerContext.baseURL}" class="pf-c-button pf-m-primary">
${msg("Go back to overview")} ${msg("Go back to overview")}
</a> </a>
${this.challenge.invalidationFlowUrl ${this.challenge.invalidationFlowUrl

View File

@ -1,4 +1,4 @@
import { LightInterface } from "@goauthentik/elements/Interface"; import { Interface } from "@goauthentik/elements/Interface";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
@ -10,7 +10,7 @@ import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-loading") @customElement("ak-loading")
export class Loading extends LightInterface { export class Loading extends Interface {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
@ -25,6 +25,16 @@ export class Loading extends LightInterface {
]; ];
} }
registerContexts(): void {
// Stub function to avoid making API requests for things we don't need. The `Interface` base class loads
// a bunch of data that is used globally by various things, however this is an interface that is shown
// very briefly and we don't need any of that data.
}
async _initCustomCSS(): Promise<void> {
// Stub function to avoid fetching custom CSS.
}
render(): TemplateResult { render(): TemplateResult {
return html` <section return html` <section
class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl" class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"

View File

@ -1,5 +1,5 @@
import { PFSize } from "@goauthentik/common/enums.js"; import { PFSize } from "@goauthentik/common/enums.js";
import { globalAK } from "@goauthentik/common/global"; import { ServerContext } from "@goauthentik/common/server-context";
import { truncateWords } from "@goauthentik/common/utils"; import { truncateWords } from "@goauthentik/common/utils";
import "@goauthentik/elements/AppIcon"; import "@goauthentik/elements/AppIcon";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { AKElement, rootInterface } from "@goauthentik/elements/Base";
@ -82,8 +82,7 @@ export class LibraryApplication extends AKElement {
? html` ? html`
<a <a
class="pf-c-button pf-m-control pf-m-small pf-m-block" class="pf-c-button pf-m-control pf-m-small pf-m-block"
href="${globalAK().api href="${ServerContext.baseURL}if/admin/#/core/applications/${application?.slug}"
.base}if/admin/#/core/applications/${application?.slug}"
> >
<i class="fas fa-edit"></i>&nbsp;${msg("Edit")} <i class="fas fa-edit"></i>&nbsp;${msg("Edit")}
</a> </a>

View File

@ -1,4 +1,4 @@
import { docLink, globalAK } from "@goauthentik/common/global"; import { ServerContext, docLink } from "@goauthentik/common/server-context";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet"; import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
@ -49,7 +49,7 @@ export class LibraryPageApplicationEmptyList extends AKElement {
<a <a
aria-disabled="false" aria-disabled="false"
class="cta pf-c-button pf-m-secondary" class="cta pf-c-button pf-m-secondary"
href="${globalAK().api.base}if/admin/${href}" href="${ServerContext.baseURL}if/admin/${href}"
>${msg("Create a new application")}</a >${msg("Create a new application")}</a
> >
</div> </div>

View File

@ -102,7 +102,7 @@ export class LibraryPage extends AKElement {
} }
loading() { loading() {
return html`<ak-empty-state loading header=${msg("Loading")}> </ak-empty-state>`; return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}> </ak-empty-state>`;
} }
running() { running() {

View File

@ -4,8 +4,8 @@ import {
EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE,
EVENT_WS_MESSAGE, EVENT_WS_MESSAGE,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { setSentryPII, tryInitializeSentry } from "@goauthentik/common/sentry";
import { configureSentry } from "@goauthentik/common/sentry"; import { ServerContext } from "@goauthentik/common/server-context";
import { UIConfig, getConfigForUser } from "@goauthentik/common/ui/config"; import { UIConfig, getConfigForUser } from "@goauthentik/common/ui/config";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { DefaultBrand } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
@ -170,14 +170,14 @@ class UserInterfacePresentation extends AKElement {
return html`<a return html`<a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
href="${globalAK().api.base}if/admin/" href="${ServerContext.baseURL}if/admin/"
slot="extra" slot="extra"
> >
${msg("Admin interface")} ${msg("Admin interface")}
</a> </a>
<a <a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none-on-md pf-u-display-block" class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none-on-md pf-u-display-block"
href="${globalAK().api.base}if/admin/" href="${ServerContext.baseURL}if/admin/"
slot="extra" slot="extra"
> >
${msg("Admin")} ${msg("Admin")}
@ -281,10 +281,12 @@ export class UserInterface extends AuthenticatedInterface {
me?: SessionUser; me?: SessionUser;
constructor() { constructor() {
configureSentry(true);
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
this.fetchConfigurationDetails(); this.fetchConfigurationDetails();
tryInitializeSentry(ServerContext.config);
this.toggleNotificationDrawer = this.toggleNotificationDrawer.bind(this); this.toggleNotificationDrawer = this.toggleNotificationDrawer.bind(this);
this.toggleApiDrawer = this.toggleApiDrawer.bind(this); this.toggleApiDrawer = this.toggleApiDrawer.bind(this);
this.fetchConfigurationDetails = this.fetchConfigurationDetails.bind(this); this.fetchConfigurationDetails = this.fetchConfigurationDetails.bind(this);
@ -325,6 +327,8 @@ export class UserInterface extends AuthenticatedInterface {
this.me = session; this.me = session;
this.uiConfig = getConfigForUser(session.user); this.uiConfig = getConfigForUser(session.user);
setSentryPII(session.user);
new EventsApi(DEFAULT_CONFIG) new EventsApi(DEFAULT_CONFIG)
.eventsNotificationsList({ .eventsNotificationsList({
seen: false, seen: false,

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