Compare commits

..

12 Commits

Author SHA1 Message Date
195091ed3b idk
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-27 19:09:33 +01:00
4de3f1f4b8 only create websocket connection for non-frame mode
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:18 +01:00
af4f1b3421 revoke access token when user logs out
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:18 +01:00
77b816ad51 fix interface and non frame redirect
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:18 +01:00
b28dd485a0 don't show logo when using frame mode
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:18 +01:00
4701389745 re-fix style
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:18 +01:00
0d0097e956 idk
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:18 +01:00
b42eb0706d set schema
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:18 +01:00
3afe386e18 also pass raw email token for custom email templates
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:18 +01:00
34dd9c0b63 add CSP middleware that allows frame embeds based on brand
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:18 +01:00
b2f2fd241d prepare flow frame
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:18 +01:00
828f477548 add default app and restrict
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-26 15:21:16 +01:00
95 changed files with 997 additions and 1174 deletions

View File

@ -19,6 +19,8 @@ from guardian.models import UserObjectPermission
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import BaseSerializer, Serializer
from structlog.stdlib import BoundLogger, get_logger
from structlog.testing import capture_logs
from structlog.types import EventDict
from yaml import load
from authentik.blueprints.v1.common import (
@ -40,7 +42,6 @@ from authentik.core.models import (
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import LicenseUsage
from authentik.enterprise.providers.rac.models import ConnectionToken
from authentik.events.logs import LogEvent, capture_logs
from authentik.events.models import SystemTask
from authentik.events.utils import cleanse_dict
from authentik.flows.models import FlowToken, Stage
@ -160,7 +161,7 @@ class Importer:
def updater(value) -> Any:
if value in self.__pk_map:
self.logger.debug("Updating reference in entry", value=value)
self.logger.debug("updating reference in entry", value=value)
return self.__pk_map[value]
return value
@ -249,7 +250,7 @@ class Importer:
model_instance = existing_models.first()
if not isinstance(model(), BaseMetaModel) and model_instance:
self.logger.debug(
"Initialise serializer with instance",
"initialise serializer with instance",
model=model,
instance=model_instance,
pk=model_instance.pk,
@ -259,14 +260,14 @@ class Importer:
elif model_instance and entry.state == BlueprintEntryDesiredState.MUST_CREATED:
raise EntryInvalidError.from_entry(
(
f"State is set to {BlueprintEntryDesiredState.MUST_CREATED} "
f"state is set to {BlueprintEntryDesiredState.MUST_CREATED} "
"and object exists already",
),
entry,
)
else:
self.logger.debug(
"Initialised new serializer instance",
"initialised new serializer instance",
model=model,
**cleanse_dict(updated_identifiers),
)
@ -323,7 +324,7 @@ class Importer:
model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
except LookupError:
self.logger.warning(
"App or Model does not exist", app=model_app_label, model=model_name
"app or model does not exist", app=model_app_label, model=model_name
)
return False
# Validate each single entry
@ -335,7 +336,7 @@ class Importer:
if entry.get_state(self._import) == BlueprintEntryDesiredState.ABSENT:
serializer = exc.serializer
else:
self.logger.warning(f"Entry invalid: {exc}", entry=entry, error=exc)
self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc)
if raise_errors:
raise exc
return False
@ -355,14 +356,14 @@ class Importer:
and state == BlueprintEntryDesiredState.CREATED
):
self.logger.debug(
"Instance exists, skipping",
"instance exists, skipping",
model=model,
instance=instance,
pk=instance.pk,
)
else:
instance = serializer.save()
self.logger.debug("Updated model", model=instance)
self.logger.debug("updated model", model=instance)
if "pk" in entry.identifiers:
self.__pk_map[entry.identifiers["pk"]] = instance.pk
entry._state = BlueprintEntryState(instance)
@ -370,12 +371,12 @@ class Importer:
instance: Model | None = serializer.instance
if instance.pk:
instance.delete()
self.logger.debug("Deleted model", mode=instance)
self.logger.debug("deleted model", mode=instance)
continue
self.logger.debug("Entry to delete with no instance, skipping")
self.logger.debug("entry to delete with no instance, skipping")
return True
def validate(self, raise_validation_errors=False) -> tuple[bool, list[LogEvent]]:
def validate(self, raise_validation_errors=False) -> tuple[bool, list[EventDict]]:
"""Validate loaded blueprint export, ensure all models are allowed
and serializers have no errors"""
self.logger.debug("Starting blueprint import validation")
@ -389,7 +390,9 @@ class Importer:
):
successful = self._apply_models(raise_errors=raise_validation_errors)
if not successful:
self.logger.warning("Blueprint validation failed")
self.logger.debug("Blueprint validation failed")
for log in logs:
getattr(self.logger, log.get("log_level"))(**log)
self.logger.debug("Finished blueprint import validation")
self._import = orig_import
return successful, logs

View File

@ -30,7 +30,6 @@ from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, E
from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE
from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.events.logs import capture_logs
from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask, prefill_task
from authentik.events.utils import sanitize_dict
@ -212,15 +211,14 @@ def apply_blueprint(self: SystemTask, instance_pk: str):
if not valid:
instance.status = BlueprintInstanceStatus.ERROR
instance.save()
self.set_status(TaskStatus.ERROR, *logs)
self.set_status(TaskStatus.ERROR, *[x["event"] for x in logs])
return
applied = importer.apply()
if not applied:
instance.status = BlueprintInstanceStatus.ERROR
instance.save()
self.set_status(TaskStatus.ERROR, "Failed to apply")
return
with capture_logs() as logs:
applied = importer.apply()
if not applied:
instance.status = BlueprintInstanceStatus.ERROR
instance.save()
self.set_status(TaskStatus.ERROR, *logs)
return
instance.status = BlueprintInstanceStatus.SUCCESSFUL
instance.last_applied_hash = file_hash
instance.last_applied = now()

View File

@ -46,6 +46,7 @@ class BrandSerializer(ModelSerializer):
fields = [
"brand_uuid",
"domain",
"origin",
"default",
"branding_title",
"branding_logo",
@ -56,6 +57,7 @@ class BrandSerializer(ModelSerializer):
"flow_unenrollment",
"flow_user_settings",
"flow_device_code",
"default_application",
"web_certificate",
"attributes",
]

View File

@ -1,12 +1,17 @@
"""Inject brand into current request"""
from collections.abc import Callable
from typing import TYPE_CHECKING
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.utils.translation import activate
from authentik.brands.utils import get_brand_for_request
from authentik.lib.config import CONFIG
if TYPE_CHECKING:
from authentik.brands.models import Brand
class BrandMiddleware:
@ -25,3 +30,41 @@ class BrandMiddleware:
if locale != "":
activate(locale)
return self.get_response(request)
class BrandHeaderMiddleware:
"""Add headers from currently active brand"""
get_response: Callable[[HttpRequest], HttpResponse]
default_csp_elements: dict[str, list[str]] = {}
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
self.default_csp_elements = {
"style-src": ["'self'", "'unsafe-inline'"], # Required due to Lit/ShadowDOM
"script-src": ["'self'", "'unsafe-inline'"], # Required for generated scripts
"img-src": ["https:", "http:", "data:"],
"default-src": ["'self'"],
"object-src": ["'none'"],
"connect-src": ["'self'"],
}
if CONFIG.get_bool("error_reporting.enabled"):
self.default_csp_elements["connect-src"].append(
# Required for sentry (TODO: Dynamic)
"https://authentik.error-reporting.a7k.io"
)
if CONFIG.get_bool("debug"):
# Also allow spotlight sidecar connection
self.default_csp_elements["connect-src"].append("http://localhost:8969")
def get_csp(self, request: HttpRequest) -> str:
brand: "Brand" = request.brand
elements = self.default_csp_elements.copy()
if brand.origin != "":
elements["frame-ancestors"] = [brand.origin]
return ";".join(f"{attr} {" ".join(value)}" for attr, value in elements.items())
def __call__(self, request: HttpRequest) -> HttpResponse:
response = self.get_response(request)
response.headers["Content-Security-Policy"] = self.get_csp(request)
return response

View File

