Merge branch 'main' into web/lage

* main: (21 commits)
  web: bump API Client version (#7964)
  release: 2023.10.5
  web: bump API Client version (#7962)
  providers/scim: use lock for sync (#7948)
  stages/email: prevent authentik emails from being marked as spam (also add text template support) (#7949)
  website/docs: prepare 2023.10.5 (#7947)
  web: bump the sentry group in /web with 2 updates (#7955)
  web: bump the wdio group in /tests/wdio with 4 updates (#7957)
  core: bump goauthentik.io/api/v3 from 3.2023104.4 to 3.2023104.5 (#7958)
  web: bump API Client version (#7953)
  events: add ASN Database reader (#7793)
  translate: Updates for file web/xliff/en.xlf in fr (#7951)
  website/docs: add expression example for geoip (#7739)
  website: fix hosted API browser (#7946)
  core: bump github.com/redis/go-redis/v9 from 9.3.0 to 9.3.1 (#7942)
  web: bump the sentry group in /web with 2 updates (#7944)
  web: bump the storybook group in /web with 7 updates (#7945)
  core: bump goauthentik.io/api/v3 from 3.2023104.3 to 3.2023104.4 (#7943)
  providers/scim: set timeout based on page and page count (#7941)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7928)
  ...
This commit is contained in:
Ken Sternberg 2023-12-21 12:11:25 -08:00
commit 12559f518a
566 changed files with 1041 additions and 9398 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2023.10.4
current_version = 2023.10.5
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -71,7 +71,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
# Stage 4: MaxMind GeoIP
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v6.0 as geoip
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City"
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
ENV GEOIPUPDATE_VERBOSE="true"
ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID"
ENV GEOIPUPDATE_LICENSE_KEY_FILE="/run/secrets/GEOIPUPDATE_LICENSE_KEY"

View File

@ -2,7 +2,7 @@
from os import environ
from typing import Optional
__version__ = "2023.10.4"
__version__ = "2023.10.5"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -19,7 +19,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer
from authentik.events.geo import GEOIP_READER
from authentik.events.context_processors.base import get_context_processors
from authentik.lib.config import CONFIG
capabilities = Signal()
@ -30,6 +30,7 @@ class Capabilities(models.TextChoices):
CAN_SAVE_MEDIA = "can_save_media"
CAN_GEO_IP = "can_geo_ip"
CAN_ASN = "can_asn"
CAN_IMPERSONATE = "can_impersonate"
CAN_DEBUG = "can_debug"
IS_ENTERPRISE = "is_enterprise"
@ -68,8 +69,9 @@ class ConfigView(APIView):
deb_test = settings.DEBUG or settings.TEST
if Path(settings.MEDIA_ROOT).is_mount() or deb_test:
caps.append(Capabilities.CAN_SAVE_MEDIA)
if GEOIP_READER.enabled:
caps.append(Capabilities.CAN_GEO_IP)
for processor in get_context_processors():
if cap := processor.capability():
caps.append(cap)
if CONFIG.get_bool("impersonation"):
caps.append(Capabilities.CAN_IMPERSONATE)
if settings.DEBUG: # pragma: no cover

View File

@ -14,7 +14,8 @@ from ua_parser import user_agent_parser
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import AuthenticatedSession
from authentik.events.geo import GEOIP_READER, GeoIPDict
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR, ASNDict
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR, GeoIPDict
class UserAgentDeviceDict(TypedDict):
@ -59,6 +60,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
current = SerializerMethodField()
user_agent = SerializerMethodField()
geo_ip = SerializerMethodField()
asn = SerializerMethodField()
def get_current(self, instance: AuthenticatedSession) -> bool:
"""Check if session is currently active session"""
@ -70,8 +72,12 @@ class AuthenticatedSessionSerializer(ModelSerializer):
return user_agent_parser.Parse(instance.last_user_agent)
def get_geo_ip(self, instance: AuthenticatedSession) -> Optional[GeoIPDict]: # pragma: no cover
"""Get parsed user agent"""
return GEOIP_READER.city_dict(instance.last_ip)
"""Get GeoIP Data"""
return GEOIP_CONTEXT_PROCESSOR.city_dict(instance.last_ip)
def get_asn(self, instance: AuthenticatedSession) -> Optional[ASNDict]: # pragma: no cover
"""Get ASN Data"""
return ASN_CONTEXT_PROCESSOR.asn_dict(instance.last_ip)
class Meta:
model = AuthenticatedSession
@ -80,6 +86,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
"current",
"user_agent",
"geo_ip",
"asn",
"user",
"last_ip",
"last_user_agent",

View File

@ -0,0 +1,79 @@
"""ASN Enricher"""
from typing import TYPE_CHECKING, Optional, TypedDict
from django.http import HttpRequest
from geoip2.errors import GeoIP2Error
from geoip2.models import ASN
from sentry_sdk import Hub
from authentik.events.context_processors.mmdb import MMDBContextProcessor
from authentik.lib.config import CONFIG
from authentik.root.middleware import ClientIPMiddleware
if TYPE_CHECKING:
from authentik.api.v3.config import Capabilities
from authentik.events.models import Event
class ASNDict(TypedDict):
"""ASN Details"""
asn: int
as_org: str | None
network: str | None
class ASNContextProcessor(MMDBContextProcessor):
"""ASN Database reader wrapper"""
def capability(self) -> Optional["Capabilities"]:
from authentik.api.v3.config import Capabilities
return Capabilities.CAN_ASN
def path(self) -> str | None:
return CONFIG.get("events.context_processors.asn")
def enrich_event(self, event: "Event"):
asn = self.asn_dict(event.client_ip)
if not asn:
return
event.context["asn"] = asn
def enrich_context(self, request: HttpRequest) -> dict:
return {
"asn": self.asn_dict(ClientIPMiddleware.get_client_ip(request)),
}
def asn(self, ip_address: str) -> Optional[ASN]:
"""Wrapper for Reader.asn"""
with Hub.current.start_span(
op="authentik.events.asn.asn",
description=ip_address,
):
if not self.enabled:
return None
self.check_expired()
try:
return self.reader.asn(ip_address)
except (GeoIP2Error, ValueError):
return None
def asn_to_dict(self, asn: ASN) -> ASNDict:
"""Convert ASN to dict"""
asn_dict: ASNDict = {
"asn": asn.autonomous_system_number,
"as_org": asn.autonomous_system_organization,
"network": str(asn.network) if asn.network else None,
}
return asn_dict
def asn_dict(self, ip_address: str) -> Optional[ASNDict]:
"""Wrapper for self.asn that returns a dict"""
asn = self.asn(ip_address)
if not asn:
return None
return self.asn_to_dict(asn)
ASN_CONTEXT_PROCESSOR = ASNContextProcessor()

View File

@ -0,0 +1,43 @@
"""Base event enricher"""
from functools import cache
from typing import TYPE_CHECKING, Optional
from django.http import HttpRequest
if TYPE_CHECKING:
from authentik.api.v3.config import Capabilities
from authentik.events.models import Event
class EventContextProcessor:
"""Base event enricher"""
def capability(self) -> Optional["Capabilities"]:
"""Return the capability this context processor provides"""
return None
def configured(self) -> bool:
"""Return true if this context processor is configured"""
return False
def enrich_event(self, event: "Event"):
"""Modify event"""
raise NotImplementedError
def enrich_context(self, request: HttpRequest) -> dict:
"""Modify context"""
raise NotImplementedError
@cache
def get_context_processors() -> list[EventContextProcessor]:
"""Get a list of all configured context processors"""
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
processors_types = [ASN_CONTEXT_PROCESSOR, GEOIP_CONTEXT_PROCESSOR]
processors = []
for _type in processors_types:
if _type.configured():
processors.append(_type)
return processors

View File

@ -0,0 +1,84 @@
"""events GeoIP Reader"""
from typing import TYPE_CHECKING, Optional, TypedDict
from django.http import HttpRequest
from geoip2.errors import GeoIP2Error
from geoip2.models import City
from sentry_sdk.hub import Hub
from authentik.events.context_processors.mmdb import MMDBContextProcessor
from authentik.lib.config import CONFIG
from authentik.root.middleware import ClientIPMiddleware
if TYPE_CHECKING:
from authentik.api.v3.config import Capabilities
from authentik.events.models import Event
class GeoIPDict(TypedDict):
"""GeoIP Details"""
continent: str
country: str
lat: float
long: float
city: str
class GeoIPContextProcessor(MMDBContextProcessor):
"""Slim wrapper around GeoIP API"""
def capability(self) -> Optional["Capabilities"]:
from authentik.api.v3.config import Capabilities
return Capabilities.CAN_GEO_IP
def path(self) -> str | None:
return CONFIG.get("events.context_processors.geoip")
def enrich_event(self, event: "Event"):
city = self.city_dict(event.client_ip)
if not city:
return
event.context["geo"] = city
def enrich_context(self, request: HttpRequest) -> dict:
# Different key `geoip` vs `geo` for legacy reasons
return {"geoip": self.city(ClientIPMiddleware.get_client_ip(request))}
def city(self, ip_address: str) -> Optional[City]:
"""Wrapper for Reader.city"""
with Hub.current.start_span(
op="authentik.events.geo.city",
description=ip_address,
):
if not self.enabled:
return None
self.check_expired()
try:
return self.reader.city(ip_address)
except (GeoIP2Error, ValueError):
return None
def city_to_dict(self, city: City) -> GeoIPDict:
"""Convert City to dict"""
city_dict: GeoIPDict = {
"continent": city.continent.code,
"country": city.country.iso_code,
"lat": city.location.latitude,
"long": city.location.longitude,
"city": "",
}
if city.city.name:
city_dict["city"] = city.city.name
return city_dict
def city_dict(self, ip_address: str) -> Optional[GeoIPDict]:
"""Wrapper for self.city that returns a dict"""
city = self.city(ip_address)
if not city:
return None
return self.city_to_dict(city)
GEOIP_CONTEXT_PROCESSOR = GeoIPContextProcessor()

View File

@ -0,0 +1,54 @@
"""Common logic for reading MMDB files"""
from pathlib import Path
from typing import Optional
from geoip2.database import Reader
from structlog.stdlib import get_logger
from authentik.events.context_processors.base import EventContextProcessor
class MMDBContextProcessor(EventContextProcessor):
"""Common logic for reading MaxMind DB files, including re-loading if the file has changed"""
def __init__(self):
self.reader: Optional[Reader] = None
self._last_mtime: float = 0.0
self.logger = get_logger()
self.open()
def path(self) -> str | None:
"""Get the path to the MMDB file to load"""
raise NotImplementedError
def open(self):
"""Get GeoIP Reader, if configured, otherwise none"""
path = self.path()
if path == "" or not path:
return
try:
self.reader = Reader(path)
self._last_mtime = Path(path).stat().st_mtime
self.logger.info("Loaded MMDB database", last_write=self._last_mtime, file=path)
except OSError as exc:
self.logger.warning("Failed to load MMDB database", exc=exc)
def check_expired(self):
"""Check if the modification date of the MMDB database has
changed, and reload it if so"""
path = self.path()
if path == "" or not path:
return
try:
mtime = Path(path).stat().st_mtime
diff = self._last_mtime < mtime
if diff > 0:
self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path)
self.open()
except OSError as exc:
self.logger.warning("Failed to check MMDB age", exc=exc)
@property
def enabled(self) -> bool:
"""Check if MMDB is enabled"""
return bool(self.reader)

View File

@ -1,100 +0,0 @@
"""events GeoIP Reader"""
from os import stat
from typing import Optional, TypedDict
from geoip2.database import Reader
from geoip2.errors import GeoIP2Error
from geoip2.models import City
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
LOGGER = get_logger()
class GeoIPDict(TypedDict):
"""GeoIP Details"""
continent: str
country: str
lat: float
long: float
city: str
class GeoIPReader:
"""Slim wrapper around GeoIP API"""
def __init__(self):
self.__reader: Optional[Reader] = None
self.__last_mtime: float = 0.0
self.__open()
def __open(self):
"""Get GeoIP Reader, if configured, otherwise none"""
path = CONFIG.get("geoip")
if path == "" or not path:
return
try:
self.__reader = Reader(path)
self.__last_mtime = stat(path).st_mtime
LOGGER.info("Loaded GeoIP database", last_write=self.__last_mtime)
except OSError as exc:
LOGGER.warning("Failed to load GeoIP database", exc=exc)
def __check_expired(self):
"""Check if the modification date of the GeoIP database has
changed, and reload it if so"""
path = CONFIG.get("geoip")
try:
mtime = stat(path).st_mtime
diff = self.__last_mtime < mtime
if diff > 0:
LOGGER.info("Found new GeoIP Database, reopening", diff=diff)
self.__open()
except OSError as exc:
LOGGER.warning("Failed to check GeoIP age", exc=exc)
return
@property
def enabled(self) -> bool:
"""Check if GeoIP is enabled"""
return bool(self.__reader)
def city(self, ip_address: str) -> Optional[City]:
"""Wrapper for Reader.city"""
with Hub.current.start_span(
op="authentik.events.geo.city",
description=ip_address,
):
if not self.enabled:
return None
self.__check_expired()
try:
return self.__reader.city(ip_address)
except (GeoIP2Error, ValueError):
return None
def city_to_dict(self, city: City) -> GeoIPDict:
"""Convert City to dict"""
city_dict: GeoIPDict = {
"continent": city.continent.code,
"country": city.country.iso_code,
"lat": city.location.latitude,
"long": city.location.longitude,
"city": "",
}
if city.city.name:
city_dict["city"] = city.city.name
return city_dict
def city_dict(self, ip_address: str) -> Optional[GeoIPDict]:
"""Wrapper for self.city that returns a dict"""
city = self.city(ip_address)
if not city:
return None
return self.city_to_dict(city)
GEOIP_READER = GeoIPReader()

View File

@ -26,7 +26,7 @@ from authentik.core.middleware import (
SESSION_KEY_IMPERSONATE_USER,
)
from authentik.core.models import ExpiringModel, Group, PropertyMapping, User
from authentik.events.geo import GEOIP_READER
from authentik.events.context_processors.base import get_context_processors
from authentik.events.utils import (
cleanse_dict,
get_user,
@ -246,21 +246,15 @@ class Event(SerializerModel, ExpiringModel):
self.user["on_behalf_of"] = get_user(request.session[SESSION_KEY_IMPERSONATE_USER])
# User 255.255.255.255 as fallback if IP cannot be determined
self.client_ip = ClientIPMiddleware.get_client_ip(request)
# Apply GeoIP Data, when enabled
self.with_geoip()
# Enrich event data
for processor in get_context_processors():
processor.enrich_event(self)
# If there's no app set, we get it from the requests too
if not self.app:
self.app = Event._get_app_from_request(request)
self.save()
return self
def with_geoip(self): # pragma: no cover
"""Apply GeoIP Data, when enabled"""
city = GEOIP_READER.city_dict(self.client_ip)
if not city:
return
self.context["geo"] = city
def save(self, *args, **kwargs):
if self._state.adding:
LOGGER.info(
@ -467,7 +461,7 @@ class NotificationTransport(SerializerModel):
}
mail = TemplateEmailMessage(
subject=subject_prefix + context["title"],
to=[notification.user.email],
to=[f"{notification.user.name} <{notification.user.email}>"],
language=notification.user.locale(),
template_name="email/event_notification.html",
template_context=context,

View File

@ -0,0 +1,24 @@
"""Test ASN Wrapper"""
from django.test import TestCase
from authentik.events.context_processors.asn import ASNContextProcessor
class TestASN(TestCase):
"""Test ASN Wrapper"""
def setUp(self) -> None:
self.reader = ASNContextProcessor()
def test_simple(self):
"""Test simple asn wrapper"""
# IPs from
# https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-ASN-Test.json
self.assertEqual(
self.reader.asn_dict("1.0.0.1"),
{
"asn": 15169,
"as_org": "Google Inc.",
"network": "1.0.0.0/24",
},
)

View File

@ -1,14 +1,14 @@
"""Test GeoIP Wrapper"""
from django.test import TestCase
from authentik.events.geo import GeoIPReader
from authentik.events.context_processors.geoip import GeoIPContextProcessor
class TestGeoIP(TestCase):
"""Test GeoIP Wrapper"""
def setUp(self) -> None:
self.reader = GeoIPReader()
self.reader = GeoIPContextProcessor()
def test_simple(self):
"""Test simple city wrapper"""

View File

@ -17,12 +17,13 @@ from django.db.models.base import Model
from django.http.request import HttpRequest
from django.utils import timezone
from django.views.debug import SafeExceptionReporterFilter
from geoip2.models import City
from geoip2.models import ASN, City
from guardian.utils import get_anonymous_user
from authentik.blueprints.v1.common import YAMLTag
from authentik.core.models import User
from authentik.events.geo import GEOIP_READER
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
from authentik.policies.types import PolicyRequest
# Special keys which are *not* cleaned, even when the default filter
@ -123,7 +124,9 @@ def sanitize_item(value: Any) -> Any:
if isinstance(value, (HttpRequest, WSGIRequest)):
return ...
if isinstance(value, City):
return GEOIP_READER.city_to_dict(value)
return GEOIP_CONTEXT_PROCESSOR.city_to_dict(value)
if isinstance(value, ASN):
return ASN_CONTEXT_PROCESSOR.asn_to_dict(value)
if isinstance(value, Path):
return str(value)
if isinstance(value, Exception):

View File

@ -35,6 +35,7 @@ REDIS_ENV_KEYS = [
]
DEPRECATIONS = {
"geoip": "events.context_processors.geoip",
"redis.broker_url": "broker.url",
"redis.broker_transport_options": "broker.transport_options",
"redis.cache_timeout": "cache.timeout",

View File

@ -108,7 +108,10 @@ cookie_domain: null
disable_update_check: false
disable_startup_analytics: false
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar,initials
geoip: "/geoip/GeoLite2-City.mmdb"
events:
context_processors:
geoip: "/geoip/GeoLite2-City.mmdb"
asn: "/geoip/GeoLite2-ASN.mmdb"
footer_links: []

View File

@ -47,6 +47,7 @@ class ReputationSerializer(ModelSerializer):
"identifier",
"ip",
"ip_geo_data",
"ip_asn_data",
"score",
"updated",
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2023-12-05 22:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies_reputation", "0005_reputation_expires_reputation_expiring"),
]
operations = [
migrations.AddField(
model_name="reputation",
name="ip_asn_data",
field=models.JSONField(default=dict),
),
]

View File

@ -76,6 +76,7 @@ class Reputation(ExpiringModel, SerializerModel):
identifier = models.TextField()
ip = models.GenericIPAddressField()
ip_geo_data = models.JSONField(default=dict)
ip_asn_data = models.JSONField(default=dict)
score = models.BigIntegerField(default=0)
expires = models.DateTimeField(default=reputation_expiry)

View File

@ -2,7 +2,8 @@
from django.core.cache import cache
from structlog.stdlib import get_logger
from authentik.events.geo import GEOIP_READER
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
from authentik.events.monitored_tasks import (
MonitoredTask,
TaskResult,
@ -26,7 +27,8 @@ def save_reputation(self: MonitoredTask):
ip=score["ip"],
identifier=score["identifier"],
)
rep.ip_geo_data = GEOIP_READER.city_dict(score["ip"]) or {}
rep.ip_geo_data = GEOIP_CONTEXT_PROCESSOR.city_dict(score["ip"]) or {}
rep.ip_asn_data = ASN_CONTEXT_PROCESSOR.asn_dict(score["ip"]) or {}
rep.score = score["score"]
objects_to_update.append(rep)
Reputation.objects.bulk_update(objects_to_update, ["score", "ip_geo_data"])

View File

@ -8,7 +8,7 @@ from django.db.models import Model
from django.http import HttpRequest
from structlog.stdlib import get_logger
from authentik.events.geo import GEOIP_READER
from authentik.events.context_processors.base import get_context_processors
if TYPE_CHECKING:
from authentik.core.models import User
@ -37,15 +37,9 @@ class PolicyRequest:
def set_http_request(self, request: HttpRequest): # pragma: no cover
"""Load data from HTTP request, including geoip when enabled"""
from authentik.root.middleware import ClientIPMiddleware
self.http_request = request
if not GEOIP_READER.enabled:
return
client_ip = ClientIPMiddleware.get_client_ip(request)
if not client_ip:
return
self.context["geoip"] = GEOIP_READER.city(client_ip)
for processor in get_context_processors():
self.context.update(processor.enrich_context(request))
@property
def should_cache(self) -> bool:

View File

@ -2,6 +2,7 @@
from django.utils.text import slugify
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import BooleanField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
@ -9,6 +10,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.tasks import TaskSerializer
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.events.monitored_tasks import TaskInfo
from authentik.providers.scim.models import SCIMProvider
@ -37,6 +39,13 @@ class SCIMProviderSerializer(ProviderSerializer):
extra_kwargs = {}
class SCIMSyncStatusSerializer(PassiveSerializer):
"""SCIM Provider sync status"""
is_running = BooleanField(read_only=True)
tasks = TaskSerializer(many=True, read_only=True)
class SCIMProviderViewSet(UsedByMixin, ModelViewSet):
"""SCIMProvider Viewset"""
@ -48,15 +57,18 @@ class SCIMProviderViewSet(UsedByMixin, ModelViewSet):
@extend_schema(
responses={
200: TaskSerializer(),
200: SCIMSyncStatusSerializer(),
404: OpenApiResponse(description="Task not found"),
}
)
@action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[])
def sync_status(self, request: Request, pk: int) -> Response:
"""Get provider's sync status"""
provider = self.get_object()
provider: SCIMProvider = self.get_object()
task = TaskInfo.by_name(f"scim_sync:{slugify(provider.name)}")
if not task:
return Response(status=404)
return Response(TaskSerializer(task).data)
tasks = [task] if task else []
status = {
"tasks": tasks,
"is_running": provider.sync_lock.locked(),
}
return Response(SCIMSyncStatusSerializer(status).data)

View File

@ -1,2 +1,3 @@
"""SCIM constants"""
PAGE_SIZE = 100
PAGE_TIMEOUT = 60 * 60 * 0.5 # Half an hour

View File

@ -1,11 +1,14 @@
"""SCIM Provider models"""
from django.core.cache import cache
from django.db import models
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from guardian.shortcuts import get_anonymous_user
from redis.lock import Lock
from rest_framework.serializers import Serializer
from authentik.core.models import BackchannelProvider, Group, PropertyMapping, User, UserTypes
from authentik.providers.scim.clients import PAGE_TIMEOUT
class SCIMProvider(BackchannelProvider):
@ -27,6 +30,15 @@ class SCIMProvider(BackchannelProvider):
help_text=_("Property mappings used for group creation/updating."),
)
@property
def sync_lock(self) -> Lock:
"""Redis lock for syncing SCIM to prevent multiple parallel syncs happening"""
return Lock(
cache.client.get_client(),
name=f"goauthentik.io/providers/scim/sync-{str(self.pk)}",
timeout=(60 * 60 * PAGE_TIMEOUT) * 3,
)
def get_user_qs(self) -> QuerySet[User]:
"""Get queryset of all users with consistent ordering
according to the provider's settings"""

View File

@ -12,7 +12,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import Group, User
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.lib.utils.reflection import path_to_class
from authentik.providers.scim.clients import PAGE_SIZE
from authentik.providers.scim.clients import PAGE_SIZE, PAGE_TIMEOUT
from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.exceptions import SCIMRequestException, StopSync
from authentik.providers.scim.clients.group import SCIMGroupClient
@ -47,12 +47,19 @@ def scim_sync(self: MonitoredTask, provider_pk: int) -> None:
).first()
if not provider:
return
lock = provider.sync_lock
if lock.locked():
LOGGER.debug("SCIM sync locked, skipping task", source=provider.name)
return
self.set_uid(slugify(provider.name))
result = TaskResult(TaskResultStatus.SUCCESSFUL, [])
result.messages.append(_("Starting full SCIM sync"))
LOGGER.debug("Starting SCIM sync")
users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE)
groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
self.soft_time_limit = self.time_limit = (
users_paginator.count + groups_paginator.count
) * PAGE_TIMEOUT
with allow_join_result():
try:
for page in users_paginator.page_range:
@ -69,7 +76,10 @@ def scim_sync(self: MonitoredTask, provider_pk: int) -> None:
self.set_status(result)
@CELERY_APP.task()
@CELERY_APP.task(
soft_time_limit=PAGE_TIMEOUT,
task_time_limit=PAGE_TIMEOUT,
)
def scim_sync_users(page: int, provider_pk: int):
"""Sync single or multiple users to SCIM"""
messages = []

View File

@ -32,7 +32,8 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
settings.TEST = True
settings.CELERY["task_always_eager"] = True
CONFIG.set("avatars", "none")
CONFIG.set("geoip", "tests/GeoLite2-City-Test.mmdb")
CONFIG.set("events.context_processors.geoip", "tests/GeoLite2-City-Test.mmdb")
CONFIG.set("events.context_processors.asn", "tests/GeoLite2-ASN-Test.mmdb")
CONFIG.set("blueprints_dir", "./blueprints")
CONFIG.set(
"outposts.container_image_base",

View File

@ -110,7 +110,7 @@ class EmailStageView(ChallengeStageView):
try:
message = TemplateEmailMessage(
subject=_(current_stage.subject),
to=[email],
to=[f"{pending_user.name} <{email}>"],
language=pending_user.locale(self.request),
template_name=current_stage.template,
template_context={

View File

@ -0,0 +1,8 @@
{% load i18n %}{% translate "Welcome!" %}
{% translate "We're excited to have you get started. First, you need to confirm your account. Just open the link below." %}
{{ url }}
--
Powered by goauthentik.io.

View File

@ -44,7 +44,7 @@
<tr>
<td style="padding: 20px; font-size: 12px; color: #212124;" align="center">
{% blocktranslate with name=source.from %}
This email was sent from the notification transport <code>{{name}}</code>.
This email was sent from the notification transport <code>{{ name }}</code>.
{% endblocktranslate %}
</td>
</tr>

View File

@ -0,0 +1,18 @@
{% load authentik_stages_email %}{% load i18n %}{% translate "Dear authentik user," %}
{% translate "The following notification was created:" %}
{{ body|indent }}
{% if key_value %}
{% translate "Additional attributes:" %}
{% for key, value in key_value.items %}
{{ key }}: {{ value|indent }}{% endfor %}
{% endif %}
{% if source %}{% blocktranslate with name=source.from %}
This email was sent from the notification transport {{ name }}.
{% endblocktranslate %}{% endif %}
--
Powered by goauthentik.io.

View File

@ -0,0 +1,12 @@
{% load i18n %}{% load humanize %}{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %}
{% blocktrans %}
You recently requested to change your password for your authentik account. Use the link below to set a new password.
{% endblocktrans %}
{{ url }}
{% blocktrans with expires=expires|naturaltime %}
If you did not request a password change, please ignore this Email. The link above is valid for {{ expires }}.
{% endblocktrans %}
--
Powered by goauthentik.io.

View File

@ -0,0 +1,7 @@
{% load i18n %}authentik Test-Email
{% blocktrans %}
This is a test email to inform you, that you've successfully configured authentik emails.
{% endblocktrans %}
--
Powered by goauthentik.io.

View File

@ -29,3 +29,9 @@ def inline_static_binary(path: str) -> str:
b64content = b64encode(_file.read().encode())
return f"data:image/{result.suffix};base64,{b64content.decode('utf-8')}"
return path
@register.filter(name="indent")
def indent_string(val, num_spaces=4):
"""Intent text by a given amount of spaces"""
return val.replace("\n", "\n" + " " * num_spaces)

View File

@ -58,9 +58,11 @@ class TestEmailStageSending(FlowTestCase):
events = Event.objects.filter(action=EventAction.EMAIL_SENT)
self.assertEqual(len(events), 1)
event = events.first()
self.assertEqual(event.context["message"], f"Email to {self.user.email} sent")
self.assertEqual(
event.context["message"], f"Email to {self.user.name} <{self.user.email}> sent"
)
self.assertEqual(event.context["subject"], "authentik")
self.assertEqual(event.context["to_email"], [self.user.email])
self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"])
self.assertEqual(event.context["from_email"], "system@authentik.local")
def test_pending_fake_user(self):

View File

@ -94,7 +94,7 @@ class TestEmailStage(FlowTestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik")
self.assertEqual(mail.outbox[0].to, [self.user.email])
self.assertEqual(mail.outbox[0].to, [f"{self.user.name} <{self.user.email}>"])
@patch(
"authentik.stages.email.models.EmailStage.backend_class",
@ -114,7 +114,7 @@ class TestEmailStage(FlowTestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik")
self.assertEqual(mail.outbox[0].to, ["foo@bar.baz"])
self.assertEqual(mail.outbox[0].to, [f"{self.user.name} <foo@bar.baz>"])
@patch(
"authentik.stages.email.models.EmailStage.backend_class",

View File

@ -4,6 +4,7 @@ from functools import lru_cache
from pathlib import Path
from django.core.mail import EmailMultiAlternatives
from django.template.exceptions import TemplateDoesNotExist
from django.template.loader import render_to_string
from django.utils import translation
@ -24,9 +25,15 @@ class TemplateEmailMessage(EmailMultiAlternatives):
"""Wrapper around EmailMultiAlternatives with integrated template rendering"""
def __init__(self, template_name=None, template_context=None, language="", **kwargs):
super().__init__(**kwargs)
with translation.override(language):
html_content = render_to_string(template_name, template_context)
super().__init__(**kwargs)
self.content_subtype = "html"
try:
text_content = render_to_string(
template_name.replace("html", "txt"), template_context
)
self.body = text_content
except TemplateDoesNotExist:
pass
self.mixed_subtype = "related"
self.attach_alternative(html_content, "text/html")

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.4}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.5}
restart: unless-stopped
command: server
environment:
@ -53,7 +53,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.4}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.5}
restart: unless-stopped
command: worker
environment:

4
go.mod
View File

@ -23,11 +23,11 @@ require (
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.17.0
github.com/redis/go-redis/v9 v9.3.0
github.com/redis/go-redis/v9 v9.3.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
goauthentik.io/api/v3 v3.2023104.3
goauthentik.io/api/v3 v3.2023104.5
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.15.0
golang.org/x/sync v0.5.0

8
go.sum
View File

@ -256,8 +256,8 @@ github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdO
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
@ -309,8 +309,8 @@ go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYO
go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY=
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.2023104.3 h1:MzwdB21Q+G+wACEZiX0T1iVV4l7PjopjaVv6muqJE1M=
goauthentik.io/api/v3 v3.2023104.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2023104.5 h1:CWaQq44DxElyqMvZVjqMhWIr1vXzobf4eqFUas0lMgs=
goauthentik.io/api/v3 v3.2023104.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

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

View File

@ -113,7 +113,7 @@ filterwarnings = [
[tool.poetry]
name = "authentik"
version = "2023.10.4"
version = "2023.10.5"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2023.10.4
version: 2023.10.5
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -17079,7 +17079,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Task'
$ref: '#/components/schemas/SCIMSyncStatus'
description: ''
'404':
description: Task not found
@ -28280,7 +28280,7 @@ components:
readOnly: true
geo_ip:
type: object
description: Get parsed user agent
description: Get GeoIP Data
properties:
continent:
type: string
@ -28302,6 +28302,24 @@ components:
- long
nullable: true
readOnly: true
asn:
type: object
description: Get ASN Data
properties:
asn:
type: integer
as_org:
type: string
nullable: true
network:
type: string
nullable: true
required:
- as_org
- asn
- network
nullable: true
readOnly: true
user:
type: integer
last_ip:
@ -28316,6 +28334,7 @@ components:
type: string
format: date-time
required:
- asn
- current
- geo_ip
- last_ip
@ -29283,6 +29302,7 @@ components:
enum:
- can_save_media
- can_geo_ip
- can_asn
- can_impersonate
- can_debug
- is_enterprise
@ -29290,6 +29310,7 @@ components:
description: |-
* `can_save_media` - Can Save Media
* `can_geo_ip` - Can Geo Ip
* `can_asn` - Can Asn
* `can_impersonate` - Can Impersonate
* `can_debug` - Can Debug
* `is_enterprise` - Is Enterprise
@ -39667,6 +39688,7 @@ components:
ip:
type: string
ip_geo_data: {}
ip_asn_data: {}
score:
type: integer
maximum: 9223372036854775807
@ -40623,6 +40645,21 @@ components:
- name
- token
- url
SCIMSyncStatus:
type: object
description: SCIM Provider sync status
properties:
is_running:
type: boolean
readOnly: true
tasks:
type: array
items:
$ref: '#/components/schemas/Task'
readOnly: true
required:
- is_running
- tasks
SMSDevice:
type: object
description: Serializer for sms authenticator devices

View File

@ -17,7 +17,12 @@ with open("local.env.yml", "w", encoding="utf-8") as _config:
},
"blueprints_dir": "./blueprints",
"cert_discovery_dir": "./certs",
"geoip": "tests/GeoLite2-City-Test.mmdb",
"events": {
"processors": {
"geoip": "tests/GeoLite2-City-Test.mmdb",
"asn": "tests/GeoLite2-ASN-Test.mmdb",
}
},
},
_config,
default_flow_style=False,

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -9,10 +9,10 @@
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"@wdio/cli": "^8.26.3",
"@wdio/local-runner": "^8.26.3",
"@wdio/mocha-framework": "^8.26.3",
"@wdio/spec-reporter": "^8.26.3",
"@wdio/cli": "^8.27.0",
"@wdio/local-runner": "^8.27.0",
"@wdio/mocha-framework": "^8.27.0",
"@wdio/spec-reporter": "^8.27.0",
"eslint": "^8.56.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.23.0",
@ -1141,18 +1141,18 @@
"dev": true
},
"node_modules/@wdio/cli": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.26.3.tgz",
"integrity": "sha512-wrq145sNBw4DrsF5GEK8TBxqVWln7GZpNpM5QeDqCcZzVHIqDud4f7nADgZGbR8dJ96NVfC3rby6wbeRQUA+eg==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.27.0.tgz",
"integrity": "sha512-wdNYNvu52XxOqNHqDMGAtexBz+MM0RE2Z5U5ljyllbP3ed5vcvvK9vswURtI4cFGoqobVeoC7wif3VeD3aN+aQ==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.1",
"@wdio/config": "8.26.3",
"@wdio/globals": "8.26.3",
"@wdio/config": "8.27.0",
"@wdio/globals": "8.27.0",
"@wdio/logger": "8.24.12",
"@wdio/protocols": "8.24.12",
"@wdio/types": "8.26.3",
"@wdio/utils": "8.26.3",
"@wdio/types": "8.27.0",
"@wdio/utils": "8.27.0",
"async-exit-hook": "^2.0.1",
"chalk": "^5.2.0",
"chokidar": "^3.5.3",
@ -1167,7 +1167,7 @@
"lodash.union": "^4.6.0",
"read-pkg-up": "^10.0.0",
"recursive-readdir": "^2.2.3",
"webdriverio": "8.26.3",
"webdriverio": "8.27.0",
"yargs": "^17.7.2"
},
"bin": {
@ -1190,14 +1190,14 @@
}
},
"node_modules/@wdio/config": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.26.3.tgz",
"integrity": "sha512-NWh2JXRSyP4gY+jeC79u0L3hSXW/s3rOWez4M6qAglT91fZTXFbIl1GM8lnZlCq03ye2qFPqYrZ+4tGNQj7YxQ==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.27.0.tgz",
"integrity": "sha512-zYM5daeiBVVAbQj0ASymAt0RUsocLVIwKiUHNa8gg/1GsZnztGjetXExSp1gXlxtMVM5xWUSKjh6ceFK79gWDQ==",
"dev": true,
"dependencies": {
"@wdio/logger": "8.24.12",
"@wdio/types": "8.26.3",
"@wdio/utils": "8.26.3",
"@wdio/types": "8.27.0",
"@wdio/utils": "8.27.0",
"decamelize": "^6.0.0",
"deepmerge-ts": "^5.0.0",
"glob": "^10.2.2",
@ -1208,29 +1208,29 @@
}
},
"node_modules/@wdio/globals": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.26.3.tgz",
"integrity": "sha512-RW3UsvnUb4DjxVOqIngXQMcDJlbH+QL/LeChznUF0FW+Mqg/mZWukBld5/dDwgQHk9F2TOzc8ctk5FM3s1AoWQ==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.27.0.tgz",
"integrity": "sha512-HUPOIsrmxfF0LhU68lVsNGQGZkW/bWOvcCd8WxeaggTAH9JyxasxxfwzeCceAuhAvwtlwoMXITOpjAXO2mj38Q==",
"dev": true,
"engines": {
"node": "^16.13 || >=18"
},
"optionalDependencies": {
"expect-webdriverio": "^4.6.1",
"webdriverio": "8.26.3"
"webdriverio": "8.27.0"
}
},
"node_modules/@wdio/local-runner": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.26.3.tgz",
"integrity": "sha512-YWxTBp6tc8Dlz09rnRhV2GXV4b3w5G0WyYEf81D+kXDI3QxDvYn6QujByr6Q7fQ9yLwJU4ptnT2uL5IbAwVo2A==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.27.0.tgz",
"integrity": "sha512-nxS17mhoLkXP20eoPMkz7tbMFMOQejSw0hZfkEvuDCNhJokr8ugp6IjYXL9f7yV9IB9UDGHox8WGY4ArSrOeBA==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/logger": "8.24.12",
"@wdio/repl": "8.24.12",
"@wdio/runner": "8.26.3",
"@wdio/types": "8.26.3",
"@wdio/runner": "8.27.0",
"@wdio/types": "8.27.0",
"async-exit-hook": "^2.0.1",
"split2": "^4.1.0",
"stream-buffers": "^3.0.2"
@ -1267,16 +1267,16 @@
}
},
"node_modules/@wdio/mocha-framework": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.26.3.tgz",
"integrity": "sha512-r9uHUcXhh6TKFqTBCacnbtQx0nZjFsnV9Pony8CY9qcNCn2q666ucQbrtMfZdjuevhn5N0E710+El4eAvK3jyw==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.27.0.tgz",
"integrity": "sha512-NaFUPv90ks1XlZy0qdUaJ5/ilBtiCCgTIxaPexshJiaVDT5cV+Igjag/O80HIcvqknOZpdKAR0I1ArQzhJrmcA==",
"dev": true,
"dependencies": {
"@types/mocha": "^10.0.0",
"@types/node": "^20.1.0",
"@wdio/logger": "8.24.12",
"@wdio/types": "8.26.3",
"@wdio/utils": "8.26.3",
"@wdio/types": "8.27.0",
"@wdio/utils": "8.27.0",
"mocha": "^10.0.0"
},
"engines": {
@ -1302,14 +1302,14 @@
}
},
"node_modules/@wdio/reporter": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.26.3.tgz",
"integrity": "sha512-F/sF1Hwxp1osM2wto4JydONQGxqkbOhwbLM0o4Y8eHPgK7/m+Kn9uygBDqPVpgQnpf0kUfhpICe9gaZzG4Jt+g==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.27.0.tgz",
"integrity": "sha512-kBwsrHbsblmXfHSWlaOKXjPRPeT29WSKTUoCmzuTcCkhvbjY4TrEB0p04cpaM7uNqdIZTxHng54gZVaG/nZPiw==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/logger": "8.24.12",
"@wdio/types": "8.26.3",
"@wdio/types": "8.27.0",
"diff": "^5.0.0",
"object-inspect": "^1.12.0"
},
@ -1318,35 +1318,35 @@
}
},
"node_modules/@wdio/runner": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.26.3.tgz",
"integrity": "sha512-mbZGkBbXTRtj1hL5QUbNxpJvhE4rkXvYlUuea1uOVk3e2/+k2dZeGeKPgh1Q7Dt07118dfujCB7pQCYldE/dGg==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.27.0.tgz",
"integrity": "sha512-da332r2d1QXdRhMhsDxMObcqLZS0l/u14pHICNTvEHp+72gOttbjUDvdMHPQY6Ae5ul7AVVQ05qpmz9CX7TzOg==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/config": "8.26.3",
"@wdio/globals": "8.26.3",
"@wdio/config": "8.27.0",
"@wdio/globals": "8.27.0",
"@wdio/logger": "8.24.12",
"@wdio/types": "8.26.3",
"@wdio/utils": "8.26.3",
"@wdio/types": "8.27.0",
"@wdio/utils": "8.27.0",
"deepmerge-ts": "^5.0.0",
"expect-webdriverio": "^4.6.1",
"gaze": "^1.1.2",
"webdriver": "8.26.3",
"webdriverio": "8.26.3"
"webdriver": "8.27.0",
"webdriverio": "8.27.0"
},
"engines": {
"node": "^16.13 || >=18"
}
},
"node_modules/@wdio/spec-reporter": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.26.3.tgz",
"integrity": "sha512-YfKlBOmxGyJk08BpDnsjPsp85XyhG7Cu2qoAVxtJ8kkJOZaGfUg9TBV9DXDqvdZcxCMnPfDfQIda6LzfkZf58Q==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.27.0.tgz",
"integrity": "sha512-EOXLBIr4oLzSDp/BQ86IqCulSF0jwEAj2EiMeY6dh9WXzBBtoR8WnoX/27xFoZ8GU2zetWC3EVnLJ0Ex8Up1mA==",
"dev": true,
"dependencies": {
"@wdio/reporter": "8.26.3",
"@wdio/types": "8.26.3",
"@wdio/reporter": "8.27.0",
"@wdio/types": "8.27.0",
"chalk": "^5.1.2",
"easy-table": "^1.2.0",
"pretty-ms": "^7.0.0"
@ -1368,9 +1368,9 @@
}
},
"node_modules/@wdio/types": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.26.3.tgz",
"integrity": "sha512-WOxvSV4sKJ5QCRNTJHeKCzgO2TZmcK1eDlJ+FObt9Pnt+4pCRy/881eVY/Aj2bozn2hhzq0AK/h6oPAUV/gjCg==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.27.0.tgz",
"integrity": "sha512-LbP9FKh8r0uW9/dKhTIUCC1Su8PsP9TmzGKXkWt6/IMacgJiB/zW3u1CgyaLw9lG0UiQORHGoeJX9zB2HZAh4w==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0"
@ -1380,14 +1380,14 @@
}
},
"node_modules/@wdio/utils": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.26.3.tgz",
"integrity": "sha512-LA/iCKgJQemAAXoN6vHyKBtngdkFUWEnjB8Yd1Xm3gUQTvY4GVlvcqOxC2RF5Th7/L2LNxc6TWuErYv/mm5H+w==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.27.0.tgz",
"integrity": "sha512-4BY+JBQssVn003P5lA289uDMie3LtGinHze5btkcW9timB6VaU+EeZS4eKTPC0pziizLhteVvXYxv3YTpeeRfA==",
"dev": true,
"dependencies": {
"@puppeteer/browsers": "^1.6.0",
"@wdio/logger": "8.24.12",
"@wdio/types": "8.26.3",
"@wdio/types": "8.27.0",
"decamelize": "^6.0.0",
"deepmerge-ts": "^5.1.0",
"edgedriver": "^5.3.5",
@ -8495,18 +8495,18 @@
}
},
"node_modules/webdriver": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.26.3.tgz",
"integrity": "sha512-vHbMj0BFXPMtKVmJsVIkFVbdOT8eXkjFeJ7LmJL8cMMe1S5Lt44DqRjSBBoGsqYoYgIBmKpqAQcDrHrv9m7smQ==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.27.0.tgz",
"integrity": "sha512-n1IA+rR3u84XxU9swiKUM06BkEC0GDimfZkBML57cny+utQOUbdM/mBpqCUnkWX/RBz/p2EfHdKNyOs3/REaog==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@types/ws": "^8.5.3",
"@wdio/config": "8.26.3",
"@wdio/config": "8.27.0",
"@wdio/logger": "8.24.12",
"@wdio/protocols": "8.24.12",
"@wdio/types": "8.26.3",
"@wdio/utils": "8.26.3",
"@wdio/types": "8.27.0",
"@wdio/utils": "8.27.0",
"deepmerge-ts": "^5.1.0",
"got": "^12.6.1",
"ky": "^0.33.0",
@ -8517,18 +8517,18 @@
}
},
"node_modules/webdriverio": {
"version": "8.26.3",
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.26.3.tgz",
"integrity": "sha512-5Ka8MOQoK866EI3whiCvzD1IiKFBq9niWF3lh92uMt6ZjbUZZoe5esWIHhFsHFxT6dOOU8uXR/Gr6qsBJFZReA==",
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.27.0.tgz",
"integrity": "sha512-Qh5VCiBjEmxnmXcL1QEFoDzFqTtaWKrXriuU5G0yHKCModGAt2G7IHTkAok3CpmkVJfZpEvY630aP1MvgDtFhw==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/config": "8.26.3",
"@wdio/config": "8.27.0",
"@wdio/logger": "8.24.12",
"@wdio/protocols": "8.24.12",
"@wdio/repl": "8.24.12",
"@wdio/types": "8.26.3",
"@wdio/utils": "8.26.3",
"@wdio/types": "8.27.0",
"@wdio/utils": "8.27.0",
"archiver": "^6.0.0",
"aria-query": "^5.0.0",
"css-shorthand-properties": "^1.1.1",
@ -8545,7 +8545,7 @@
"resq": "^1.9.1",
"rgb2hex": "0.2.5",
"serialize-error": "^11.0.1",
"webdriver": "8.26.3"
"webdriver": "8.27.0"
},
"engines": {
"node": "^16.13 || >=18"

View File

@ -6,10 +6,10 @@
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"@wdio/cli": "^8.26.3",
"@wdio/local-runner": "^8.26.3",
"@wdio/mocha-framework": "^8.26.3",
"@wdio/spec-reporter": "^8.26.3",
"@wdio/cli": "^8.27.0",
"@wdio/local-runner": "^8.27.0",
"@wdio/mocha-framework": "^8.27.0",
"@wdio/spec-reporter": "^8.27.0",
"eslint": "^8.56.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.23.0",

482
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@
"lage": "^2.7.9"
},
"workspaces": [
"packages/web"
"packages/monolith"
],
"engines": {
"node": ">=20"

View File

Before

Width:  |  Height:  |  Size: 663 B

After

Width:  |  Height:  |  Size: 663 B

View File

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 254 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 351 B

View File

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 402 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 543 B

After

Width:  |  Height:  |  Size: 543 B

View File

Before

Width:  |  Height:  |  Size: 368 B

After

Width:  |  Height:  |  Size: 368 B

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 878 B

After

Width:  |  Height:  |  Size: 878 B

View File

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 587 B

View File

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 887 B

After

Width:  |  Height:  |  Size: 887 B

View File

Before

Width:  |  Height:  |  Size: 732 B

After

Width:  |  Height:  |  Size: 732 B

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,5 +1,5 @@
{
"name": "@goauthentik/web",
"name": "@goauthentik/monolith",
"version": "0.0.0",
"private": true,
"license": "MIT",
@ -16,7 +16,7 @@
"watch": "run-s build-locales rollup:watch",
"lint": "eslint . --max-warnings 0 --fix",
"lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[M?][M?]' | cut -c8- | grep -E '\\.(ts|js|tsx|jsx)$') ",
"lint:spelling": "codespell -D - -D ../.github/codespell-dictionary.txt -I ../.github/codespell-words.txt -S './src/locales/**' ./src -s",
"lint:spelling": "codespell -D - -D ../../../.github/codespell-dictionary.txt -I ../../../.github/codespell-words.txt -S './src/locales/**' ./src -s",
"lit-analyse": "lit-analyzer src",
"precommit": "run-s tsc lit-analyse lint:precommit lint:spelling prettier",
"prequick": "run-s tsc:execute lit-analyse lint:precommit lint:spelling",
@ -42,15 +42,15 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.3",
"@fortawesome/fontawesome-free": "^6.5.1",
"@goauthentik/api": "^2023.10.4-1702989148",
"@goauthentik/api": "^2023.10.5-1703167718",
"@lit-labs/context": "^0.4.0",
"@lit-labs/task": "^3.1.0",
"@lit/localize": "^0.11.4",
"@open-wc/lit-helpers": "^0.6.0",
"@patternfly/elements": "^2.4.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.88.0",
"@sentry/tracing": "^7.88.0",
"@sentry/browser": "^7.90.0",
"@sentry/tracing": "^7.90.0",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.4.1",
@ -86,13 +86,13 @@
"@rollup/plugin-replace": "^5.0.5",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.5",
"@storybook/addon-essentials": "^7.6.5",
"@storybook/addon-links": "^7.6.5",
"@storybook/api": "^7.6.5",
"@storybook/addon-essentials": "^7.6.6",
"@storybook/addon-links": "^7.6.6",
"@storybook/api": "^7.6.6",
"@storybook/blocks": "^7.6.4",
"@storybook/manager-api": "^7.6.5",
"@storybook/web-components": "^7.6.5",
"@storybook/web-components-vite": "^7.6.5",
"@storybook/manager-api": "^7.6.6",
"@storybook/web-components": "^7.6.6",
"@storybook/web-components-vite": "^7.6.6",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "5.60.15",
@ -120,7 +120,7 @@
"rollup-plugin-cssimport": "^1.0.3",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^7.6.5",
"storybook": "^7.6.6",
"storybook-addon-mock": "^4.3.0",
"ts-lit-plugin": "^2.0.1",
"tslib": "^2.6.2",

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