Compare commits
12 Commits
providers/
...
core/b2c-i
Author | SHA1 | Date | |
---|---|---|---|
195091ed3b | |||
4de3f1f4b8 | |||
af4f1b3421 | |||
77b816ad51 | |||
b28dd485a0 | |||
4701389745 | |||
0d0097e956 | |||
b42eb0706d | |||
3afe386e18 | |||
34dd9c0b63 | |||
b2f2fd241d | |||
828f477548 |
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
21
authentik/brands/migrations/0007_brand_origin.py
Normal file
21
authentik/brands/migrations/0007_brand_origin.py
Normal 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.",
|
||||
),
|
||||
),
|
||||
]
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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"""
|
||||
|
||||
|
@ -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"""
|
||||
|
@ -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)
|
@ -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,
|
||||
|
@ -47,4 +47,3 @@ class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
|
||||
filterset_fields = "__all__"
|
||||
search_fields = ["stage__name"]
|
||||
ordering = ["order"]
|
||||
ordering_fields = ["order", "stage__name"]
|
||||
|
@ -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:
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
15
authentik/providers/oauth2/signals.py
Normal file
15
authentik/providers/oauth2/signals.py
Normal 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()
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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",
|
||||
]
|
||||
|
@ -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)
|
||||
|
@ -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
2
go.mod
@ -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
4
go.sum
@ -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=
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
36
poetry.lock
generated
@ -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]
|
||||
|
80
schema.yml
80
schema.yml
@ -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
|
||||
|
@ -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"""
|
||||
|
16
tests/wdio/package-lock.json
generated
16
tests/wdio/package-lock.json
generated
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
193
web/package-lock.json
generated
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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`
|
||||
<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>
|
||||
|
@ -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")),
|
||||
];
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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}`);
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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"),
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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("");
|
||||
|
@ -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}`,
|
||||
];
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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&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"}">
|
||||
|
@ -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 {
|
||||
|
@ -47,6 +47,10 @@ export class AuthenticatorValidateStage
|
||||
return this.host.brand;
|
||||
}
|
||||
|
||||
get frameMode(): boolean {
|
||||
return this.host.frameMode;
|
||||
}
|
||||
|
||||
@state()
|
||||
_selectedDeviceChallenge?: DeviceChallenge;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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[]) {
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
```
|
||||
|
||||
|
39
website/docs/installation/air-gapped.md
Normal file
39
website/docs/installation/air-gapped.md
Normal 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)
|
@ -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)
|
@ -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.
|
||||
|
@ -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'
|
||||
```
|
||||
|
@ -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.
|
||||
:::
|
14
website/package-lock.json
generated
14
website/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -63,7 +63,6 @@ module.exports = {
|
||||
"services/portainer/index",
|
||||
"services/proxmox-ve/index",
|
||||
"services/rancher/index",
|
||||
"services/xen-orchestra/index",
|
||||
"services/vmware-vcenter/index",
|
||||
],
|
||||
},
|
||||
|
Reference in New Issue
Block a user