@ -0,0 +1,26 @@
# Generated by Django 5.0.3 on 2024-03-21 15:42
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0005_tenantuuid_to_branduuid"),
("authentik_core", "0033_alter_user_options"),
]
operations = [
migrations.AddField(
model_name="brand",
name="default_application",
field=models.ForeignKey(
default=None,
help_text="When set, external users will be redirected to this application after authenticating.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_core.application",
),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 5.0.3 on 2024-03-26 14:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0006_brand_default_application"),
]
operations = [
migrations.AddField(
model_name="brand",
name="origin",
field=models.TextField(
blank=True,
help_text="Origin domain that activates this brand. Can be left empty to not allow any origins.",
),
),
]

View File

@ -23,6 +23,12 @@ class Brand(SerializerModel):
"Domain that activates this brand. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`"
)
)
origin = models.TextField(
help_text=_(
"Origin domain that activates this brand. Can be left empty to not allow any origins."
),
blank=True,
)
default = models.BooleanField(
default=False,
)
@ -51,6 +57,16 @@ class Brand(SerializerModel):
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code"
)
default_application = models.ForeignKey(
"authentik_core.Application",
null=True,
default=None,
on_delete=models.SET_DEFAULT,
help_text=_(
"When set, external users will be redirected to this application after authenticating."
),
)
web_certificate = models.ForeignKey(
CertificateKeyPair,
null=True,

View File

@ -1,11 +1,15 @@
"""Brand utilities"""
from typing import Any
from urllib.parse import urlparse
from django.db.models import F, Q
from django.db.models import Value as V
from django.http import HttpResponse
from django.http.request import HttpRequest
from django.utils.cache import patch_vary_headers
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik import get_full_version
from authentik.brands.models import Brand
@ -13,13 +17,17 @@ from authentik.tenants.models import Tenant
_q_default = Q(default=True)
DEFAULT_BRAND = Brand(domain="fallback")
LOGGER = get_logger()
def get_brand_for_request(request: HttpRequest) -> Brand:
"""Get brand object for current request"""
query = Q(host_domain__iendswith=F("domain"))
if "Origin" in request.headers:
query &= Q(Q(origin=request.headers.get("Origin", "")) | Q(origin=""))
db_brands = (
Brand.objects.annotate(host_domain=V(request.get_host()))
.filter(Q(host_domain__iendswith=F("domain")) | _q_default)
.filter(Q(query) | _q_default)
.order_by("default")
)
brands = list(db_brands.all())
@ -42,3 +50,46 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
"sentry_trace": trace,
"version": get_full_version(),
}
def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: str):
"""Add headers to permit CORS requests from allowed_origins, with or without credentials,
with any headers."""
origin = request.META.get("HTTP_ORIGIN")
if not origin:
return response
# OPTIONS requests don't have an authorization header -> hence
# we can't extract the provider this request is for
# so for options requests we allow the calling origin without checking
allowed = request.method == "OPTIONS"
received_origin = urlparse(origin)
for allowed_origin in allowed_origins:
url = urlparse(allowed_origin)
if (
received_origin.scheme == url.scheme
and received_origin.hostname == url.hostname
and received_origin.port == url.port
):
allowed = True
if not allowed:
LOGGER.warning(
"CORS: Origin is not an allowed origin",
requested=received_origin,
allowed=allowed_origins,
)
return response
# From the CORS spec: The string "*" cannot be used for a resource that supports credentials.
response["Access-Control-Allow-Origin"] = origin
patch_vary_headers(response, ["Origin"])
response["Access-Control-Allow-Credentials"] = "true"
if request.method == "OPTIONS":
if "HTTP_ACCESS_CONTROL_REQUEST_HEADERS" in request.META:
response["Access-Control-Allow-Headers"] = request.META[
"HTTP_ACCESS_CONTROL_REQUEST_HEADERS"
]
response["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
return response

View File

@ -20,14 +20,15 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from structlog.testing import capture_logs
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import Application, User
from authentik.events.logs import LogEventSerializer, capture_logs
from authentik.events.models import EventAction
from authentik.events.utils import sanitize_dict
from authentik.lib.utils.file import (
FilePathSerializer,
FileUploadSerializer,
@ -181,9 +182,9 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
if request.user.is_superuser:
log_messages = []
for log in logs:
if log.attributes.get("process", "") == "PolicyProcess":
if log.get("process", "") == "PolicyProcess":
continue
log_messages.append(LogEventSerializer(log).data)
log_messages.append(sanitize_dict(log))
result.log_messages = log_messages
response = PolicyTestResultSerializer(result)
return Response(response.data)

View File

@ -6,7 +6,6 @@ from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import RedirectView
from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
@ -20,7 +19,12 @@ from authentik.core.api.transactional_applications import TransactionalApplicati
from authentik.core.api.users import UserViewSet
from authentik.core.views import apps
from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.interface import (
BrandDefaultRedirectView,
FlowInterfaceView,
InterfaceView,
RootRedirectView,
)
from authentik.core.views.session import EndSessionView
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.messages.consumer import MessageConsumer
@ -29,13 +33,11 @@ from authentik.root.middleware import ChannelsLoggingMiddleware
urlpatterns = [
path(
"",
login_required(
RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True)
),
login_required(RootRedirectView.as_view()),
name="root-redirect",
),
path(
# We have to use this format since everything else uses applications/o or applications/saml
# We have to use this format since everything else uses application/o or application/saml
"application/launch/<slug:application_slug>/",
apps.RedirectToAppLaunch.as_view(),
name="application-launch",
@ -43,12 +45,12 @@ urlpatterns = [
# Interfaces
path(
"if/admin/",
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")),
ensure_csrf_cookie(BrandDefaultRedirectView.as_view(template_name="if/admin.html")),
name="if-admin",
),
path(
"if/user/",
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")),
ensure_csrf_cookie(BrandDefaultRedirectView.as_view(template_name="if/user.html")),
name="if-user",
),
path(

View File

@ -3,15 +3,43 @@
from json import dumps
from typing import Any
from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView
from django.http import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.utils.translation import gettext as _
from django.views.generic.base import RedirectView, TemplateView
from rest_framework.request import Request
from authentik import get_build_hash
from authentik.admin.tasks import LOCAL_VERSION
from authentik.api.v3.config import ConfigView
from authentik.brands.api import CurrentBrandSerializer
from authentik.brands.models import Brand
from authentik.core.models import UserTypes
from authentik.flows.models import Flow
from authentik.policies.denied import AccessDeniedResponse
class RootRedirectView(RedirectView):
"""Root redirect view, redirect to brand's default application if set"""
pattern_name = "authentik_core:if-user"
query_string = True
def redirect_to_app(self, request: HttpRequest):
if request.user.is_authenticated and request.user.type == UserTypes.EXTERNAL:
brand: Brand = request.brand
if brand.default_application:
return redirect(
"authentik_core:application-launch",
application_slug=brand.default_application.slug,
)
return None
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
if redirect_response := RootRedirectView().redirect_to_app(request):
return redirect_response
return super().dispatch(request, *args, **kwargs)
class InterfaceView(TemplateView):
@ -27,6 +55,22 @@ class InterfaceView(TemplateView):
return super().get_context_data(**kwargs)
class BrandDefaultRedirectView(InterfaceView):
"""By default redirect to default app"""
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
if request.user.is_authenticated and request.user.type == UserTypes.EXTERNAL:
brand: Brand = request.brand
if brand.default_application:
return redirect(
"authentik_core:application-launch",
application_slug=brand.default_application.slug,
)
response = AccessDeniedResponse(self.request)
response.error_message = _("Interface can only be accessed by internal users.")
return super().dispatch(request, *args, **kwargs)
class FlowInterfaceView(InterfaceView):
"""Flow interface"""

View File

@ -12,6 +12,7 @@ from rest_framework.fields import (
ChoiceField,
DateTimeField,
FloatField,
ListField,
SerializerMethodField,
)
from rest_framework.request import Request
@ -20,7 +21,6 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ReadOnlyModelViewSet
from structlog.stdlib import get_logger
from authentik.events.logs import LogEventSerializer
from authentik.events.models import SystemTask, TaskStatus
from authentik.rbac.decorators import permission_required
@ -39,7 +39,7 @@ class SystemTaskSerializer(ModelSerializer):
duration = FloatField(read_only=True)
status = ChoiceField(choices=[(x.value, x.name) for x in TaskStatus])
messages = LogEventSerializer(many=True)
messages = ListField(child=CharField())
def get_full_name(self, instance: SystemTask) -> str:
"""Get full name with UID"""

View File

@ -1,82 +0,0 @@
from collections.abc import Generator
from contextlib import contextmanager
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
from django.utils.timezone import now
from rest_framework.fields import CharField, ChoiceField, DateTimeField, DictField
from structlog import configure, get_config
from structlog.stdlib import NAME_TO_LEVEL, ProcessorFormatter
from structlog.testing import LogCapture
from structlog.types import EventDict
from authentik.core.api.utils import PassiveSerializer
from authentik.events.utils import sanitize_dict
@dataclass()
class LogEvent:
event: str
log_level: str
logger: str
timestamp: datetime = field(default_factory=now)
attributes: dict[str, Any] = field(default_factory=dict)
@staticmethod
def from_event_dict(item: EventDict) -> "LogEvent":
event = item.pop("event")
log_level = item.pop("level").lower()
timestamp = datetime.fromisoformat(item.pop("timestamp"))
item.pop("pid", None)
# Sometimes log entries have both `level` and `log_level` set, but `level` is always set
item.pop("log_level", None)
return LogEvent(
event, log_level, item.pop("logger"), timestamp, attributes=sanitize_dict(item)
)
class LogEventSerializer(PassiveSerializer):
"""Single log message with all context logged."""
timestamp = DateTimeField()
log_level = ChoiceField(choices=tuple((x, x) for x in NAME_TO_LEVEL.keys()))
logger = CharField()
event = CharField()
attributes = DictField()
# TODO(2024.6?): This is a migration helper to return a correct API response for logs that
# have been saved in an older format (mostly just list[str] with just the messages)
def to_representation(self, instance):
if isinstance(instance, str):
instance = LogEvent(instance, "", "")
elif isinstance(instance, list):
instance = [LogEvent(x, "", "") for x in instance]
return super().to_representation(instance)
@contextmanager
def capture_logs(log_default_output=True) -> Generator[list[LogEvent], None, None]:
"""Capture log entries created"""
logs = []
cap = LogCapture()
# Modify `_Configuration.default_processors` set via `configure` but always
# keep the list instance intact to not break references held by bound
# loggers.
processors: list = get_config()["processors"]
old_processors = processors.copy()
try:
# clear processors list and use LogCapture for testing
if ProcessorFormatter.wrap_for_formatter in processors:
processors.remove(ProcessorFormatter.wrap_for_formatter)
processors.append(cap)
configure(processors=processors)
yield logs
for raw_log in cap.entries:
logs.append(LogEvent.from_event_dict(raw_log))
finally:
# remove LogCapture and restore original processors
processors.clear()
processors.extend(old_processors)
configure(processors=processors)

View File

@ -9,7 +9,6 @@ from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger
from tenant_schemas_celery.task import TenantTask
from authentik.events.logs import LogEvent
from authentik.events.models import Event, EventAction, TaskStatus
from authentik.events.models import SystemTask as DBSystemTask
from authentik.events.utils import sanitize_item
@ -25,7 +24,7 @@ class SystemTask(TenantTask):
save_on_success: bool
_status: TaskStatus
_messages: list[LogEvent]
_messages: list[str]
_uid: str | None
# Precise start time from perf_counter
@ -45,20 +44,15 @@ class SystemTask(TenantTask):
"""Set UID, so in the case of an unexpected error its saved correctly"""
self._uid = uid
def set_status(self, status: TaskStatus, *messages: LogEvent):
def set_status(self, status: TaskStatus, *messages: str):
"""Set result for current run, will overwrite previous result."""
self._status = status
self._messages = list(messages)
for idx, msg in enumerate(self._messages):
if not isinstance(msg, LogEvent):
self._messages[idx] = LogEvent(msg, logger=self.__name__, log_level="info")
self._messages = messages
def set_error(self, exception: Exception):
"""Set result to error and save exception"""
self._status = TaskStatus.ERROR
self._messages = [
LogEvent(exception_to_string(exception), logger=self.__name__, log_level="error")
]
self._messages = [exception_to_string(exception)]
def before_start(self, task_id, args, kwargs):
self._start_precise = perf_counter()
@ -104,7 +98,8 @@ class SystemTask(TenantTask):
def on_failure(self, exc, task_id, args, kwargs, einfo):
super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
if not self._status:
self.set_error(exc)
self._status = TaskStatus.ERROR
self._messages = exception_to_string(exc)
DBSystemTask.objects.update_or_create(
name=self.__name__,
uid=self._uid,

View File

@ -47,4 +47,3 @@ class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
filterset_fields = "__all__"
search_fields = ["stage__name"]
ordering = ["order"]
ordering_fields = ["order", "stage__name"]

View File

@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, ReadOnlyField
from rest_framework.fields import BooleanField, CharField, DictField, ListField, ReadOnlyField
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
@ -19,7 +19,7 @@ from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import CacheSerializer, LinkSerializer, PassiveSerializer
from authentik.events.logs import LogEventSerializer
from authentik.events.utils import sanitize_dict
from authentik.flows.api.flows_diagram import FlowDiagram, FlowDiagramSerializer
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow
@ -107,7 +107,7 @@ class FlowSetSerializer(FlowSerializer):
class FlowImportResultSerializer(PassiveSerializer):
"""Logs of an attempted flow import"""
logs = LogEventSerializer(many=True, read_only=True)
logs = ListField(child=DictField(), read_only=True)
success = BooleanField(read_only=True)
@ -184,7 +184,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
importer = Importer.from_string(file.read().decode())
valid, logs = importer.validate()
import_response.initial_data["logs"] = [LogEventSerializer(log).data for log in logs]
import_response.initial_data["logs"] = [sanitize_dict(log) for log in logs]
import_response.initial_data["success"] = valid
import_response.is_valid()
if not valid:

View File

@ -59,11 +59,11 @@ class FlowPlan:
markers: list[StageMarker] = field(default_factory=list)
def append_stage(self, stage: Stage, marker: StageMarker | None = None):
"""Append `stage` to the end of the plan, optionally with stage marker"""
"""Append `stage` to all stages, optionally with stage marker"""
return self.append(FlowStageBinding(stage=stage), marker)
def append(self, binding: FlowStageBinding, marker: StageMarker | None = None):
"""Append `stage` to the end of the plan, optionally with stage marker"""
"""Append `stage` to all stages, optionally with stage marker"""
self.bindings.append(binding)
self.markers.append(marker or StageMarker())

View File

@ -24,6 +24,7 @@ from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger
from authentik.brands.models import Brand
from authentik.brands.utils import cors_allow
from authentik.core.models import Application
from authentik.events.models import Event, EventAction, cleanse_dict
from authentik.flows.apps import HIST_FLOW_EXECUTION_STAGE_TIME
@ -155,6 +156,14 @@ class FlowExecutorView(APIView):
return plan
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
response = self.dispatch_wrapper(request, flow_slug)
origins = []
if request.brand.origin != "":
origins.append(request.brand.origin)
cors_allow(request, response, *origins)
return response
def dispatch_wrapper(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
with Hub.current.start_span(
op="authentik.flow.executor.dispatch", description=self.flow.slug
) as span:
@ -450,7 +459,7 @@ class FlowExecutorView(APIView):
return to_stage_response(self.request, challenge_view.get(self.request))
def cancel(self):
"""Cancel current flow execution"""
"""Cancel current execution and return a redirect"""
keys_to_delete = [
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN,

View File

@ -3,9 +3,9 @@
from dataclasses import dataclass
from structlog.stdlib import get_logger
from structlog.testing import capture_logs
from authentik import __version__, get_build_hash
from authentik.events.logs import LogEvent, capture_logs
from authentik.lib.config import CONFIG
from authentik.lib.sentry import SentryIgnoredException
from authentik.outposts.models import (
@ -63,21 +63,21 @@ class BaseController:
"""Called by scheduled task to reconcile deployment/service/etc"""
raise NotImplementedError
def up_with_logs(self) -> list[LogEvent]:
def up_with_logs(self) -> list[str]:
"""Call .up() but capture all log output and return it."""
with capture_logs() as logs:
self.up()
return logs
return [x["event"] for x in logs]
def down(self):
"""Handler to delete everything we've created"""
raise NotImplementedError
def down_with_logs(self) -> list[LogEvent]:
def down_with_logs(self) -> list[str]:
"""Call .down() but capture all log output and return it."""
with capture_logs() as logs:
self.down()
return logs
return [x["event"] for x in logs]
def __enter__(self):
return self

View File

@ -9,10 +9,10 @@ from kubernetes.client.exceptions import OpenApiException
from kubernetes.config.config_exception import ConfigException
from kubernetes.config.incluster_config import load_incluster_config
from kubernetes.config.kube_config import load_kube_config_from_dict
from structlog.testing import capture_logs
from urllib3.exceptions import HTTPError
from yaml import dump_all
from authentik.events.logs import LogEvent, capture_logs
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
@ -91,7 +91,7 @@ class KubernetesController(BaseController):
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
raise ControllerException(str(exc)) from exc
def up_with_logs(self) -> list[LogEvent]:
def up_with_logs(self) -> list[str]:
try:
all_logs = []
for reconcile_key in self.reconcile_order:
@ -104,9 +104,7 @@ class KubernetesController(BaseController):
continue
reconciler = reconciler_cls(self)
reconciler.up()
for log in logs:
log.logger = reconcile_key.title()
all_logs.extend(logs)
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
return all_logs
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
raise ControllerException(str(exc)) from exc
@ -124,7 +122,7 @@ class KubernetesController(BaseController):
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
raise ControllerException(str(exc)) from exc
def down_with_logs(self) -> list[LogEvent]:
def down_with_logs(self) -> list[str]:
try:
all_logs = []
for reconcile_key in self.reconcile_order:
@ -137,9 +135,7 @@ class KubernetesController(BaseController):
continue
reconciler = reconciler_cls(self)
reconciler.down()
for log in logs:
log.logger = reconcile_key.title()
all_logs.extend(logs)
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
return all_logs
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
raise ControllerException(str(exc)) from exc

View File

@ -149,8 +149,10 @@ def outpost_controller(
if not controller_type:
return
with controller_type(outpost, outpost.service_connection) as controller:
LOGGER.debug("---------------Outpost Controller logs starting----------------")
logs = getattr(controller, f"{action}_with_logs")()
LOGGER.debug("---------------Outpost Controller logs starting----------------")
for log in logs:
LOGGER.debug(log)
LOGGER.debug("-----------------Outpost Controller logs end-------------------")
except (ControllerException, ServiceConnectionInvalid) as exc:
self.set_error(exc)

View File

@ -1,11 +1,10 @@
"""Serializer for policy execution"""
from rest_framework.fields import BooleanField, CharField, ListField
from rest_framework.fields import BooleanField, CharField, DictField, ListField
from rest_framework.relations import PrimaryKeyRelatedField
from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.core.models import User
from authentik.events.logs import LogEventSerializer
class PolicyTestSerializer(PassiveSerializer):
@ -20,4 +19,4 @@ class PolicyTestResultSerializer(PassiveSerializer):
passing = BooleanField()
messages = ListField(child=CharField(), read_only=True)
log_messages = LogEventSerializer(many=True, read_only=True)
log_messages = ListField(child=DictField(), read_only=True)

View File

@ -11,11 +11,12 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from structlog.testing import capture_logs
from authentik.core.api.applications import user_app_cache_key
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import CacheSerializer, MetaNameSerializer, TypeCreateSerializer
from authentik.events.logs import LogEventSerializer, capture_logs
from authentik.events.utils import sanitize_dict
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer
from authentik.policies.models import Policy, PolicyBinding
@ -165,9 +166,9 @@ class PolicyViewSet(
result = proc.execute()
log_messages = []
for log in logs:
if log.attributes.get("process", "") == "PolicyProcess":
if log.get("process", "") == "PolicyProcess":
continue
log_messages.append(LogEventSerializer(log).data)
log_messages.append(sanitize_dict(log))
result.log_messages = log_messages
response = PolicyTestResultSerializer(result)
return Response(response.data)

View File

@ -13,7 +13,6 @@ from authentik.events.context_processors.base import get_context_processors
if TYPE_CHECKING:
from authentik.core.models import User
from authentik.events.logs import LogEvent
from authentik.policies.models import PolicyBinding
LOGGER = get_logger()
@ -75,7 +74,7 @@ class PolicyResult:
source_binding: PolicyBinding | None
source_results: list[PolicyResult] | None
log_messages: list[LogEvent] | None
log_messages: list[dict] | None
def __init__(self, passing: bool, *messages: str):
self.passing = passing

View File

@ -1,9 +1,9 @@
"""authentik oauth provider app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikProviderOAuth2Config(AppConfig):
class AuthentikProviderOAuth2Config(ManagedAppConfig):
"""authentik oauth provider app config"""
name = "authentik.providers.oauth2"
@ -13,3 +13,4 @@ class AuthentikProviderOAuth2Config(AppConfig):
"authentik.providers.oauth2.urls_root": "",
"authentik.providers.oauth2.urls": "application/o/",
}
default = True

View File

@ -0,0 +1,15 @@
from hashlib import sha256
from django.contrib.auth.signals import user_logged_out
from django.dispatch import receiver
from django.http import HttpRequest
from authentik.core.models import User
from authentik.providers.oauth2.models import AccessToken
@receiver(user_logged_out)
def user_logged_out_oauth_access_token(sender, request: HttpRequest, user: User, **_):
"""Revoke access tokens upon user logout"""
hashed_session_key = sha256(request.session.session_key.encode("ascii")).hexdigest()
AccessToken.objects.filter(user=user, session_id=hashed_session_key).delete()

View File

@ -4,11 +4,9 @@ import re
from base64 import b64decode
from binascii import Error
from typing import Any
from urllib.parse import urlparse
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.http.response import HttpResponseRedirect
from django.utils.cache import patch_vary_headers
from structlog.stdlib import get_logger
from authentik.core.middleware import CTX_AUTH_VIA, KEY_USER
@ -30,49 +28,6 @@ class TokenResponse(JsonResponse):
self["Pragma"] = "no-cache"
def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: str):
"""Add headers to permit CORS requests from allowed_origins, with or without credentials,
with any headers."""
origin = request.META.get("HTTP_ORIGIN")
if not origin:
return response
# OPTIONS requests don't have an authorization header -> hence
# we can't extract the provider this request is for
# so for options requests we allow the calling origin without checking
allowed = request.method == "OPTIONS"
received_origin = urlparse(origin)
for allowed_origin in allowed_origins:
url = urlparse(allowed_origin)
if (
received_origin.scheme == url.scheme
and received_origin.hostname == url.hostname
and received_origin.port == url.port
):
allowed = True
if not allowed:
LOGGER.warning(
"CORS: Origin is not an allowed origin",
requested=received_origin,
allowed=allowed_origins,
)
return response
# From the CORS spec: The string "*" cannot be used for a resource that supports credentials.
response["Access-Control-Allow-Origin"] = origin
patch_vary_headers(response, ["Origin"])
response["Access-Control-Allow-Credentials"] = "true"
if request.method == "OPTIONS":
if "HTTP_ACCESS_CONTROL_REQUEST_HEADERS" in request.META:
response["Access-Control-Allow-Headers"] = request.META[
"HTTP_ACCESS_CONTROL_REQUEST_HEADERS"
]
response["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
return response
def extract_access_token(request: HttpRequest) -> str | None:
"""
Get the access token using Authorization Request Header Field method.

View File

@ -25,7 +25,7 @@ class OAuthDeviceCodeFinishChallengeResponse(ChallengeResponse):
class OAuthDeviceCodeFinishStage(ChallengeStageView):
"""Stage to finish the OAuth device code flow"""
"""Stage show at the end of a device flow"""
response_class = OAuthDeviceCodeFinishChallengeResponse

View File

@ -3,7 +3,7 @@
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from django.views import View
from rest_framework.exceptions import ValidationError
from rest_framework.exceptions import ErrorDetail
from rest_framework.fields import CharField, IntegerField
from structlog.stdlib import get_logger
@ -57,7 +57,6 @@ def validate_code(code: int, request: HttpRequest) -> HttpResponse | None:
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider)
planner = FlowPlanner(token.provider.authorization_flow)
planner.allow_empty_flows = True
planner.use_cache = False
try:
plan = planner.plan(
request,
@ -129,13 +128,6 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
code = IntegerField()
component = CharField(default="ak-provider-oauth2-device-code")
def validate_code(self, code: int) -> HttpResponse | None:
"""Validate code and save the returned http response"""
response = validate_code(code, self.stage.request)
if not response:
raise ValidationError("Invalid code", "invalid")
return response
class OAuthDeviceCodeStage(ChallengeStageView):
"""Flow challenge for users to enter device codes"""
@ -151,4 +143,12 @@ class OAuthDeviceCodeStage(ChallengeStageView):
)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return response.validated_data["code"]
code = response.validated_data["code"]
validation = validate_code(code, self.request)
if not validation:
response._errors.setdefault("code", [])
response._errors["code"].append(ErrorDetail(_("Invalid code"), "invalid"))
return self.challenge_invalid(response)
# Run cancel to cleanup the current flow
self.executor.cancel()
return validation

View File

@ -8,6 +8,7 @@ from django.views import View
from guardian.shortcuts import get_anonymous_user
from structlog.stdlib import get_logger
from authentik.brands.utils import cors_allow
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.models import Application
from authentik.providers.oauth2.constants import (
@ -28,7 +29,6 @@ from authentik.providers.oauth2.models import (
ResponseTypes,
ScopeMapping,
)
from authentik.providers.oauth2.utils import cors_allow
LOGGER = get_logger()

View File

@ -20,6 +20,7 @@ from jwt import PyJWK, PyJWT, PyJWTError, decode
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.brands.utils import cors_allow
from authentik.core.middleware import CTX_AUTH_VIA
from authentik.core.models import (
USER_ATTRIBUTE_EXPIRES,
@ -59,7 +60,7 @@ from authentik.providers.oauth2.models import (
OAuth2Provider,
RefreshToken,
)
from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth
from authentik.providers.oauth2.utils import TokenResponse, extract_client_auth
from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES
from authentik.sources.oauth.models import OAuthSource
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS

View File

@ -11,6 +11,7 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger
from authentik.brands.utils import cors_allow
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.events.models import Event, EventAction
from authentik.flows.challenge import PermissionDict
@ -28,7 +29,7 @@ from authentik.providers.oauth2.models import (
RefreshToken,
ScopeMapping,
)
from authentik.providers.oauth2.utils import TokenResponse, cors_allow, protected_resource_view
from authentik.providers.oauth2.utils import TokenResponse, protected_resource_view
LOGGER = get_logger()

View File

@ -241,7 +241,7 @@ MIDDLEWARE = [
"django.middleware.common.CommonMiddleware",
"authentik.root.middleware.CsrfViewMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"authentik.brands.middleware.BrandHeaderMiddleware",
"authentik.core.middleware.ImpersonateMiddleware",
"django_prometheus.middleware.PrometheusAfterMiddleware",
]

View File

@ -118,6 +118,7 @@ class EmailStageView(ChallengeStageView):
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
"user": pending_user,
"expires": token.expires,
"token": token.key,
},
)
send_mails(current_stage, message)

View File

@ -7609,6 +7609,11 @@
"title": "Domain",
"description": "Domain that activates this brand. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`"
},
"origin": {
"type": "string",
"title": "Origin",
"description": "Origin domain that activates this brand. Can be left empty to not allow any origins."
},
"default": {
"type": "boolean",
"title": "Default"
@ -7652,6 +7657,11 @@
"type": "integer",
"title": "Flow device code"
},
"default_application": {
"type": "integer",
"title": "Default application",
"description": "When set, external users will be redirected to this application after authenticating."
},
"web_certificate": {
"type": "integer",
"title": "Web certificate",

2
go.mod
View File

@ -30,7 +30,7 @@ require (
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024022.7
goauthentik.io/api/v3 v3.2024022.5
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.18.0
golang.org/x/sync v0.6.0

4
go.sum
View File

@ -280,8 +280,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024022.7 h1:VR9OmcZvTzPSjit2Dx2EoHrLc9v9XRyjPXNpnGISWWM=
goauthentik.io/api/v3 v3.2024022.7/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2024022.5 h1:z1ZaVY/UpwpHAghf/PyYRSOQT7U9g8E2N23YlRB5BJQ=
goauthentik.io/api/v3 v3.2024022.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -35,7 +35,7 @@ type ProviderInstance struct {
cert *tls.Certificate
certUUID string
outpostName string
providerPk int32
outpostPk int32
searchAllowedGroups []*strfmt.UUID
boundUsersMutex *sync.RWMutex
boundUsers map[string]*flags.UserFlags

View File

@ -22,7 +22,7 @@ import (
func (ls *LDAPServer) getCurrentProvider(pk int32) *ProviderInstance {
for _, p := range ls.providers {
if p.providerPk == pk {
if p.outpostPk == pk {
return p
}
}
@ -83,7 +83,7 @@ func (ls *LDAPServer) Refresh() error {
gidStartNumber: provider.GetGidStartNumber(),
mfaSupport: provider.GetMfaSupport(),
outpostName: ls.ac.Outpost.Name,
providerPk: provider.Pk,
outpostPk: provider.Pk,
}
if kp := provider.Certificate.Get(); kp != nil {
err := ls.cs.AddKeypair(*kp)

View File

@ -6,10 +6,8 @@ import (
"net"
"sort"
"strings"
"sync"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/ldap/flags"
)
func parseCIDRs(raw string) []*net.IPNet {
@ -31,25 +29,6 @@ func parseCIDRs(raw string) []*net.IPNet {
return cidrs
}
func (rs *RadiusServer) getCurrentProvider(pk int32) *ProviderInstance {
for _, p := range rs.providers {
if p.providerPk == pk {
return p
}
}
return nil
}
func (rs *RadiusServer) getInvalidationFlow() string {
req, _, err := rs.ac.Client.CoreApi.CoreBrandsCurrentRetrieve(context.Background()).Execute()
if err != nil {
rs.log.WithError(err).Warning("failed to fetch brand config")
return ""
}
flow := req.GetFlowInvalidation()
return flow
}
func (rs *RadiusServer) Refresh() error {
outposts, _, err := rs.ac.Client.OutpostsApi.OutpostsRadiusList(context.Background()).Execute()
if err != nil {
@ -58,33 +37,17 @@ func (rs *RadiusServer) Refresh() error {
if len(outposts.Results) < 1 {
return errors.New("no radius provider defined")
}
invalidationFlow := rs.getInvalidationFlow()
providers := make([]*ProviderInstance, len(outposts.Results))
for idx, provider := range outposts.Results {
logger := log.WithField("logger", "authentik.outpost.radius").WithField("provider", provider.Name)
// Get existing instance so we can transfer boundUsers
existing := rs.getCurrentProvider(provider.Pk)
usersMutex := &sync.RWMutex{}
users := make(map[string]*flags.UserFlags)
if existing != nil {
usersMutex = existing.boundUsersMutex
// Shallow copy, no need to lock
users = existing.boundUsers
}
providers[idx] = &ProviderInstance{
SharedSecret: []byte(provider.GetSharedSecret()),
ClientNetworks: parseCIDRs(provider.GetClientNetworks()),
MFASupport: provider.GetMfaSupport(),
appSlug: provider.ApplicationSlug,
authenticationFlowSlug: provider.AuthFlowSlug,
invalidationFlowSlug: invalidationFlow,
s: rs,
log: logger,
providerPk: provider.Pk,
boundUsersMutex: usersMutex,
boundUsers: users,
SharedSecret: []byte(provider.GetSharedSecret()),
ClientNetworks: parseCIDRs(provider.GetClientNetworks()),
MFASupport: provider.GetMfaSupport(),
appSlug: provider.ApplicationSlug,
flowSlug: provider.AuthFlowSlug,
s: rs,
log: logger,
}
}
rs.providers = providers

View File

@ -4,17 +4,15 @@ import (
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/flow"
"goauthentik.io/internal/outpost/ldap/flags"
"goauthentik.io/internal/outpost/radius/metrics"
"layeh.com/radius"
"layeh.com/radius/rfc2865"
"layeh.com/radius/rfc2866"
)
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
username := rfc2865.UserName_GetString(r.Packet)
fe := flow.NewFlowExecutor(r.Context(), r.pi.authenticationFlowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{
fe := flow.NewFlowExecutor(r.Context(), r.pi.flowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{
"username": username,
"client": r.RemoteAddr(),
"requestId": r.ID(),
@ -66,28 +64,5 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
}).Inc()
return
}
// Get user info to store in context
userInfo, _, err := fe.ApiClient().CoreApi.CoreUsersMeRetrieve(r.Context()).Execute()
if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"type": "bind",
"reason": "user_info_fail",
}).Inc()
r.Log().WithError(err).Warning("failed to get user info")
return
}
response := r.Response(radius.CodeAccessAccept)
_ = rfc2866.AcctSessionID_SetString(response, fe.GetSession().String())
r.pi.boundUsersMutex.Lock()
r.pi.boundUsers[fe.GetSession().String()] = &flags.UserFlags{
Session: fe.GetSession(),
UserPk: userInfo.Original.Pk,
}
r.pi.boundUsersMutex.Unlock()
err = w.Write(response)
if err != nil {
r.Log().WithError(err).Warning("failed to write response")
}
_ = w.Write(r.Response(radius.CodeAccessAccept))
}

View File

@ -1,54 +0,0 @@
package radius
import (
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/flow"
"goauthentik.io/internal/outpost/ldap/flags"
"layeh.com/radius"
"layeh.com/radius/rfc2866"
)
func (rs *RadiusServer) Handle_DisconnectRequest(w radius.ResponseWriter, r *RadiusRequest) {
session := rfc2866.AcctSessionID_GetString(r.Packet)
sendFailResponse := func() {
failResponse := r.Response(radius.CodeDisconnectACK)
err := w.Write(failResponse)
if err != nil {
r.Log().WithError(err).Warning("failed to write response")
}
}
r.pi.boundUsersMutex.Lock()
var f *flags.UserFlags
if ff, ok := r.pi.boundUsers[session]; !ok {
r.pi.boundUsersMutex.Unlock()
sendFailResponse()
return
} else {
f = ff
}
r.pi.boundUsersMutex.Unlock()
fe := flow.NewFlowExecutor(r.Context(), r.pi.invalidationFlowSlug, rs.ac.Client.GetConfig(), log.Fields{
"client": r.RemoteAddr(),
"requestId": r.ID(),
})
fe.SetSession(f.Session)
fe.DelegateClientIP(r.RemoteAddr())
fe.Params.Add("goauthentik.io/outpost/radius", "true")
_, err := fe.Execute()
if err != nil {
r.log.WithError(err).Warning("failed to logout user")
sendFailResponse()
return
}
r.pi.boundUsersMutex.Lock()
delete(r.pi.boundUsers, session)
r.pi.boundUsersMutex.Unlock()
response := r.Response(radius.CodeDisconnectACK)
err = w.Write(response)
if err != nil {
r.Log().WithError(err).Warning("failed to write response")
}
}

View File

@ -74,12 +74,7 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
}
nr.pi = pi
switch nr.Code {
case radius.CodeAccessRequest:
if nr.Code == radius.CodeAccessRequest {
rs.Handle_AccessRequest(w, nr)
case radius.CodeDisconnectRequest:
rs.Handle_DisconnectRequest(w, nr)
default:
nr.Log().WithField("code", nr.Code.String()).Debug("Unsupported packet code")
}
}

View File

@ -9,25 +9,20 @@ import (
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/config"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ldap/flags"
"goauthentik.io/internal/outpost/radius/metrics"
"layeh.com/radius"
)
type ProviderInstance struct {
ClientNetworks []*net.IPNet
SharedSecret []byte
MFASupport bool
boundUsersMutex *sync.RWMutex
boundUsers map[string]*flags.UserFlags
providerPk int32
ClientNetworks []*net.IPNet
SharedSecret []byte
MFASupport bool
appSlug string
authenticationFlowSlug string
invalidationFlowSlug string
s *RadiusServer
log *log.Entry
appSlug string
flowSlug string
s *RadiusServer
log *log.Entry
}
type RadiusServer struct {

36
poetry.lock generated
View File

@ -1142,13 +1142,13 @@ bcrypt = ["bcrypt"]
[[package]]
name = "django-filter"
version = "24.2"
version = "24.1"
description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
optional = false
python-versions = ">=3.8"
files = [
{file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"},
{file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"},
{file = "django-filter-24.1.tar.gz", hash = "sha256:65cb43ce272077e5ac6aae1054d76c121cd6b552e296a82a13921e9371baf8c1"},
{file = "django_filter-24.1-py3-none-any.whl", hash = "sha256:335bcae6cbd3e984b024841070f567b22faea57594f27d37c52f8f131f8d8621"},
]
[package.dependencies]
@ -3332,20 +3332,22 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "requests-mock"
version = "1.12.1"
version = "1.11.0"
description = "Mock out responses from the requests package"
optional = false
python-versions = ">=3.5"
python-versions = "*"
files = [
{file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"},
{file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"},
{file = "requests-mock-1.11.0.tar.gz", hash = "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4"},
{file = "requests_mock-1.11.0-py2.py3-none-any.whl", hash = "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15"},
]
[package.dependencies]
requests = ">=2.22,<3"
requests = ">=2.3,<3"
six = "*"
[package.extras]
fixture = ["fixtures"]
test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testtools"]
[[package]]
name = "requests-oauthlib"
@ -3550,13 +3552,13 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"]
[[package]]
name = "selenium"
version = "4.19.0"
version = "4.18.1"
description = ""
optional = false
python-versions = ">=3.8"
files = [
{file = "selenium-4.19.0-py3-none-any.whl", hash = "sha256:5b4f49240d61e687a73f7968ae2517d403882aae3550eae2a229c745e619f1d9"},
{file = "selenium-4.19.0.tar.gz", hash = "sha256:d9dfd6d0b021d71d0a48b865fe7746490ba82b81e9c87b212360006629eb1853"},
{file = "selenium-4.18.1-py3-none-any.whl", hash = "sha256:b24a3cdd2d47c29832e81345bfcde0c12bb608738013e53c781b211b418df241"},
{file = "selenium-4.18.1.tar.gz", hash = "sha256:a11f67afa8bfac6b77e148c987b33f6b14eb1cae4d352722a75de1f26e3f0ae2"},
]
[package.dependencies]
@ -3568,13 +3570,13 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
[[package]]
name = "sentry-sdk"
version = "1.44.0"
version = "1.43.0"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = "*"
files = [
{file = "sentry-sdk-1.44.0.tar.gz", hash = "sha256:f7125a9235795811962d52ff796dc032cd1d0dd98b59beaced8380371cd9c13c"},
{file = "sentry_sdk-1.44.0-py2.py3-none-any.whl", hash = "sha256:eb65289da013ca92fad2694851ad2f086aa3825e808dc285bd7dcaf63602bb18"},
{file = "sentry-sdk-1.43.0.tar.gz", hash = "sha256:41df73af89d22921d8733714fb0fc5586c3461907e06688e6537d01a27e0e0f6"},
{file = "sentry_sdk-1.43.0-py2.py3-none-any.whl", hash = "sha256:8d768724839ca18d7b4c7463ef7528c40b7aa2bfbf7fe554d5f9a7c044acfd36"},
]
[package.dependencies]
@ -4201,13 +4203,13 @@ files = [
[[package]]
name = "webauthn"
version = "2.1.0"
version = "2.0.0"
description = "Pythonic WebAuthn"
optional = false
python-versions = "*"
files = [
{file = "webauthn-2.1.0-py3-none-any.whl", hash = "sha256:9e1cf916e5ed7c01d54a6dfcc19dacbd2b87b81d2648f001b1fcbcb7aa2ff130"},
{file = "webauthn-2.1.0.tar.gz", hash = "sha256:b196a4246c2818820857ba195c6e6e5398c761117f2269e3d2deab11c7995fc4"},
{file = "webauthn-2.0.0-py3-none-any.whl", hash = "sha256:644dc68af5caaade06be6a2a2278775e85116e92dd755ad7a49d992d51c82033"},
{file = "webauthn-2.0.0.tar.gz", hash = "sha256:12cc1759da98668b8242badc37c4129df300f89d89f5c183fac80e7b33c41dfd"},
]
[package.dependencies]

View File

@ -31334,6 +31334,10 @@ components:
type: string
description: Domain that activates this brand. Can be a superset, i.e. `a.b`
for `aa.b` and `ba.b`
origin:
type: string
description: Origin domain that activates this brand. Can be left empty
to not allow any origins.
default:
type: boolean
branding_title:
@ -31366,6 +31370,12 @@ components:
type: string
format: uuid
nullable: true
default_application:
type: string
format: uuid
nullable: true
description: When set, external users will be redirected to this application
after authenticating.
web_certificate:
type: string
format: uuid
@ -31384,6 +31394,10 @@ components:
minLength: 1
description: Domain that activates this brand. Can be a superset, i.e. `a.b`
for `aa.b` and `ba.b`
origin:
type: string
description: Origin domain that activates this brand. Can be left empty
to not allow any origins.
default:
type: boolean
branding_title:
@ -31419,6 +31433,12 @@ components:
type: string
format: uuid
nullable: true
default_application:
type: string
format: uuid
nullable: true
description: When set, external users will be redirected to this application
after authenticating.
web_certificate:
type: string
format: uuid
@ -33782,7 +33802,8 @@ components:
logs:
type: array
items:
$ref: '#/components/schemas/LogEvent'
type: object
additionalProperties: {}
readOnly: true
success:
type: boolean
@ -35514,48 +35535,6 @@ components:
type: string
required:
- link
LogEvent:
type: object
description: Single log message with all context logged.
properties:
timestamp:
type: string
format: date-time
log_level:
$ref: '#/components/schemas/LogLevelEnum'
logger:
type: string
event:
type: string
attributes:
type: object
additionalProperties: {}
required:
- attributes
- event
- log_level
- logger
- timestamp
LogLevelEnum:
enum:
- critical
- exception
- error
- warn
- warning
- info
- debug
- notset
type: string
description: |-
* `critical` - critical
* `exception` - exception
* `error` - error
* `warn` - warn
* `warning` - warning
* `info` - info
* `debug` - debug
* `notset` - notset
LoginChallengeTypes:
oneOf:
- $ref: '#/components/schemas/RedirectChallenge'
@ -38559,6 +38538,10 @@ components:
minLength: 1
description: Domain that activates this brand. Can be a superset, i.e. `a.b`
for `aa.b` and `ba.b`
origin:
type: string
description: Origin domain that activates this brand. Can be left empty
to not allow any origins.
default:
type: boolean
branding_title:
@ -38594,6 +38577,12 @@ components:
type: string
format: uuid
nullable: true
default_application:
type: string
format: uuid
nullable: true
description: When set, external users will be redirected to this application
after authenticating.
web_certificate:
type: string
format: uuid
@ -41350,7 +41339,8 @@ components:
log_messages:
type: array
items:
$ref: '#/components/schemas/LogEvent'
type: object
additionalProperties: {}
readOnly: true
required:
- log_messages
@ -44364,7 +44354,7 @@ components:
messages:
type: array
items:
$ref: '#/components/schemas/LogEvent'
type: string
required:
- description
- duration

View File

@ -26,6 +26,12 @@ class TestProxyKubernetes(TestCase):
outpost_connection_discovery()
self.controller = None
def tearDown(self) -> None:
if self.controller:
for log in self.controller.down_with_logs():
LOGGER.info(log)
return super().tearDown()
@pytest.mark.timeout(120)
def test_kubernetes_controller_static(self):
"""Test Kubernetes Controller"""

View File

@ -6,7 +6,7 @@
"": {
"name": "@goauthentik/web-tests",
"dependencies": {
"chromedriver": "^123.0.1"
"chromedriver": "^123.0.0"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
@ -18,7 +18,7 @@
"@wdio/spec-reporter": "^8.32.4",
"eslint": "^8.57.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.25.0",
"eslint-plugin-sonarjs": "^0.24.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
@ -2084,9 +2084,9 @@
}
},
"node_modules/chromedriver": {
"version": "123.0.1",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.1.tgz",
"integrity": "sha512-YQUIP/zdlzDIRCZNCv6rEVDSY4RAxo/tDL0OiGPPuai+z8unRNqJr/9V6XTBypVFyDheXNalKt9QxEqdMPuLAQ==",
"version": "123.0.0",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.0.tgz",
"integrity": "sha512-OE9mpxXwbFy5LncAisqXm1aEzuLPtEMORIxyYIn9uT7N8rquJWyoip6w9Rytub3o2gnynW9+PFOTPVTldaYrtw==",
"hasInstallScript": true,
"dependencies": {
"@testim/chrome-version": "^1.1.4",
@ -3114,9 +3114,9 @@
}
},
"node_modules/eslint-plugin-sonarjs": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.25.0.tgz",
"integrity": "sha512-DaZOtpUucEZbvowgKxVFwICV6r0h7jSCAx0IHICvCowP+etFussnhtaiCPSnYAuwVJ+P/6UFUhkv7QJklpXFyA==",
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.24.0.tgz",
"integrity": "sha512-87zp50mbbNrSTuoEOebdRQBPa0mdejA5UEjyuScyIw8hEpEjfWP89Qhkq5xVZfVyVSRQKZc9alVm7yRKQvvUmg==",
"dev": true,
"engines": {
"node": ">=16"

View File

@ -12,7 +12,7 @@
"@wdio/spec-reporter": "^8.32.4",
"eslint": "^8.57.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.25.0",
"eslint-plugin-sonarjs": "^0.24.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
@ -32,6 +32,6 @@
"node": ">=20"
},
"dependencies": {
"chromedriver": "^123.0.1"
"chromedriver": "^123.0.0"
}
}

View File

@ -1,17 +0,0 @@
### 2024-03-26T09:25:06-0700
Split the tsconfig file into a base and build variant.
Lesson: This lesson is stored here and not in a comment in tsconfig.json because
JSON doesn't like comments. Doug Crockford's purity requirement has doomed an
entire generation to keeping its human-facing meta somewhere other than in the
file where it belongs.
Lesson: The `extend` command of tsconfig has an unexpected behavior. It is
neither a merge or a replace, but some mixture of the two. The buildfile's
`compilerOptions` is not a full replacement; instead, each of _its_ top-level
fields is a replacement for what is found in the basefile. So while you don't
need to include _everything_ in a `compilerOptions` field if you want to change
one thing, if you want to modify _one_ path in `compilerOptions.path`, you must
include the entire `compilerOptions.path` collection in your buildfile.
g

193
web/package-lock.json generated
View File

@ -11,13 +11,13 @@
"dependencies": {
"@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.5",
"@codemirror/lang-python": "^6.1.4",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.5",
"@fortawesome/fontawesome-free": "^6.5.1",
"@goauthentik/api": "^2024.2.2-1711643691",
"@goauthentik/api": "^2024.2.2-1711369360",
"@lit-labs/task": "^3.1.0",
"@lit/context": "^1.1.0",
"@lit/localize": "^0.12.1",
@ -25,7 +25,7 @@
"@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^2.4.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.109.0",
"@sentry/browser": "^7.108.0",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.4.2",
@ -59,7 +59,7 @@
"@jeysal/storybook-addon-css-user-preferences": "^0.2.0",
"@lit/localize-tools": "^0.7.2",
"@rollup/plugin-replace": "^5.0.5",
"@spotlightjs/spotlight": "^1.2.16",
"@spotlightjs/spotlight": "^1.2.15",
"@storybook/addon-essentials": "^7.6.17",
"@storybook/addon-links": "^7.6.17",
"@storybook/api": "^7.6.17",
@ -84,10 +84,10 @@
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-lit": "^1.11.0",
"eslint-plugin-sonarjs": "^0.25.0",
"eslint-plugin-sonarjs": "^0.24.0",
"eslint-plugin-storybook": "^0.8.0",
"github-slugger": "^2.0.0",
"glob": "^10.3.12",
"glob": "^10.3.10",
"lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
@ -111,9 +111,9 @@
"@esbuild/darwin-arm64": "^0.20.1",
"@esbuild/linux-amd64": "^0.18.11",
"@esbuild/linux-arm64": "^0.20.1",
"@rollup/rollup-darwin-arm64": "4.13.2",
"@rollup/rollup-linux-arm64-gnu": "4.13.2",
"@rollup/rollup-linux-x64-gnu": "4.13.2"
"@rollup/rollup-darwin-arm64": "4.13.0",
"@rollup/rollup-linux-arm64-gnu": "4.13.0",
"@rollup/rollup-linux-x64-gnu": "4.13.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -2164,9 +2164,8 @@
}
},
"node_modules/@codemirror/lang-python": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.5.tgz",
"integrity": "sha512-hCm+8X6wrnXJCGf+QhmFu1AXkdTVG7dHy0Ly6SI1N3SRPptaMvwX6oNQonOXOMPvmcjiB0xq342KAxX3BYpijw==",
"version": "6.1.4",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.3.2",
"@codemirror/language": "^6.8.0",
@ -2821,9 +2820,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2024.2.2-1711643691",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.2.2-1711643691.tgz",
"integrity": "sha512-QHe+3gaNRkId54AuqndYNL0e5kG8nPlH4OjOYOPqOr3u70rxby63PBSPgSRKgqsigBpZufhQGsUBAPmpR8Hv0w=="
"version": "2024.2.2-1711369360",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.2.2-1711369360.tgz",
"integrity": "sha512-8/J6cfxzpaUyz+piZUXrxPZuAlJ9SxwNrH+Z8xSRLAVavmEjmRM+Oy2XJEIZLDbcBKhNEuE99xdOxq6il/FJVw=="
},
"node_modules/@hcaptcha/types": {
"version": "1.0.3",
@ -4260,9 +4259,9 @@
"peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.13.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.2.tgz",
"integrity": "sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz",
"integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==",
"cpu": [
"arm64"
],
@ -4300,9 +4299,9 @@
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.13.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.2.tgz",
"integrity": "sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz",
"integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==",
"cpu": [
"arm64"
],
@ -4340,9 +4339,9 @@
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.13.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.2.tgz",
"integrity": "sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz",
"integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==",
"cpu": [
"x64"
],
@ -4408,102 +4407,102 @@
"peer": true
},
"node_modules/@sentry-internal/feedback": {
"version": "7.109.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.109.0.tgz",
"integrity": "sha512-EL7N++poxvJP9rYvh6vSu24tsKkOveNCcCj4IM7+irWPjsuD2GLYYlhp/A/Mtt9l7iqO4plvtiQU5HGk7smcTQ==",
"version": "7.108.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.108.0.tgz",
"integrity": "sha512-8JcgZEnk1uWrXJhsd3iRvFtEiVeaWOEhN0NZwhwQXHfvODqep6JtrkY1yCIyxbpA37aZmrPc2JhyotRERGfUjg==",
"dependencies": {
"@sentry/core": "7.109.0",
"@sentry/types": "7.109.0",
"@sentry/utils": "7.109.0"
"@sentry/core": "7.108.0",
"@sentry/types": "7.108.0",
"@sentry/utils": "7.108.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "7.109.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.109.0.tgz",
"integrity": "sha512-Lh/K60kmloR6lkPUcQP0iamw7B/MdEUEx/ImAx4tUSMrLj+IoUEcq/ECgnnVyQkJq59+8nPEKrVLt7x6PUPEjw==",
"version": "7.108.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.108.0.tgz",
"integrity": "sha512-R5tvjGqWUV5vSk0N1eBgVW7wIADinrkfDEBZ9FyKP2mXHBobsyNGt30heJDEqYmVqluRqjU2NuIRapsnnrpGnA==",
"dependencies": {
"@sentry/core": "7.109.0",
"@sentry/replay": "7.109.0",
"@sentry/types": "7.109.0",
"@sentry/utils": "7.109.0"
"@sentry/core": "7.108.0",
"@sentry/replay": "7.108.0",
"@sentry/types": "7.108.0",
"@sentry/utils": "7.108.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry-internal/tracing": {
"version": "7.109.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.109.0.tgz",
"integrity": "sha512-PzK/joC5tCuh2R/PRh+7dp+uuZl7pTsBIjPhVZHMTtb9+ls65WkdZJ1/uKXPouyz8NOo9Xok7aEvEo9seongyw==",
"version": "7.108.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.108.0.tgz",
"integrity": "sha512-zuK5XsTsb+U+hgn3SPetYDAogrXsM16U/LLoMW7+TlC6UjlHGYQvmX3o+M2vntejoU1QZS8m1bCAZSMWEypAEw==",
"dependencies": {
"@sentry/core": "7.109.0",
"@sentry/types": "7.109.0",
"@sentry/utils": "7.109.0"
"@sentry/core": "7.108.0",
"@sentry/types": "7.108.0",
"@sentry/utils": "7.108.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser": {
"version": "7.109.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.109.0.tgz",
"integrity": "sha512-yx+OFG+Ab9qUDDgV9ZDv8M9O9Mqr0fjKta/LMlWALYLjzkMvxsPlRPFj7oMBlHqOTVLDeg7lFYmsA8wyWQ8Z8g==",
"version": "7.108.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.108.0.tgz",
"integrity": "sha512-FNpzsdTvGvdHJMUelqEouUXMZU7jC+dpN7CdT6IoHVVFEkoAgrjMVUhXZoQ/dmCkdKWHmFSQhJ8Fm6V+e9Aq0A==",
"dependencies": {
"@sentry-internal/feedback": "7.109.0",
"@sentry-internal/replay-canvas": "7.109.0",
"@sentry-internal/tracing": "7.109.0",
"@sentry/core": "7.109.0",
"@sentry/replay": "7.109.0",
"@sentry/types": "7.109.0",
"@sentry/utils": "7.109.0"
"@sentry-internal/feedback": "7.108.0",
"@sentry-internal/replay-canvas": "7.108.0",
"@sentry-internal/tracing": "7.108.0",
"@sentry/core": "7.108.0",
"@sentry/replay": "7.108.0",
"@sentry/types": "7.108.0",
"@sentry/utils": "7.108.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/core": {
"version": "7.109.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.109.0.tgz",
"integrity": "sha512-xwD4U0IlvvlE/x/g/W1I8b4Cfb16SsCMmiEuBf6XxvAa3OfWBxKoqLifb3GyrbxMC4LbIIZCN/SvLlnGJPgszA==",
"version": "7.108.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.108.0.tgz",
"integrity": "sha512-I/VNZCFgLASxHZaD0EtxZRM34WG9w2gozqgrKGNMzAymwmQ3K9g/1qmBy4e6iS3YRptb7J5UhQkZQHrcwBbjWQ==",
"dependencies": {
"@sentry/types": "7.109.0",
"@sentry/utils": "7.109.0"
"@sentry/types": "7.108.0",
"@sentry/utils": "7.108.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay": {
"version": "7.109.0",
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.109.0.tgz",
"integrity": "sha512-hCDjbTNO7ErW/XsaBXlyHFsUhneyBUdTec1Swf98TFEfVqNsTs6q338aUcaR8dGRLbLrJ9YU9D1qKq++v5h2CA==",
"version": "7.108.0",
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.108.0.tgz",
"integrity": "sha512-jo8fDOzcZJclP1+4n9jUtVxTlBFT9hXwxhAMrhrt70FV/nfmCtYQMD3bzIj79nwbhUtFP6pN39JH1o7Xqt1hxQ==",
"dependencies": {
"@sentry-internal/tracing": "7.109.0",
"@sentry/core": "7.109.0",
"@sentry/types": "7.109.0",
"@sentry/utils": "7.109.0"
"@sentry-internal/tracing": "7.108.0",
"@sentry/core": "7.108.0",
"@sentry/types": "7.108.0",
"@sentry/utils": "7.108.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry/types": {
"version": "7.109.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.109.0.tgz",
"integrity": "sha512-egCBnDv3YpVFoNzRLdP0soVrxVLCQ+rovREKJ1sw3rA2/MFH9WJ+DZZexsX89yeAFzy1IFsCp7/dEqudusml6g==",
"version": "7.108.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.108.0.tgz",
"integrity": "sha512-bKtHITmBN3kqtqE5eVvL8mY8znM05vEodENwRpcm6TSrrBjC2RnwNWVwGstYDdHpNfFuKwC8mLY9bgMJcENo8g==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils": {
"version": "7.109.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.109.0.tgz",
"integrity": "sha512-3RjxMOLMBwZ5VSiH84+o/3NY2An4Zldjz0EbfEQNRY9yffRiCPJSQiCJID8EoylCFOh/PAhPimBhqbtWJxX6iw==",
"version": "7.108.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.108.0.tgz",
"integrity": "sha512-a45yEFD5qtgZaIFRAcFkG8C8lnDzn6t4LfLXuV4OafGAy/3ZAN3XN8wDnrruHkiUezSSANGsLg3bXaLW/JLvJw==",
"dependencies": {
"@sentry/types": "7.109.0"
"@sentry/types": "7.108.0"
},
"engines": {
"node": ">=8"
@ -4515,9 +4514,9 @@
"license": "MIT"
},
"node_modules/@spotlightjs/overlay": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/@spotlightjs/overlay/-/overlay-1.8.2.tgz",
"integrity": "sha512-g3pzaJFKK67pBIl72qSNoFJIfP/dmdFoSPWZZQW6MKAdU7IOY5yf3BB52xEc6iSfeLGG/KpYNThefpobX3hb7Q==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@spotlightjs/overlay/-/overlay-1.8.1.tgz",
"integrity": "sha512-t8S2b6AxgDfDoPls3CU7uABLdKx3g8cCXQWEHOICC1i7MYUSQLFMDpWzFWTEjN0XA8MGwNf/QKNlZ/HhaKTzJw==",
"dev": true
},
"node_modules/@spotlightjs/sidecar": {
@ -4529,12 +4528,12 @@
}
},
"node_modules/@spotlightjs/spotlight": {
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@spotlightjs/spotlight/-/spotlight-1.2.16.tgz",
"integrity": "sha512-grqK7Qwzz0zJKaM4+u/0DS81gEGKkUsKwXGY1kA07rXsbp6ilT62JWI1tQDgYHb1i3MbR2ch0EuMT476CAtA+A==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@spotlightjs/spotlight/-/spotlight-1.2.15.tgz",
"integrity": "sha512-M0VTAyameAsK9kjI9k31CehTLJMqUdOvv7DSOr27dcioytBV0uC0l8w7ngHWxdqCOTpbruEs8EIrbQ0T9b4YZQ==",
"dev": true,
"dependencies": {
"@spotlightjs/overlay": "1.8.2",
"@spotlightjs/overlay": "1.8.1",
"@spotlightjs/sidecar": "1.4.0"
},
"bin": {
@ -10090,10 +10089,9 @@
"license": "MIT"
},
"node_modules/eslint-plugin-sonarjs": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.25.0.tgz",
"integrity": "sha512-DaZOtpUucEZbvowgKxVFwICV6r0h7jSCAx0IHICvCowP+etFussnhtaiCPSnYAuwVJ+P/6UFUhkv7QJklpXFyA==",
"version": "0.24.0",
"dev": true,
"license": "LGPL-3.0-only",
"engines": {
"node": ">=16"
},
@ -11236,16 +11234,16 @@
"license": "ISC"
},
"node_modules/glob": {
"version": "10.3.12",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
"integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.6",
"jackspeak": "^2.3.5",
"minimatch": "^9.0.1",
"minipass": "^7.0.4",
"path-scurry": "^1.10.2"
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
"path-scurry": "^1.10.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
@ -11273,15 +11271,6 @@
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/glob/node_modules/minipass": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/globals": {
"version": "11.12.0",
"dev": true,
@ -14580,12 +14569,11 @@
"license": "MIT"
},
"node_modules/path-scurry": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
"integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
"version": "1.10.1",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"lru-cache": "^9.1.1 || ^10.0.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
@ -14597,9 +14585,8 @@
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
"integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
"dev": true,
"license": "ISC",
"engines": {
"node": "14 || >=16.14"
}

View File

@ -14,8 +14,8 @@
"build": "run-s build-locales esbuild:build",
"build-proxy": "run-s build-locales esbuild:build-proxy",
"watch": "run-s build-locales esbuild:watch",
"lint": "cross-env NODE_OPTIONS='--max_old_space_size=8192' eslint . --max-warnings 0 --fix",
"lint:precommit": "cross-env NODE_OPTIONS='--max_old_space_size=8192' node scripts/eslint-precommit.mjs",
"lint": "eslint . --max-warnings 0 --fix",
"lint:precommit": "node scripts/eslint-precommit.mjs",
"lint:spelling": "node scripts/check-spelling.mjs",
"lit-analyse": "lit-analyzer src",
"precommit": "npm-run-all --parallel tsc lit-analyse lint:spelling --sequential lint:precommit prettier",
@ -32,13 +32,13 @@
"dependencies": {
"@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.5",
"@codemirror/lang-python": "^6.1.4",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.5",
"@fortawesome/fontawesome-free": "^6.5.1",
"@goauthentik/api": "^2024.2.2-1711643691",
"@goauthentik/api": "^2024.2.2-1711369360",
"@lit-labs/task": "^3.1.0",
"@lit/context": "^1.1.0",
"@lit/localize": "^0.12.1",
@ -46,7 +46,7 @@
"@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^2.4.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.109.0",
"@sentry/browser": "^7.108.0",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.4.2",
@ -80,7 +80,7 @@
"@jeysal/storybook-addon-css-user-preferences": "^0.2.0",
"@lit/localize-tools": "^0.7.2",
"@rollup/plugin-replace": "^5.0.5",
"@spotlightjs/spotlight": "^1.2.16",
"@spotlightjs/spotlight": "^1.2.15",
"@storybook/addon-essentials": "^7.6.17",
"@storybook/addon-links": "^7.6.17",
"@storybook/api": "^7.6.17",
@ -105,10 +105,10 @@
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-lit": "^1.11.0",
"eslint-plugin-sonarjs": "^0.25.0",
"eslint-plugin-sonarjs": "^0.24.0",
"eslint-plugin-storybook": "^0.8.0",
"github-slugger": "^2.0.0",
"glob": "^10.3.12",
"glob": "^10.3.10",
"lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
@ -129,9 +129,9 @@
"@esbuild/darwin-arm64": "^0.20.1",
"@esbuild/linux-amd64": "^0.18.11",
"@esbuild/linux-arm64": "^0.20.1",
"@rollup/rollup-darwin-arm64": "4.13.2",
"@rollup/rollup-linux-arm64-gnu": "4.13.2",
"@rollup/rollup-linux-x64-gnu": "4.13.2"
"@rollup/rollup-darwin-arm64": "4.13.0",
"@rollup/rollup-linux-arm64-gnu": "4.13.0",
"@rollup/rollup-linux-x64-gnu": "4.13.0"
},
"engines": {
"node": ">=20"

View File

@ -1,6 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/elements/events/LogViewer";
import { Form } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -84,7 +83,28 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
<div class="pf-c-form__group-label">
<div class="c-form__horizontal-group">
<dl class="pf-c-description-list pf-m-horizontal">
<ak-log-viewer .logs=${this.result?.logMessages}></ak-log-viewer>
${(this.result?.logMessages || []).length > 0
? this.result?.logMessages?.map((m) => {
return html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${m.log_level}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${m.event}
</div>
</dd>
</div>`;
})
: html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("No log messages.")}</span
>
</dt>
</div>`}
</dl>
</div>
</div>

View File

@ -15,7 +15,13 @@ import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { Brand, CoreApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api";
import {
Application,
Brand,
CoreApi,
CoreApplicationsListRequest,
FlowsInstancesListDesignationEnum,
} from "@goauthentik/api";
@customElement("ak-brand-form")
export class BrandForm extends ModelForm<Brand, string> {
@ -137,6 +143,46 @@ export class BrandForm extends ModelForm<Brand, string> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("External user settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Default application")}
name="defaultApplication"
>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Application[]> => {
const args: CoreApplicationsListRequest = {
ordering: "name",
superuserFullList: true,
};
if (query !== undefined) {
args.search = query;
}
const users = await new CoreApi(
DEFAULT_CONFIG,
).coreApplicationsList(args);
return users.results;
}}
.renderElement=${(item: Application): string => {
return item.name;
}}
.renderDescription=${(item: Application): TemplateResult => {
return html`${item.slug}`;
}}
.value=${(item: Application | undefined): string | undefined => {
return item?.pk;
}}
.selected=${(item: Application): boolean => {
return item.pk === this.instance?.defaultApplication;
}}
>
</ak-search-select>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Default flows")} </span>
<div slot="body" class="pf-c-form">

View File

@ -13,13 +13,14 @@ import { TablePage } from "@goauthentik/elements/table/TablePage";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { TemplateResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Brand, CoreApi, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api";
import { WithBrandConfig } from "@goauthentik/authentik/elements/Interface/brandProvider";
@customElement("ak-brand-list")
export class BrandListPage extends TablePage<Brand> {
export class BrandListPage extends WithBrandConfig(TablePage<Brand>) {
searchEnabled(): boolean {
return true;
}
@ -84,7 +85,9 @@ export class BrandListPage extends TablePage<Brand> {
row(item: Brand): TemplateResult[] {
return [
html`${item.domain}`,
html`${item.domain}${this.brand.matchedDomain === item.domain ? html`
&nbsp;<ak-status-label ?good=${false} type="info" bad-label=${msg("Active")}></ak-status-label>
` : nothing}`,
html`${item.brandingTitle}`,
html`<ak-status-label ?good=${item._default}></ak-status-label>`,
html`<ak-forms-modal>

View File

@ -23,15 +23,13 @@ export class BoundStagesList extends Table<FlowStageBinding> {
checkbox = true;
clearOnRefresh = true;
order = "order";
@property()
target?: string;
async apiEndpoint(page: number): Promise<PaginatedResponse<FlowStageBinding>> {
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsList({
target: this.target || "",
ordering: this.order,
ordering: "order",
page: page,
pageSize: (await uiConfig()).pagination.perPage,
});
@ -39,8 +37,8 @@ export class BoundStagesList extends Table<FlowStageBinding> {
columns(): TableColumn[] {
return [
new TableColumn(msg("Order"), "order"),
new TableColumn(msg("Name"), "stage__name"),
new TableColumn(msg("Order")),
new TableColumn(msg("Name")),
new TableColumn(msg("Type")),
new TableColumn(msg("Actions")),
];

View File

@ -1,7 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { SentryIgnoredError } from "@goauthentik/common/errors";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/elements/events/LogViewer";
import { Form } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -56,7 +55,28 @@ export class FlowImportForm extends Form<Flow> {
<div class="pf-c-form__group-label">
<div class="c-form__horizontal-group">
<dl class="pf-c-description-list pf-m-horizontal">
<ak-log-viewer .logs=${this.result?.logs}></ak-log-viewer>
${(this.result?.logs || []).length > 0
? this.result?.logs?.map((m) => {
return html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${m.log_level}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${m.event}
</div>
</dd>
</div>`;
})
: html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("No log messages.")}</span
>
</dt>
</div>`}
</dl>
</div>
</div>

View File

@ -31,12 +31,10 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
checkbox = true;
clearOnRefresh = true;
order = "order";
async apiEndpoint(page: number): Promise<PaginatedResponse<PolicyBinding>> {
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({
target: this.target || "",
ordering: this.order,
ordering: "order",
page: page,
pageSize: (await uiConfig()).pagination.perPage,
});

View File

@ -3,7 +3,6 @@ import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/events/LogViewer";
import { Form } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -86,7 +85,28 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
<div class="pf-c-form__group-label">
<div class="c-form__horizontal-group">
<dl class="pf-c-description-list pf-m-horizontal">
<ak-log-viewer .logs=${this.result?.logMessages}></ak-log-viewer>
${(this.result?.logMessages || []).length > 0
? this.result?.logMessages?.map((m) => {
return html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${m.log_level}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${m.event}
</div>
</dd>
</div>`;
})
: html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("No log messages.")}</span
>
</dt>
</div>`}
</dl>
</div>
</div>

View File

@ -29,6 +29,8 @@ class PreviewStageHost implements StageHost {
flowSlug = undefined;
loading = false;
brand = undefined;
frameMode = false;
async submit(payload: unknown): Promise<boolean> {
this.promptForm.previewResult = payload;
return false;

View File

@ -5,7 +5,6 @@ import { getRelativeTime } from "@goauthentik/common/utils";
import { PFColor } from "@goauthentik/elements/Label";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/events/LogViewer";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -96,7 +95,9 @@ export class SystemTaskListPage extends TablePage<SystemTask> {
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-log-viewer .logs=${item?.messages}></ak-log-viewer>
${item.messages.map((m) => {
return html`<li>${m}</li>`;
})}
</div>
</dd>
</div>

View File

@ -3,7 +3,7 @@ import {
EventMiddleware,
LoggingMiddleware,
} from "@goauthentik/common/api/middleware";
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
import { EVENT_LOCALE_REQUEST, EVENT_REFRESH, VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
@ -61,16 +61,18 @@ export function brand(): Promise<CurrentBrand> {
return globalBrandPromise;
}
export function getMetaContent(key: string): string {
export function getMetaContent(key: string): string | undefined {
const metaEl = document.querySelector<HTMLMetaElement>(`meta[name=${key}]`);
if (!metaEl) return "";
return metaEl.content;
return metaEl?.content;
}
export const DEFAULT_CONFIG = new Configuration({
basePath: (process.env.AK_API_BASE_PATH || window.location.origin) + "/api/v3",
basePath:
(process.env.AK_API_BASE_PATH ||
getMetaContent("authentik-api") ||
window.location.origin) + "/api/v3",
headers: {
"sentry-trace": getMetaContent("sentry-trace"),
"sentry-trace": getMetaContent("sentry-trace") || "",
},
middleware: [
new CSRFMiddleware(),
@ -86,4 +88,13 @@ export function AndNext(url: string): string {
return `?next=${encodeURIComponent(url)}`;
}
window.addEventListener(EVENT_REFRESH, () => {
// Upon global refresh, disregard whatever was pre-hydrated and
// actually load info from API
globalConfigPromise = undefined;
globalBrandPromise = undefined;
config();
brand();
});
console.debug(`authentik(early): version ${VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`);

View File

@ -16,7 +16,7 @@ export async function parseAPIError(error: Error): Promise<APIErrorTypes> {
if (!(error instanceof ResponseError)) {
return error;
}
if (error.response.status < 400 || error.response.status > 499) {
if (error.response.status < 400 && error.response.status > 499) {
return error;
}
const body = await error.response.json();

View File

@ -55,7 +55,9 @@ export class PlexAPIClient {
): Promise<{ authUrl: string; pin: PlexPinResponse }> {
const headers = {
...DEFAULT_HEADERS,
"X-Plex-Client-Identifier": clientIdentifier,
...{
"X-Plex-Client-Identifier": clientIdentifier,
},
};
const pinResponse = await fetch("https://plex.tv/api/v2/pins.json?strong=true", {
method: "POST",
@ -73,7 +75,9 @@ export class PlexAPIClient {
static async pinStatus(clientIdentifier: string, id: number): Promise<string | undefined> {
const headers = {
...DEFAULT_HEADERS,
"X-Plex-Client-Identifier": clientIdentifier,
...{
"X-Plex-Client-Identifier": clientIdentifier,
},
};
const pinResponse = await fetch(`https://plex.tv/api/v2/pins/${id}`, {
headers: headers,

View File

@ -123,7 +123,7 @@ const isCSSResult = (v: unknown): v is CSSResult =>
// prettier-ignore
export const _adaptCSS = (sheet: AdaptableStylesheet): CSSStyleSheet =>
(typeof sheet === "string" ? css([sheet] as unknown as TemplateStringsArray, []).styleSheet
(typeof sheet === "string" ? css([sheet] as unknown as TemplateStringsArray, ...[]).styleSheet
: isCSSResult(sheet) ? sheet.styleSheet
: sheet) as CSSStyleSheet;

View File

@ -1,11 +1,9 @@
import { createContext } from "@lit/context";
import type { Config, CurrentBrand, LicenseSummary, SessionUser } from "@goauthentik/api";
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
export const authentikConfigContext = createContext<Config>(Symbol("authentik-config-context"));
export const authentikUserContext = createContext<SessionUser>(Symbol("authentik-user-context"));
export const authentikEnterpriseContext = createContext<LicenseSummary>(
Symbol("authentik-enterprise-context"),
);

View File

@ -1,52 +0,0 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import { ContextProvider } from "@lit/context";
import { ReactiveController, ReactiveControllerHost } from "lit";
import type { CurrentBrand } from "@goauthentik/api";
import { CoreApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
export class BrandContextController implements ReactiveController {
host!: ReactiveElementHost;
context!: ContextProvider<{ __context__: CurrentBrand | undefined }>;
constructor(host: ReactiveElementHost) {
this.host = host;
this.context = new ContextProvider(this.host, {
context: authentikBrandContext,
initialValue: undefined,
});
this.fetch = this.fetch.bind(this);
this.fetch();
}
fetch() {
new CoreApi(DEFAULT_CONFIG).coreBrandsCurrentRetrieve().then((brand) => {
this.context.setValue(brand);
this.host.brand = brand;
});
}
hostConnected() {
window.addEventListener(EVENT_REFRESH, this.fetch);
}
hostDisconnected() {
window.removeEventListener(EVENT_REFRESH, this.fetch);
}
hostUpdate() {
// If the Interface changes its brand information for some reason,
// we should notify all users of the context of that change. doesn't
if (this.host.brand !== this.context.value) {
this.context.setValue(this.host.brand);
}
}
}

View File

@ -1,53 +0,0 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import { ContextProvider } from "@lit/context";
import { ReactiveController, ReactiveControllerHost } from "lit";
import type { Config } from "@goauthentik/api";
import { RootApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
export class ConfigContextController implements ReactiveController {
host!: ReactiveElementHost;
context!: ContextProvider<{ __context__: Config | undefined }>;
constructor(host: ReactiveElementHost) {
this.host = host;
this.context = new ContextProvider(this.host, {
context: authentikConfigContext,
initialValue: undefined,
});
this.fetch = this.fetch.bind(this);
this.fetch();
}
fetch() {
new RootApi(DEFAULT_CONFIG).rootConfigRetrieve().then((config) => {
this.context.setValue(config);
this.host.config = config;
});
}
hostConnected() {
window.addEventListener(EVENT_REFRESH, this.fetch);
}
hostDisconnected() {
window.removeEventListener(EVENT_REFRESH, this.fetch);
}
hostUpdate() {
// If the Interface changes its config information, we should notify all
// users of the context of that change, without creating an infinite
// loop of resets.
if (this.host.config !== this.context.value) {
this.context.setValue(this.host.config);
}
}
}

View File

@ -1,53 +0,0 @@
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
import { ContextProvider } from "@lit/context";
import { ReactiveController, ReactiveControllerHost } from "lit";
import type { LicenseSummary } from "@goauthentik/api";
import { EnterpriseApi } from "@goauthentik/api";
import type { AkEnterpriseInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkEnterpriseInterface;
export class EnterpriseContextController implements ReactiveController {
host!: ReactiveElementHost;
context!: ContextProvider<{ __context__: LicenseSummary | undefined }>;
constructor(host: ReactiveElementHost) {
this.host = host;
this.context = new ContextProvider(this.host, {
context: authentikEnterpriseContext,
initialValue: undefined,
});
this.fetch = this.fetch.bind(this);
this.fetch();
}
fetch() {
new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((enterprise) => {
this.context.setValue(enterprise);
this.host.licenseSummary = enterprise;
});
}
hostConnected() {
window.addEventListener(EVENT_REFRESH_ENTERPRISE, this.fetch);
}
hostDisconnected() {
window.removeEventListener(EVENT_REFRESH_ENTERPRISE, this.fetch);
}
hostUpdate() {
// If the Interface changes its config information, we should notify all
// users of the context of that change, without creating an infinite
// loop of resets.
if (this.host.licenseSummary !== this.context.value) {
this.context.setValue(this.host.licenseSummary);
}
}
}

View File

@ -1,47 +1,77 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { brand, config } from "@goauthentik/common/api/config";
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import {
authentikBrandContext,
authentikConfigContext,
authentikEnterpriseContext,
} from "@goauthentik/elements/AuthentikContexts";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
import { ContextProvider } from "@lit/context";
import { state } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
import { UiThemeEnum } from "@goauthentik/api";
import { EnterpriseApi, UiThemeEnum } from "@goauthentik/api";
import { AKElement } from "../Base";
import { BrandContextController } from "./BrandContextController";
import { ConfigContextController } from "./ConfigContextController";
import { EnterpriseContextController } from "./EnterpriseContextController";
export type AkInterface = HTMLElement & {
type AkInterface = HTMLElement & {
getTheme: () => Promise<UiThemeEnum>;
brand?: CurrentBrand;
uiConfig?: UIConfig;
config?: Config;
};
const brandContext = Symbol("brandContext");
const configContext = Symbol("configContext");
export class Interface extends AKElement implements AkInterface {
@state()
uiConfig?: UIConfig;
[brandContext]!: BrandContextController;
_configContext = new ContextProvider(this, {
context: authentikConfigContext,
initialValue: undefined,
});
[configContext]!: ConfigContextController;
_config?: Config;
@state()
config?: Config;
set config(c: Config) {
this._config = c;
this._configContext.setValue(c);
this.requestUpdate();
}
get config(): Config | undefined {
return this._config;
}
_brandContext = new ContextProvider(this, {
context: authentikBrandContext,
initialValue: undefined,
});
_brand?: CurrentBrand;
@state()
brand?: CurrentBrand;
set brand(c: CurrentBrand) {
this._brand = c;
this._brandContext.setValue(c);
this.requestUpdate();
}
get brand(): CurrentBrand | undefined {
return this._brand;
}
constructor() {
super();
document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)];
this[brandContext] = new BrandContextController(this);
this[configContext] = new ConfigContextController(this);
brand().then((brand) => (this.brand = brand));
config().then((config) => (this.config = config));
this.dataset.akInterfaceRoot = "true";
}
@ -58,20 +88,37 @@ export class Interface extends AKElement implements AkInterface {
}
}
export type AkEnterpriseInterface = AkInterface & {
licenseSummary?: LicenseSummary;
};
const enterpriseContext = Symbol("enterpriseContext");
export class EnterpriseAwareInterface extends Interface {
[enterpriseContext]!: EnterpriseContextController;
_licenseSummaryContext = new ContextProvider(this, {
context: authentikEnterpriseContext,
initialValue: undefined,
});
_licenseSummary?: LicenseSummary;
@state()
licenseSummary?: LicenseSummary;
set licenseSummary(c: LicenseSummary) {
this._licenseSummary = c;
this._licenseSummaryContext.setValue(c);
this.requestUpdate();
}
get licenseSummary(): LicenseSummary | undefined {
return this._licenseSummary;
}
constructor() {
super();
this[enterpriseContext] = new EnterpriseContextController(this);
const refreshStatus = () => {
new EnterpriseApi(DEFAULT_CONFIG)
.enterpriseLicenseSummaryRetrieve()
.then((enterprise) => {
this.licenseSummary = enterprise;
});
};
refreshStatus();
window.addEventListener(EVENT_REFRESH_ENTERPRISE, () => {
refreshStatus();
});
}
}

View File

@ -56,7 +56,7 @@ export class Markdown extends AKElement {
converter = new showdown.Converter({ metadata: true, tables: true });
replaceAdmonitions(input: string): string {
const admonitionStart = /:::(\w+)(<br\s*\/>|\s*$)/gm;
const admonitionStart = /:::(\w+)<br\s\/>/gm;
const admonitionEnd = /:::/gm;
return (
input

View File

@ -12,7 +12,7 @@ import AKTokenCopyButton from "./ak-token-copy-button";
function makeid(length: number) {
const sample = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from({ length })
return new Array(length)
.fill(" ")
.map(() => sample.charAt(Math.floor(Math.random() * sample.length)))
.join("");

View File

@ -1,114 +0,0 @@
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/elements/EmptyState";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { LogEvent, LogLevelEnum } from "@goauthentik/api";
@customElement("ak-log-viewer")
export class LogViewer extends Table<LogEvent> {
@property({ attribute: false })
logs?: LogEvent[] = [];
expandable = true;
paginated = false;
static get styles(): CSSResult[] {
return super.styles.concat(PFDescriptionList);
}
async apiEndpoint(_page: number): Promise<PaginatedResponse<LogEvent>> {
return {
pagination: {
next: 0,
previous: 0,
count: 1,
current: 1,
totalPages: 1,
startIndex: 1,
endIndex: 1,
},
results: this.logs || [],
};
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state header=${msg("No log messages.")}> </ak-empty-state>`,
);
}
renderExpanded(item: LogEvent): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Timestamp")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${item.timestamp.toLocaleString()}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Attributes")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</dd>
</div>
</dl>
</div>
</td>`;
}
renderToolbarContainer(): TemplateResult {
return html``;
}
columns(): TableColumn[] {
return [
new TableColumn(msg("Time")),
new TableColumn(msg("Level")),
new TableColumn(msg("Event")),
new TableColumn(msg("Logger")),
];
}
statusForItem(item: LogEvent): string {
switch (item.logLevel) {
case LogLevelEnum.Critical:
case LogLevelEnum.Error:
case LogLevelEnum.Exception:
return "error";
case LogLevelEnum.Warn:
case LogLevelEnum.Warning:
return "warning";
default:
return "info";
}
}
row(item: LogEvent): TemplateResult[] {
return [
html`${getRelativeTime(item.timestamp)}`,
html`<ak-status-label
type=${this.statusForItem(item)}
bad-label=${item.logLevel}
></ak-status-label>`,
html`${item.event}`,
html`${item.logger}`,
];
}
}

View File

@ -91,7 +91,7 @@ export class RouterOutlet extends AKElement {
let matchedRoute: RouteMatch | null = null;
this.routes.some((route) => {
const match = route.url.exec(activeUrl);
if (match !== null) {
if (match != null) {
matchedRoute = new RouteMatch(route);
matchedRoute.arguments = match.groups || {};
matchedRoute.fullUrl = activeUrl;

View File

@ -1,3 +1,4 @@
import { ensureCSSStyleSheet } from "@goauthentik/authentik/elements/utils/ensureCSSStyleSheet";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import {
EVENT_FLOW_ADVANCE,
@ -76,7 +77,8 @@ export class FlowExecutor extends Interface implements StageHost {
@state()
flowInfo?: ContextualFlowInfo;
ws: WebsocketClient;
@state()
frameMode = window !== window.top;
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFDrawer, PFButton, PFTitle, PFList, PFBackgroundImage].concat(css`
@ -140,6 +142,25 @@ export class FlowExecutor extends Interface implements StageHost {
:host([theme="dark"]) .pf-c-login.sidebar_right .pf-c-list {
color: var(--ak-dark-foreground);
}
/* frame design */
.pf-c-login.frame {
padding: 0;
min-height: initial !important;
}
.pf-c-login.frame .pf-c-login__main {
margin-bottom: 0;
height: 100%;
}
.pf-c-login.frame .ak-login-container {
height: 100vh;
width: 100vw;
}
.pf-c-login.frame .pf-c-login__footer {
display: none;
}
.pf-c-login.frame .pf-c-login__footer .pf-c-list {
padding: 0;
}
.pf-c-brand {
padding-top: calc(
var(--pf-c-login__main-footer-links--PaddingTop) +
@ -161,7 +182,21 @@ export class FlowExecutor extends Interface implements StageHost {
constructor() {
super();
this.ws = new WebsocketClient();
if (this.frameMode) {
// PatternFly sets html and body to 100% height, which we don't
// want in this case since the iframe should only take the space it needs
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
ensureCSSStyleSheet(css`
body,
html {
height: unset !important;
}
`),
];
} else {
new WebsocketClient();
}
if (window.location.search.includes("inspector")) {
this.inspectorOpen = true;
}
@ -437,9 +472,11 @@ export class FlowExecutor extends Interface implements StageHost {
}
renderChallengeWrapper(): TemplateResult {
const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
<img src="${first(this.brand?.brandingLogo, "")}" alt="authentik Logo" />
</div>`;
const logo = this.frameMode
? nothing
: html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
<img src="${first(this.brand?.brandingLogo, "")}" alt="authentik Logo" />
</div>`;
if (!this.challenge) {
return html`${logo}<ak-empty-state ?loading=${true} header=${msg("Loading")}>
</ak-empty-state>`;
@ -482,7 +519,26 @@ export class FlowExecutor extends Interface implements StageHost {
}
render(): TemplateResult {
return html` <ak-locale-context>
if (this.frameMode) {
return html`<ak-locale-context>
<div class="pf-c-login frame">
<div class="ak-login-container">
<div class="pf-c-login__main">${this.renderChallengeWrapper()}</div>
<footer class="pf-c-login__footer">
<ul class="pf-c-list pf-m-inline">
<li>
<a
href="https://goauthentik.io?utm_source=authentik&amp;utm_medium=flow"
>${msg("Powered by authentik")}</a
>
</li>
</ul>
</footer>
</div>
</div>
</ak-locale-context>`;
}
return html`<ak-locale-context>
<div class="pf-c-background-image"></div>
<div class="pf-c-page__drawer">
<div class="pf-c-drawer ${this.inspectorOpen ? "pf-m-expanded" : "pf-m-collapsed"}">

View File

@ -49,8 +49,27 @@ export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeRes
"authentik/stages/redirect: redirecting to url from server",
this.challenge.to,
);
window.location.assign(this.challenge.to);
this.startedRedirect = true;
if (this.host.frameMode && window.top) {
try {
window.top.location.assign(this.challenge.to);
} catch {
window.top.postMessage(
{
source: "goauthentik.io",
context: "flow-executor",
component: this.challenge.component,
to: this.challenge.to,
},
document.location.ancestorOrigins[0],
);
}
if (this.challenge.to.startsWith("http")) {
return;
}
} else {
window.location.assign(this.challenge.to);
}
}
renderLoading(): TemplateResult {

View File

@ -47,6 +47,10 @@ export class AuthenticatorValidateStage
return this.host.brand;
}
get frameMode(): boolean {
return this.host.frameMode;
}
@state()
_selectedDeviceChallenge?: DeviceChallenge;

View File

@ -16,6 +16,7 @@ export interface StageHost {
challenge?: unknown;
flowSlug?: string;
loading: boolean;
frameMode: boolean;
submit(payload: unknown, options?: SubmitOptions): Promise<boolean>;
readonly brand?: CurrentBrand;

View File

@ -1,4 +1,5 @@
import { docLink } from "@goauthentik/common/global";
import { adaptCSS } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
@ -19,23 +20,23 @@ import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
* administrator, provide a link to the "Create a new application" page.
*/
const styles = adaptCSS([
PFBase,
PFEmptyState,
PFButton,
PFContent,
PFSpacing,
css`
.cta {
display: inline-block;
font-weight: bold;
}
`,
]);
@customElement("ak-library-application-empty-list")
export class LibraryPageApplicationEmptyList extends AKElement {
static get styles() {
return [
PFBase,
PFEmptyState,
PFButton,
PFContent,
PFSpacing,
css`
.cta {
display: inline-block;
font-weight: bold;
}
`,
];
}
static styles = styles;
@property({ attribute: "isadmin", type: Boolean })
isAdmin = false;

View File

@ -31,22 +31,22 @@ const LAYOUTS = new Map<string, [string, string]>([
],
]);
const styles = [
PFBase,
PFEmptyState,
PFContent,
PFGrid,
css`
.app-group-header {
margin-bottom: 1em;
margin-top: 1.2em;
}
`,
];
@customElement("ak-library-application-list")
export class LibraryPageApplicationList extends AKElement {
static get styles() {
return [
PFBase,
PFEmptyState,
PFContent,
PFGrid,
css`
.app-group-header {
margin-bottom: 1em;
margin-top: 1.2em;
}
`,
];
}
static styles = styles;
@property({ attribute: true })
layout = "row" as LayoutType;

View File

@ -18,26 +18,24 @@ import { customEvent } from "./helpers";
@customElement("ak-library-list-search")
export class LibraryPageApplicationList extends AKElement {
static get styles() {
return [
PFBase,
PFDisplay,
css`
input {
width: 30ch;
box-sizing: border-box;
border: 0;
border-bottom: 1px solid;
border-bottom-color: var(--ak-accent);
background-color: transparent;
font-size: 1.5rem;
}
input:focus {
outline: 0;
}
`,
];
}
static styles = [
PFBase,
PFDisplay,
css`
input {
width: 30ch;
box-sizing: border-box;
border: 0;
border-bottom: 1px solid;
border-bottom-color: var(--ak-accent);
background-color: transparent;
font-size: 1.5rem;
}
input:focus {
outline: 0;
}
`,
];
@property({ attribute: false })
set apps(value: Application[]) {

View File

@ -35,9 +35,7 @@ import type { AppGroupList, PageUIConfig } from "./types";
@customElement("ak-library-impl")
export class LibraryPage extends AKElement {
static get styles() {
return styles;
}
static styles = styles;
@property({ attribute: "isadmin", type: Boolean })
isAdmin = false;

View File

@ -22,6 +22,7 @@ import "@goauthentik/elements/sidebar/Sidebar";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import "@goauthentik/elements/sidebar/SidebarItem";
import { ROUTES } from "@goauthentik/user/Routes";
import "@goauthentik/user/user-settings/details/UserSettingsFlowExecutor";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { match } from "ts-pattern";

View File

@ -53,6 +53,8 @@ export class UserSettingsFlowExecutor
@property({ type: Boolean })
loading = false;
frameMode = false;
static get styles(): CSSResult[] {
return [PFBase, PFCard, PFPage, PFButton, PFContent];
}
@ -87,7 +89,7 @@ export class UserSettingsFlowExecutor
}
firstUpdated(): void {
this.flowSlug = this.brand?.flowUserSettings;
this.flowSlug = this.flowSlug || this.brand?.flowUserSettings;
if (!this.flowSlug) {
return;
}

View File

@ -1,54 +0,0 @@
{
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"esModuleInterop": true,
"paths": {
"@goauthentik/docs/*": ["../website/docs/*"]
},
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"sourceMap": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"lib": [
"ES5",
"ES2015",
"ES2016",
"ES2017",
"ES2018",
"ES2019",
"ES2020",
"ESNext",
"DOM",
"DOM.Iterable",
"WebWorker"
],
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"useDefineForClassFields": false,
"alwaysStrict": true,
"noImplicitAny": true,
"plugins": [
{
"name": "ts-lit-plugin",
"strict": true,
"rules": {
"no-unknown-tag-name": "off",
"no-missing-import": "off",
"no-incompatible-type-binding": "off",
"no-unknown-property": "off",
"no-unknown-attribute": "off"
}
}
]
}
}

View File

@ -1,6 +1,6 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"strict": true,
"paths": {
"@goauthentik/authentik/*": ["src/*"],
"@goauthentik/admin/*": ["src/admin/*"],
@ -14,5 +14,51 @@
"@goauthentik/standalone/*": ["src/standalone/*"],
"@goauthentik/user/*": ["src/user/*"]
},
"baseUrl": ".",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"sourceMap": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"lib": [
"ES5",
"ES2015",
"ES2016",
"ES2017",
"ES2018",
"ES2019",
"ES2020",
"ESNext",
"DOM",
"DOM.Iterable",
"WebWorker"
],
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"useDefineForClassFields": false,
"alwaysStrict": true,
"noImplicitAny": true,
"plugins": [
{
"name": "ts-lit-plugin",
"strict": true,
"rules": {
"no-unknown-tag-name": "off",
"no-missing-import": "off",
"no-incompatible-type-binding": "off",
"no-unknown-property": "off",
"no-unknown-attribute": "off"
}
}
]
}
}

View File

@ -27,7 +27,7 @@ Starting in 2021.9, you can also select a Notification mapping. This allows you
```python
return {
"foo": request.context['notification'].body,
"foo": context['notification'].body,
}
```

View File

@ -0,0 +1,39 @@
---
title: Air-gapped environments
---
## Outbound connections
By default, authentik creates outbound connections to the following URLs:
- https://version.goauthentik.io: Periodic update check
- https://goauthentik.io: Anonymous analytics on startup
- https://secure.gravatar.com: Avatars for users
- https://authentik.error-reporting.a7k.io: Error reporting
To disable these outbound connections, set the following in your `.env` file:
```
AUTHENTIK_DISABLE_UPDATE_CHECK=true
AUTHENTIK_ERROR_REPORTING__ENABLED=false
AUTHENTIK_DISABLE_STARTUP_ANALYTICS=true
AUTHENTIK_AVATARS=initials
```
For a Helm-based install, set the following in your values.yaml file:
```yaml
authentik:
avatars: none
error_reporting:
enabled: false
disable_update_check: true
disable_startup_analytics: true
```
## Container images
Container images can be pulled from the following URLs:
- ghcr.io/goauthentik/server (https://ghcr.io)
- beryju/authentik (https://index.docker.io)

View File

@ -1,68 +0,0 @@
---
title: Air-gapped environments
---
## Outbound connections
By default, authentik creates outbound connections to the following URLs:
- https://version.goauthentik.io: Periodic update check
- https://goauthentik.io: Anonymous analytics on startup
- https://secure.gravatar.com: Avatars for users
- https://authentik.error-reporting.a7k.io: Error reporting
To disable these outbound connections, set the following in your `.env` file:
## Configuration options
To see a list of all configuration options, see [here](./configuration.mdx).
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
<Tabs
defaultValue="docker-compose"
values={[
{label: 'docker-compose', value: 'docker-compose'},
{label: 'Kubernetes', value: 'kubernetes'},
]}>
<TabItem value="docker-compose">
Add the following block to your `.env` file:
```shell
AUTHENTIK_DISABLE_STARTUP_ANALYTICS=true
AUTHENTIK_DISABLE_UPDATE_CHECK=true
AUTHENTIK_ERROR_REPORTING__ENABLED=false
```
Afterwards, run the upgrade commands from the latest release notes.
</TabItem>
<TabItem value="kubernetes">
Add the following block to your `values.yml` file:
```yaml
authentik:
error_reporting:
enabled: false
disable_update_check: true
disable_startup_analytics: true
```
Afterwards, run the upgrade commands from the latest release notes.
</TabItem>
</Tabs>
## Settings
In addition to the configuration options above, the following [System settings](../core/settings.md) need to also be adjusted:
- **Avatars**: By default this setting uses [Gravatar](https://secure.gravatar.com/). The option can be set to a combination of any of the other options, for example `initials`
## Container images
Container images can be pulled from the following URLs:
- ghcr.io/goauthentik/server (https://ghcr.io)
- beryju/authentik (https://index.docker.io)

View File

@ -32,10 +32,7 @@ The following placeholders will be used:
- **Redirect URIs/Origins (RegEx)**:
:::note
Please note that the following URIs are just examples. Be sure to include all of the domains / URLs that you will use to access Immich.
:::
- app.immich:/
- https://immich.company/auth/login
- https://immich.company/user-settings
::: - app.immich:/ - https://immich.company/auth/login - https://immich.company/user-settings
- **Signing Key**: authentik Self-signed Certificate
- Leave everything else as default
2. Open the new provider you've just created.

View File

@ -49,8 +49,8 @@ environment: OAUTH2_ENABLED=true
OAUTH2_USERINFO_ENDPOINT=/application/o/userinfo/
OAUTH2_TOKEN_ENDPOINT=/application/o/token/
OAUTH2_SECRET=<Client Secret from above>
OAUTH2_ID_MAP=sub
OAUTH2_USERNAME_MAP=email
OAUTH2_ID_MAP=preferred_username
OAUTH2_USERNAME_MAP=preferred_username
OAUTH2_FULLNAME_MAP=given_name
OAUTH2_EMAIL_MAP=email
```
@ -70,8 +70,8 @@ edit `.env` and add the following:
OAUTH2_USERINFO_ENDPOINT='/application/o/userinfo/'
OAUTH2_TOKEN_ENDPOINT='/application/o/token/'
OAUTH2_SECRET='<Client Secret from above>'
OAUTH2_ID_MAP='sub'
OAUTH2_USERNAME_MAP='email'
OAUTH2_ID_MAP='preferred_username'
OAUTH2_USERNAME_MAP='preferred_username'
OAUTH2_FULLNAME_MAP='given_name'
OAUTH2_EMAIL_MAP='email'
```

View File

@ -1,69 +0,0 @@
---
title: Xen Orchestra
---
<span class="badge badge--secondary">Support level: Community</span>
## What is Xen Orchestra
> Xen Orchestra provides a user friendly web interface for every Xen based hypervisor (XenServer, xcp-ng, etc.).
>
> -- https://xen-orchestra.com/
:::note
Xen Orchestra offers authentication plugins for OpenID Connect, SAML and LDAP. This guide is using the OpenID Connect plugin.
If you are using the Xen Orchestra Appliance, the OIDC Plugin should be present. If you are using Xen Orchestra compiled from sources, make sure the plugin `auth-oidc` is installed.
:::
## Preparation
The following placeholders will be used:
- `xenorchestra.company` is the FQDN of the Xen Orchestra instance.
- `authentik.company` is the FQDN of the authentik install.
## authentik configuration
### 1. Provider
Under _Providers_, create an OAuth2/OpenID provider with these settings:
- Name: Provider for XenOrchestra
- Authorization Flow: Select one of the available Flows.
- Client type: Confidential
- Redirect URIs/Origins: `https://xenorchestra.company/signin/oidc/callback`
Take note of the Client ID and the Client Secret, because we need them for the configuration of Xen Orchestra.
### 2. Application
Create an application with the following details:
- Slug: `xenorchestra` (If you want to choose a different slug, your URLs for the Xen Orchestra Configuration may vary.)
- Provider: Select the one we have created in Step 1
- Set the Launch URL to `https://xenorchestra.company/`
Optionally apply access restrictions to the application.
## Xen Orchestra configuration
Xen Orchestra allows the configuration of the OpenID Connect authentication in the plugin-section.
All of the URLs mentioned below can be copied & pasted from authentik (_Applications -> Providers -> *the provider created earlier*_).
1. Navigate to Settings -> Plugins
2. Scroll to **auth-oidc** and click on the **+** icon on the right hand side.
3. Configure the auth-oidc plugin with the following configuration values:
- Set the `Auto-discovery URL` to `https://authentik.company/application/o/xenorchestra/.well-known/openid-configuration`.
- Set the `Client identifier (key)` to the Client ID from your notes.
- Set the `Client secret` to the Client Secret from your notes.
- Check the `Fill information (optional)`-Checkbox to open the advanced menu.
- Set the `Username field` to `username`
- Set the `Scopes` to `openid profile email`
4. Enable the `auth-oidc`-Plugin by toggling the switch above the configuration.
5. You should be able to login with OIDC.
:::note
The first time a user signs in, Xen Orchesta will create a new user with the same username used in authentik. If you want to map the users by their e-mail-address instead of their username, you have to set the `Username field` to `email` in the Xen Orchestra plugin configuration.
:::

View File

@ -33,7 +33,7 @@
"@docusaurus/module-type-aliases": "3.1.1",
"@docusaurus/tsconfig": "3.1.1",
"@docusaurus/types": "3.1.1",
"@types/react": "^18.2.73",
"@types/react": "^18.2.70",
"prettier": "3.2.5",
"typescript": "~5.4.3"
},
@ -3999,11 +3999,12 @@
"integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA=="
},
"node_modules/@types/react": {
"version": "18.2.73",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.73.tgz",
"integrity": "sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==",
"version": "18.2.70",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.70.tgz",
"integrity": "sha512-hjlM2hho2vqklPhopNkXkdkeq6Lv8WSZTpr7956zY+3WS5cfYUewtCzsJLsbW5dEv3lfSeQ4W14ZFeKC437JRQ==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
@ -4049,6 +4050,11 @@
"@types/node": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.5",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz",
"integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw=="
},
"node_modules/@types/send": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.3.tgz",

View File

@ -52,7 +52,7 @@
"@docusaurus/module-type-aliases": "3.1.1",
"@docusaurus/tsconfig": "3.1.1",
"@docusaurus/types": "3.1.1",
"@types/react": "^18.2.73",
"@types/react": "^18.2.70",
"prettier": "3.2.5",
"typescript": "~5.4.3"
},

View File

@ -63,7 +63,6 @@ module.exports = {
"services/portainer/index",
"services/proxmox-ve/index",
"services/rancher/index",
"services/xen-orchestra/index",
"services/vmware-vcenter/index",
],
},