Merge branch 'main' into multi-tenant-django-tenants
This commit is contained in:
commit
2af782c023
|
@ -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+)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
@ -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 self.request.tenant.impersonation:
|
||||
caps.append(Capabilities.CAN_IMPERSONATE)
|
||||
if settings.DEBUG: # pragma: no cover
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -44,6 +44,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
|||
if request:
|
||||
req.http_request = request
|
||||
self._context["request"] = req
|
||||
req.context.update(**kwargs)
|
||||
self._context.update(**kwargs)
|
||||
self.dry_run = dry_run
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from prometheus_client import Gauge
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.lib.config import CONFIG, ENV_PREFIX
|
||||
|
||||
GAUGE_TASKS = Gauge(
|
||||
"authentik_system_tasks",
|
||||
|
@ -21,3 +22,24 @@ class AuthentikEventsConfig(ManagedAppConfig):
|
|||
def reconcile_load_events_signals(self):
|
||||
"""Load events signals"""
|
||||
self.import_module("authentik.events.signals")
|
||||
|
||||
def reconcile_check_deprecations(self):
|
||||
"""Check for config deprecations"""
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
for key_replace, msg in CONFIG.deprecations.items():
|
||||
key, replace = key_replace
|
||||
key_env = f"{ENV_PREFIX}_{key.replace('.', '__')}".upper()
|
||||
replace_env = f"{ENV_PREFIX}_{replace.replace('.', '__')}".upper()
|
||||
if Event.objects.filter(
|
||||
action=EventAction.CONFIGURATION_ERROR, context__deprecated_option=key
|
||||
).exists():
|
||||
continue
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
deprecated_option=key,
|
||||
deprecated_env=key_env,
|
||||
replacement_option=replace,
|
||||
replacement_env=replace_env,
|
||||
message=msg,
|
||||
).save()
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
"""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.configured():
|
||||
return None
|
||||
self.check_expired()
|
||||
try:
|
||||
return self.reader.asn(ip_address)
|
||||
except (GeoIP2Error, ValueError):
|
||||
return None
|
||||
|
||||
def asn_to_dict(self, asn: ASN | None) -> ASNDict:
|
||||
"""Convert ASN to dict"""
|
||||
if not asn:
|
||||
return {}
|
||||
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()
|
|
@ -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
|
|
@ -0,0 +1,86 @@
|
|||
"""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.configured():
|
||||
return None
|
||||
self.check_expired()
|
||||
try:
|
||||
return self.reader.city(ip_address)
|
||||
except (GeoIP2Error, ValueError):
|
||||
return None
|
||||
|
||||
def city_to_dict(self, city: City | None) -> GeoIPDict:
|
||||
"""Convert City to dict"""
|
||||
if not city:
|
||||
return {}
|
||||
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()
|
|
@ -0,0 +1,53 @@
|
|||
"""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", path=path, 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)
|
||||
|
||||
def configured(self) -> bool:
|
||||
"""Return true if this context processor is configured"""
|
||||
return bool(self.reader)
|
|
@ -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()
|
|
@ -28,7 +28,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,
|
||||
|
@ -249,21 +249,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(
|
||||
|
@ -470,7 +464,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,
|
||||
|
|
|
@ -45,9 +45,14 @@ def get_login_event(request: HttpRequest) -> Optional[Event]:
|
|||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
|
||||
def on_user_logged_out(sender, request: HttpRequest, user: User, **kwargs):
|
||||
"""Log successfully logout"""
|
||||
Event.new(EventAction.LOGOUT).from_http(request, user=user)
|
||||
# Check if this even comes from the user_login stage's middleware, which will set an extra
|
||||
# argument
|
||||
event = Event.new(EventAction.LOGOUT)
|
||||
if "event_extra" in kwargs:
|
||||
event.context.update(kwargs["event_extra"])
|
||||
event.from_http(request, user=user)
|
||||
|
||||
|
||||
@receiver(user_write)
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
)
|
|
@ -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"""
|
|
@ -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):
|
||||
|
|
|
@ -36,6 +36,7 @@ REDIS_ENV_KEYS = [
|
|||
|
||||
# Old key -> new key
|
||||
DEPRECATIONS = {
|
||||
"geoip": "events.context_processors.geoip",
|
||||
"redis.broker_url": "broker.url",
|
||||
"redis.broker_transport_options": "broker.transport_options",
|
||||
"redis.cache_timeout": "cache.timeout",
|
||||
|
@ -113,6 +114,8 @@ class ConfigLoader:
|
|||
|
||||
A variable like AUTHENTIK_POSTGRESQL__HOST would translate to postgresql.host"""
|
||||
|
||||
deprecations: dict[tuple[str, str], str] = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__()
|
||||
self.__config = {}
|
||||
|
@ -139,9 +142,9 @@ class ConfigLoader:
|
|||
self.update_from_file(env_file)
|
||||
self.update_from_env()
|
||||
self.update(self.__config, kwargs)
|
||||
self.check_deprecations()
|
||||
self.deprecations = self.check_deprecations()
|
||||
|
||||
def check_deprecations(self):
|
||||
def check_deprecations(self) -> dict[str, str]:
|
||||
"""Warn if any deprecated configuration options are used"""
|
||||
|
||||
def _pop_deprecated_key(current_obj, dot_parts, index):
|
||||
|
@ -154,8 +157,10 @@ class ConfigLoader:
|
|||
current_obj.pop(dot_part)
|
||||
return value
|
||||
|
||||
deprecation_replacements = {}
|
||||
for deprecation, replacement in DEPRECATIONS.items():
|
||||
if self.get(deprecation, default=UNSET) is not UNSET:
|
||||
if self.get(deprecation, default=UNSET) is UNSET:
|
||||
continue
|
||||
message = (
|
||||
f"'{deprecation}' has been deprecated in favor of '{replacement}'! "
|
||||
+ "Please update your configuration."
|
||||
|
@ -164,15 +169,11 @@ class ConfigLoader:
|
|||
"warning",
|
||||
message,
|
||||
)
|
||||
try:
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
Event.new(EventAction.CONFIGURATION_ERROR, message=message).save()
|
||||
except ImportError:
|
||||
continue
|
||||
deprecation_replacements[(deprecation, replacement)] = message
|
||||
|
||||
deprecated_attr = _pop_deprecated_key(self.__config, deprecation.split("."), 0)
|
||||
self.set(replacement, deprecated_attr.value)
|
||||
self.set(replacement, deprecated_attr)
|
||||
return deprecation_replacements
|
||||
|
||||
def log(self, level: str, message: str, **kwargs):
|
||||
"""Custom Log method, we want to ensure ConfigLoader always logs JSON even when
|
||||
|
@ -319,7 +320,9 @@ class ConfigLoader:
|
|||
|
||||
def set(self, path: str, value: Any, sep="."):
|
||||
"""Set value using same syntax as get()"""
|
||||
set_path_in_dict(self.raw, path, Attr(value), sep=sep)
|
||||
if not isinstance(value, Attr):
|
||||
value = Attr(value)
|
||||
set_path_in_dict(self.raw, path, value, sep=sep)
|
||||
|
||||
|
||||
CONFIG = ConfigLoader()
|
||||
|
|
|
@ -104,7 +104,10 @@ reputation:
|
|||
cookie_domain: null
|
||||
disable_update_check: false
|
||||
disable_startup_analytics: false
|
||||
geoip: "/geoip/GeoLite2-City.mmdb"
|
||||
events:
|
||||
context_processors:
|
||||
geoip: "/geoip/GeoLite2-City.mmdb"
|
||||
asn: "/geoip/GeoLite2-ASN.mmdb"
|
||||
|
||||
cert_discovery_dir: /certs
|
||||
default_token_length: 60
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
"""http helpers"""
|
||||
from requests.sessions import Session
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from requests.sessions import PreparedRequest, Session
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import get_full_version
|
||||
|
@ -12,8 +15,25 @@ def authentik_user_agent() -> str:
|
|||
return f"authentik@{get_full_version()}"
|
||||
|
||||
|
||||
class DebugSession(Session):
|
||||
"""requests session which logs http requests and responses"""
|
||||
|
||||
def send(self, req: PreparedRequest, *args, **kwargs):
|
||||
request_id = str(uuid4())
|
||||
LOGGER.debug("HTTP request sent", uid=request_id, path=req.path_url, headers=req.headers)
|
||||
resp = super().send(req, *args, **kwargs)
|
||||
LOGGER.debug(
|
||||
"HTTP response received",
|
||||
uid=request_id,
|
||||
status=resp.status_code,
|
||||
body=resp.text,
|
||||
headers=resp.headers,
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
def get_http_session() -> Session:
|
||||
"""Get a requests session with common headers"""
|
||||
session = Session()
|
||||
session = DebugSession() if settings.DEBUG else Session()
|
||||
session.headers["User-Agent"] = authentik_user_agent()
|
||||
return session
|
||||
|
|
|
@ -47,6 +47,7 @@ class ReputationSerializer(ModelSerializer):
|
|||
"identifier",
|
||||
"ip",
|
||||
"ip_geo_data",
|
||||
"ip_asn_data",
|
||||
"score",
|
||||
"updated",
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 5.0 on 2023-12-22 23:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentik_providers_oauth2", "0016_alter_refreshtoken_token"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="accesstoken",
|
||||
name="session_id",
|
||||
field=models.CharField(blank=True, default=""),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="authorizationcode",
|
||||
name="session_id",
|
||||
field=models.CharField(blank=True, default=""),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="refreshtoken",
|
||||
name="session_id",
|
||||
field=models.CharField(blank=True, default=""),
|
||||
),
|
||||
]
|
|
@ -296,6 +296,7 @@ class BaseGrantModel(models.Model):
|
|||
revoked = models.BooleanField(default=False)
|
||||
_scope = models.TextField(default="", verbose_name=_("Scopes"))
|
||||
auth_time = models.DateTimeField(verbose_name="Authentication time")
|
||||
session_id = models.CharField(default="", blank=True)
|
||||
|
||||
@property
|
||||
def scope(self) -> list[str]:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""authentik OAuth2 Authorization views"""
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from hashlib import sha256
|
||||
from json import dumps
|
||||
from re import error as RegexError
|
||||
from re import fullmatch
|
||||
|
@ -282,6 +283,7 @@ class OAuthAuthorizationParams:
|
|||
expires=now + timedelta_from_string(self.provider.access_code_validity),
|
||||
scope=self.scope,
|
||||
nonce=self.nonce,
|
||||
session_id=sha256(request.session.session_key.encode("ascii")).hexdigest(),
|
||||
)
|
||||
|
||||
if self.code_challenge and self.code_challenge_method:
|
||||
|
@ -569,6 +571,7 @@ class OAuthFulfillmentStage(StageView):
|
|||
expires=access_token_expiry,
|
||||
provider=self.provider,
|
||||
auth_time=auth_event.created if auth_event else now,
|
||||
session_id=sha256(self.request.session.session_key.encode("ascii")).hexdigest(),
|
||||
)
|
||||
|
||||
id_token = IDToken.new(self.provider, token, self.request)
|
||||
|
|
|
@ -487,6 +487,7 @@ class TokenView(View):
|
|||
# Keep same scopes as previous token
|
||||
scope=self.params.authorization_code.scope,
|
||||
auth_time=self.params.authorization_code.auth_time,
|
||||
session_id=self.params.authorization_code.session_id,
|
||||
)
|
||||
access_token.id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
@ -502,6 +503,7 @@ class TokenView(View):
|
|||
expires=refresh_token_expiry,
|
||||
provider=self.provider,
|
||||
auth_time=self.params.authorization_code.auth_time,
|
||||
session_id=self.params.authorization_code.session_id,
|
||||
)
|
||||
id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
@ -539,6 +541,7 @@ class TokenView(View):
|
|||
# Keep same scopes as previous token
|
||||
scope=self.params.refresh_token.scope,
|
||||
auth_time=self.params.refresh_token.auth_time,
|
||||
session_id=self.params.refresh_token.session_id,
|
||||
)
|
||||
access_token.id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
@ -554,6 +557,7 @@ class TokenView(View):
|
|||
expires=refresh_token_expiry,
|
||||
provider=self.provider,
|
||||
auth_time=self.params.refresh_token.auth_time,
|
||||
session_id=self.params.refresh_token.session_id,
|
||||
)
|
||||
id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
"""proxy provider tasks"""
|
||||
from hashlib import sha256
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.db import DatabaseError, InternalError, ProgrammingError
|
||||
|
@ -23,6 +25,7 @@ def proxy_set_defaults():
|
|||
def proxy_on_logout(session_id: str):
|
||||
"""Update outpost instances connected to a single outpost"""
|
||||
layer = get_channel_layer()
|
||||
hashed_session_id = sha256(session_id.encode("ascii")).hexdigest()
|
||||
for outpost in Outpost.objects.filter(type=OutpostType.PROXY):
|
||||
group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
|
||||
async_to_sync(layer.group_send)(
|
||||
|
@ -30,6 +33,6 @@ def proxy_on_logout(session_id: str):
|
|||
{
|
||||
"type": "event.provider.specific",
|
||||
"sub_type": "logout",
|
||||
"session_id": session_id,
|
||||
"session_id": hashed_session_id,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -47,6 +47,10 @@ 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"))
|
||||
|
|
|
@ -56,7 +56,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
|||
pass
|
||||
return session_key
|
||||
|
||||
def process_request(self, request):
|
||||
def process_request(self, request: HttpRequest):
|
||||
raw_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
||||
session_key = SessionMiddleware.decode_session_key(raw_session)
|
||||
request.session = self.SessionStore(session_key)
|
||||
|
@ -297,7 +297,7 @@ class LoggingMiddleware:
|
|||
response = self.get_response(request)
|
||||
status_code = response.status_code
|
||||
kwargs = {
|
||||
"request_id": request.request_id,
|
||||
"request_id": getattr(request, "request_id", None),
|
||||
}
|
||||
kwargs.update(getattr(response, "ak_context", {}))
|
||||
self.log(request, status_code, int((default_timer() - start) * 1000), **kwargs)
|
||||
|
|
|
@ -230,7 +230,7 @@ MIDDLEWARE = [
|
|||
"authentik.root.middleware.LoggingMiddleware",
|
||||
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||
"authentik.root.middleware.ClientIPMiddleware",
|
||||
"authentik.root.middleware.SessionMiddleware",
|
||||
"authentik.stages.user_login.middleware.BoundSessionMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"authentik.core.middleware.RequestIDMiddleware",
|
||||
"authentik.brands.middleware.BrandMiddleware",
|
||||
|
|
|
@ -31,7 +31,8 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
|
|||
|
||||
settings.TEST = True
|
||||
settings.CELERY["task_always_eager"] = True
|
||||
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",
|
||||
|
|
|
@ -4,7 +4,11 @@ from re import split
|
|||
from typing import Optional
|
||||
|
||||
from ldap3 import BASE
|
||||
from ldap3.core.exceptions import LDAPAttributeError, LDAPUnwillingToPerformResult
|
||||
from ldap3.core.exceptions import (
|
||||
LDAPAttributeError,
|
||||
LDAPNoSuchAttributeResult,
|
||||
LDAPUnwillingToPerformResult,
|
||||
)
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
|
@ -97,7 +101,7 @@ class LDAPPasswordChanger:
|
|||
return
|
||||
try:
|
||||
self._connection.extend.microsoft.modify_password(user_dn, password)
|
||||
except (LDAPAttributeError, LDAPUnwillingToPerformResult):
|
||||
except (LDAPAttributeError, LDAPUnwillingToPerformResult, LDAPNoSuchAttributeResult):
|
||||
self._connection.extend.standard.modify_password(user_dn, new_password=password)
|
||||
|
||||
def _ad_check_password_existing(self, password: str, user_dn: str) -> bool:
|
||||
|
|
|
@ -4,8 +4,8 @@ from typing import Any
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
||||
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
@ -20,7 +20,7 @@ class AzureADOAuthRedirect(OAuthRedirect):
|
|||
}
|
||||
|
||||
|
||||
class AzureADOAuthCallback(OAuthCallback):
|
||||
class AzureADOAuthCallback(OpenIDConnectOAuth2Callback):
|
||||
"""AzureAD OAuth2 Callback"""
|
||||
|
||||
client_class = UserprofileHeaderAuthClient
|
||||
|
@ -50,7 +50,7 @@ class AzureADType(SourceType):
|
|||
|
||||
authorization_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
||||
access_token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token" # nosec
|
||||
profile_url = "https://graph.microsoft.com/v1.0/me"
|
||||
profile_url = "https://login.microsoftonline.com/common/openid/userinfo"
|
||||
oidc_well_known_url = (
|
||||
"https://login.microsoftonline.com/common/.well-known/openid-configuration"
|
||||
)
|
||||
|
|
|
@ -23,7 +23,7 @@ class OpenIDConnectOAuth2Callback(OAuthCallback):
|
|||
client_class = UserprofileHeaderAuthClient
|
||||
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
return info.get("sub", "")
|
||||
return info.get("sub", None)
|
||||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
|
|
|
@ -3,8 +3,8 @@ from typing import Any
|
|||
|
||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@ class OktaOAuthRedirect(OAuthRedirect):
|
|||
}
|
||||
|
||||
|
||||
class OktaOAuth2Callback(OAuthCallback):
|
||||
class OktaOAuth2Callback(OpenIDConnectOAuth2Callback):
|
||||
"""Okta OAuth2 Callback"""
|
||||
|
||||
# Okta has the same quirk as azure and throws an error if the access token
|
||||
|
@ -25,9 +25,6 @@ class OktaOAuth2Callback(OAuthCallback):
|
|||
# see https://github.com/goauthentik/authentik/issues/1910
|
||||
client_class = UserprofileHeaderAuthClient
|
||||
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
return info.get("sub", "")
|
||||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
info: dict[str, Any],
|
||||
|
|
|
@ -3,8 +3,8 @@ from json import dumps
|
|||
from typing import Any, Optional
|
||||
|
||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
||||
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
|
||||
|
@ -27,14 +27,11 @@ class TwitchOAuthRedirect(OAuthRedirect):
|
|||
}
|
||||
|
||||
|
||||
class TwitchOAuth2Callback(OAuthCallback):
|
||||
class TwitchOAuth2Callback(OpenIDConnectOAuth2Callback):
|
||||
"""Twitch OAuth2 Callback"""
|
||||
|
||||
client_class = TwitchClient
|
||||
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
return info.get("sub", "")
|
||||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
info: dict[str, Any],
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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.
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -15,6 +15,8 @@ class UserLoginStageSerializer(StageSerializer):
|
|||
"session_duration",
|
||||
"terminate_other_sessions",
|
||||
"remember_me_offset",
|
||||
"network_binding",
|
||||
"geoip_binding",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
"""Sessions bound to ASN/Network and GeoIP/Continent/etc"""
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
from django.http.request import HttpRequest
|
||||
from django.shortcuts import redirect
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import AuthenticatedSession
|
||||
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
|
||||
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.root.middleware import ClientIPMiddleware, SessionMiddleware
|
||||
from authentik.stages.user_login.models import GeoIPBinding, NetworkBinding
|
||||
|
||||
SESSION_KEY_LAST_IP = "authentik/stages/user_login/last_ip"
|
||||
SESSION_KEY_BINDING_NET = "authentik/stages/user_login/binding/net"
|
||||
SESSION_KEY_BINDING_GEO = "authentik/stages/user_login/binding/geo"
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class SessionBindingBroken(SentryIgnoredException):
|
||||
"""Session binding was broken due to specified `reason`"""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self, reason: str, old_value: str, new_value: str, old_ip: str, new_ip: str
|
||||
) -> None:
|
||||
self.reason = reason
|
||||
self.old_value = old_value
|
||||
self.new_value = new_value
|
||||
self.old_ip = old_ip
|
||||
self.new_ip = new_ip
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Session binding broken due to {self.reason}; "
|
||||
f"old value: {self.old_value}, new value: {self.new_value}"
|
||||
)
|
||||
|
||||
def to_event(self) -> dict:
|
||||
"""Convert to dict for usage with event"""
|
||||
return {
|
||||
"logout_reason": "Session binding broken",
|
||||
"binding": {
|
||||
"reason": self.reason,
|
||||
"previous_value": self.old_value,
|
||||
"new_value": self.new_value,
|
||||
},
|
||||
"ip": {
|
||||
"previous": self.old_ip,
|
||||
"new": self.new_ip,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def logout_extra(request: HttpRequest, exc: SessionBindingBroken):
|
||||
"""Similar to django's logout method, but able to carry more info to the signal"""
|
||||
# Dispatch the signal before the user is logged out so the receivers have a
|
||||
# chance to find out *who* logged out.
|
||||
user = getattr(request, "user", None)
|
||||
if not getattr(user, "is_authenticated", True):
|
||||
user = None
|
||||
user_logged_out.send(
|
||||
sender=user.__class__, request=request, user=user, event_extra=exc.to_event()
|
||||
)
|
||||
request.session.flush()
|
||||
if hasattr(request, "user"):
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
request.user = AnonymousUser()
|
||||
|
||||
|
||||
class BoundSessionMiddleware(SessionMiddleware):
|
||||
"""Sessions bound to ASN/Network and GeoIP/Continent/etc"""
|
||||
|
||||
def process_request(self, request: HttpRequest):
|
||||
super().process_request(request)
|
||||
try:
|
||||
self.recheck_session(request)
|
||||
except SessionBindingBroken as exc:
|
||||
LOGGER.warning("Session binding broken", exc=exc)
|
||||
# At this point, we need to logout the current user
|
||||
# however since this middleware has to run before the `AuthenticationMiddleware`
|
||||
# we don't have access to the user yet
|
||||
# Logout will still work, however event logs won't display the user being logged out
|
||||
AuthenticationMiddleware(lambda request: request).process_request(request)
|
||||
logout_extra(request, exc)
|
||||
request.session.clear()
|
||||
return redirect(settings.LOGIN_URL)
|
||||
return None
|
||||
|
||||
def recheck_session(self, request: HttpRequest):
|
||||
"""Check if a session is still valid with a changed IP"""
|
||||
last_ip = request.session.get(SESSION_KEY_LAST_IP)
|
||||
new_ip = ClientIPMiddleware.get_client_ip(request)
|
||||
# Check changed IP
|
||||
if new_ip == last_ip:
|
||||
return
|
||||
configured_binding_net = request.session.get(
|
||||
SESSION_KEY_BINDING_NET, NetworkBinding.NO_BINDING
|
||||
)
|
||||
configured_binding_geo = request.session.get(
|
||||
SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING
|
||||
)
|
||||
if configured_binding_net != NetworkBinding.NO_BINDING:
|
||||
self.recheck_session_net(configured_binding_net, last_ip, new_ip)
|
||||
if configured_binding_geo != GeoIPBinding.NO_BINDING:
|
||||
self.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
|
||||
# If we got to this point without any error being raised, we need to
|
||||
# update the last saved IP to the current one
|
||||
request.session[SESSION_KEY_LAST_IP] = new_ip
|
||||
AuthenticatedSession.objects.filter(session_key=request.session.session_key).update(
|
||||
last_ip=new_ip, last_user_agent=request.META.get("HTTP_USER_AGENT", "")
|
||||
)
|
||||
|
||||
def recheck_session_net(self, binding: NetworkBinding, last_ip: str, new_ip: str):
|
||||
"""Check network/ASN binding"""
|
||||
last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip)
|
||||
new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip)
|
||||
if not last_asn or not new_asn:
|
||||
raise SessionBindingBroken(
|
||||
"network.missing",
|
||||
ASN_CONTEXT_PROCESSOR.asn_to_dict(last_asn),
|
||||
ASN_CONTEXT_PROCESSOR.asn_to_dict(new_asn),
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
if binding in [
|
||||
NetworkBinding.BIND_ASN,
|
||||
NetworkBinding.BIND_ASN_NETWORK,
|
||||
NetworkBinding.BIND_ASN_NETWORK_IP,
|
||||
]:
|
||||
# Check ASN which is required for all 3 modes
|
||||
if last_asn.autonomous_system_number != new_asn.autonomous_system_number:
|
||||
raise SessionBindingBroken(
|
||||
"network.asn",
|
||||
last_asn.autonomous_system_number,
|
||||
new_asn.autonomous_system_number,
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
if binding in [NetworkBinding.BIND_ASN_NETWORK, NetworkBinding.BIND_ASN_NETWORK_IP]:
|
||||
# Check Network afterwards
|
||||
if last_asn.network != new_asn.network:
|
||||
raise SessionBindingBroken(
|
||||
"network.asn_network",
|
||||
last_asn.network,
|
||||
new_asn.network,
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
if binding in [NetworkBinding.BIND_ASN_NETWORK_IP]:
|
||||
# Only require strict IP checking
|
||||
if last_ip != new_ip:
|
||||
raise SessionBindingBroken(
|
||||
"network.ip",
|
||||
last_ip,
|
||||
new_ip,
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
|
||||
def recheck_session_geo(self, binding: GeoIPBinding, last_ip: str, new_ip: str):
|
||||
"""Check GeoIP binding"""
|
||||
last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip)
|
||||
new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip)
|
||||
if not last_geo or not new_geo:
|
||||
raise SessionBindingBroken(
|
||||
"geoip.missing",
|
||||
GEOIP_CONTEXT_PROCESSOR.city_to_dict(last_geo),
|
||||
GEOIP_CONTEXT_PROCESSOR.city_to_dict(new_geo),
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
if binding in [
|
||||
GeoIPBinding.BIND_CONTINENT,
|
||||
GeoIPBinding.BIND_CONTINENT_COUNTRY,
|
||||
GeoIPBinding.BIND_CONTINENT_COUNTRY_CITY,
|
||||
]:
|
||||
# Check Continent which is required for all 3 modes
|
||||
if last_geo.continent != new_geo.continent:
|
||||
raise SessionBindingBroken(
|
||||
"geoip.continent",
|
||||
last_geo.continent,
|
||||
new_geo.continent,
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
if binding in [
|
||||
GeoIPBinding.BIND_CONTINENT_COUNTRY,
|
||||
GeoIPBinding.BIND_CONTINENT_COUNTRY_CITY,
|
||||
]:
|
||||
# Check Country afterwards
|
||||
if last_geo.country != new_geo.country:
|
||||
raise SessionBindingBroken(
|
||||
"geoip.country",
|
||||
last_geo.country,
|
||||
new_geo.country,
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
if binding in [GeoIPBinding.BIND_CONTINENT_COUNTRY_CITY]:
|
||||
# Check city afterwards
|
||||
if last_geo.city != new_geo.city:
|
||||
raise SessionBindingBroken(
|
||||
"geoip.city",
|
||||
last_geo.city,
|
||||
new_geo.city,
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 4.2.7 on 2023-12-14 10:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentik_stages_user_login", "0005_userloginstage_remember_me_offset"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userloginstage",
|
||||
name="geoip_binding",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("no_binding", "No Binding"),
|
||||
("bind_continent", "Bind Continent"),
|
||||
("bind_continent_country", "Bind Continent Country"),
|
||||
("bind_continent_country_city", "Bind Continent Country City"),
|
||||
],
|
||||
default="no_binding",
|
||||
help_text="Bind sessions created by this stage to the configured GeoIP location",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userloginstage",
|
||||
name="network_binding",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("no_binding", "No Binding"),
|
||||
("bind_asn", "Bind Asn"),
|
||||
("bind_asn_network", "Bind Asn Network"),
|
||||
("bind_asn_network_ip", "Bind Asn Network Ip"),
|
||||
],
|
||||
default="no_binding",
|
||||
help_text="Bind sessions created by this stage to the configured network",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -9,6 +9,26 @@ from authentik.flows.models import Stage
|
|||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
|
||||
|
||||
class NetworkBinding(models.TextChoices):
|
||||
"""Network session binding modes"""
|
||||
|
||||
NO_BINDING = "no_binding"
|
||||
BIND_ASN = "bind_asn" # Bind to ASN only
|
||||
BIND_ASN_NETWORK = "bind_asn_network" # Bind to ASN and Network
|
||||
BIND_ASN_NETWORK_IP = "bind_asn_network_ip" # Bind to ASN, Network and IP
|
||||
|
||||
|
||||
class GeoIPBinding(models.TextChoices):
|
||||
"""Geo session binding modes"""
|
||||
|
||||
NO_BINDING = "no_binding"
|
||||
BIND_CONTINENT = "bind_continent" # Bind to continent only
|
||||
BIND_CONTINENT_COUNTRY = "bind_continent_country" # Bind to continent and country
|
||||
BIND_CONTINENT_COUNTRY_CITY = (
|
||||
"bind_continent_country_city" # Bind to continent, country and city
|
||||
)
|
||||
|
||||
|
||||
class UserLoginStage(Stage):
|
||||
"""Attaches the currently pending user to the current session."""
|
||||
|
||||
|
@ -21,6 +41,16 @@ class UserLoginStage(Stage):
|
|||
"(Format: hours=-1;minutes=-2;seconds=-3)"
|
||||
),
|
||||
)
|
||||
network_binding = models.TextField(
|
||||
choices=NetworkBinding.choices,
|
||||
default=NetworkBinding.NO_BINDING,
|
||||
help_text=_("Bind sessions created by this stage to the configured network"),
|
||||
)
|
||||
geoip_binding = models.TextField(
|
||||
choices=GeoIPBinding.choices,
|
||||
default=GeoIPBinding.NO_BINDING,
|
||||
help_text=_("Bind sessions created by this stage to the configured GeoIP location"),
|
||||
)
|
||||
terminate_other_sessions = models.BooleanField(
|
||||
default=False, help_text=_("Terminate all other sessions of the user logging in.")
|
||||
)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
"""Login stage logic"""
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
@ -10,8 +12,14 @@ from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUse
|
|||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_SOURCE
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.root.middleware import ClientIPMiddleware
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.user_login.middleware import (
|
||||
SESSION_KEY_BINDING_GEO,
|
||||
SESSION_KEY_BINDING_NET,
|
||||
SESSION_KEY_LAST_IP,
|
||||
)
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
|
||||
|
||||
|
@ -51,6 +59,26 @@ class UserLoginStageView(ChallengeStageView):
|
|||
def challenge_valid(self, response: UserLoginChallengeResponse) -> HttpResponse:
|
||||
return self.do_login(self.request, response.validated_data["remember_me"])
|
||||
|
||||
def set_session_duration(self, remember: bool) -> timedelta:
|
||||
"""Update the sessions' expiry"""
|
||||
delta = timedelta_from_string(self.executor.current_stage.session_duration)
|
||||
if remember:
|
||||
offset = timedelta_from_string(self.executor.current_stage.remember_me_offset)
|
||||
delta = delta + offset
|
||||
if delta.total_seconds() == 0:
|
||||
self.request.session.set_expiry(0)
|
||||
else:
|
||||
self.request.session.set_expiry(delta)
|
||||
return delta
|
||||
|
||||
def set_session_ip(self):
|
||||
"""Set the sessions' last IP and session bindings"""
|
||||
stage: UserLoginStage = self.executor.current_stage
|
||||
|
||||
self.request.session[SESSION_KEY_LAST_IP] = ClientIPMiddleware.get_client_ip(self.request)
|
||||
self.request.session[SESSION_KEY_BINDING_NET] = stage.network_binding
|
||||
self.request.session[SESSION_KEY_BINDING_GEO] = stage.geoip_binding
|
||||
|
||||
def do_login(self, request: HttpRequest, remember: bool = False) -> HttpResponse:
|
||||
"""Attach the currently pending user to the current session"""
|
||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||
|
@ -64,14 +92,8 @@ class UserLoginStageView(ChallengeStageView):
|
|||
user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||
if not user.is_active:
|
||||
self.logger.warning("User is not active, login will not work.")
|
||||
delta = timedelta_from_string(self.executor.current_stage.session_duration)
|
||||
if remember:
|
||||
offset = timedelta_from_string(self.executor.current_stage.remember_me_offset)
|
||||
delta = delta + offset
|
||||
if delta.total_seconds() == 0:
|
||||
self.request.session.set_expiry(0)
|
||||
else:
|
||||
self.request.session.set_expiry(delta)
|
||||
delta = self.set_session_duration(remember)
|
||||
self.set_session_ip()
|
||||
login(
|
||||
self.request,
|
||||
user,
|
||||
|
|
|
@ -3874,6 +3874,11 @@
|
|||
"additionalProperties": true,
|
||||
"title": "Ip geo data"
|
||||
},
|
||||
"ip_asn_data": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Ip asn data"
|
||||
},
|
||||
"score": {
|
||||
"type": "integer",
|
||||
"minimum": -9223372036854775808,
|
||||
|
@ -5419,13 +5424,13 @@
|
|||
"type": "string",
|
||||
"enum": [
|
||||
"apple",
|
||||
"openidconnect",
|
||||
"azuread",
|
||||
"discord",
|
||||
"facebook",
|
||||
"github",
|
||||
"google",
|
||||
"mailcow",
|
||||
"openidconnect",
|
||||
"okta",
|
||||
"patreon",
|
||||
"reddit",
|
||||
|
@ -8286,6 +8291,28 @@
|
|||
"minLength": 1,
|
||||
"title": "Remember me offset",
|
||||
"description": "Offset the session will be extended by when the user picks the remember me option. Default of 0 means that the remember me option will not be shown. (Format: hours=-1;minutes=-2;seconds=-3)"
|
||||
},
|
||||
"network_binding": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"no_binding",
|
||||
"bind_asn",
|
||||
"bind_asn_network",
|
||||
"bind_asn_network_ip"
|
||||
],
|
||||
"title": "Network binding",
|
||||
"description": "Bind sessions created by this stage to the configured network"
|
||||
},
|
||||
"geoip_binding": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"no_binding",
|
||||
"bind_continent",
|
||||
"bind_continent_country",
|
||||
"bind_continent_country_city"
|
||||
],
|
||||
"title": "Geoip binding",
|
||||
"description": "Bind sessions created by this stage to the configured GeoIP location"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
|
|
@ -14,8 +14,11 @@ entries:
|
|||
expression: |
|
||||
# This mapping is used by the authentik proxy. It passes extra user attributes,
|
||||
# which are used for example for the HTTP-Basic Authentication mapping.
|
||||
session_id = None
|
||||
if "token" in request.context:
|
||||
session_id = request.context.get("token").session_id
|
||||
return {
|
||||
"sid": request.http_request.session.session_key,
|
||||
"sid": session_id,
|
||||
"ak_proxy": {
|
||||
"user_attributes": request.user.group_attributes(request),
|
||||
"is_superuser": request.user.is_superuser,
|
||||
|
|
|
@ -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
4
go.mod
|
@ -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.2023105.2
|
||||
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
8
go.sum
|
@ -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.2023105.2 h1:ZUblqN5LidnCSlEZ/L19h7OnwppnAA3m5AGC7wUN0Ew=
|
||||
goauthentik.io/api/v3 v3.2023105.2/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=
|
||||
|
|
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2023.10.4"
|
||||
const VERSION = "2023.10.5"
|
||||
|
|
|
@ -36,6 +36,7 @@ func (ps *ProxyServer) handleWSMessage(ctx context.Context, args map[string]inte
|
|||
switch msg.SubType {
|
||||
case WSProviderSubTypeLogout:
|
||||
for _, p := range ps.apps {
|
||||
ps.log.WithField("provider", p.Host).Debug("Logging out")
|
||||
err := p.Logout(ctx, func(c application.Claims) bool {
|
||||
return c.Sid == msg.SessionID
|
||||
})
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-12-20 08:41+0000\n"
|
||||
"POT-Creation-Date: 2023-12-27 10:56+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -343,7 +343,7 @@ msgid "Powered by authentik"
|
|||
msgstr ""
|
||||
|
||||
#: authentik/core/views/apps.py:53
|
||||
#: authentik/providers/oauth2/views/authorize.py:393
|
||||
#: authentik/providers/oauth2/views/authorize.py:395
|
||||
#: authentik/providers/oauth2/views/device_init.py:70
|
||||
#: authentik/providers/saml/views/sso.py:70
|
||||
#, python-format
|
||||
|
@ -388,105 +388,105 @@ msgstr ""
|
|||
msgid "License Usage Records"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:295
|
||||
#: authentik/events/models.py:289
|
||||
msgid "Event"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:296
|
||||
#: authentik/events/models.py:290
|
||||
msgid "Events"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:302
|
||||
#: authentik/events/models.py:296
|
||||
msgid "authentik inbuilt notifications"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:303
|
||||
#: authentik/events/models.py:297
|
||||
msgid "Generic Webhook"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:304
|
||||
#: authentik/events/models.py:298
|
||||
msgid "Slack Webhook (Slack/Discord)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:305
|
||||
#: authentik/events/models.py:299
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:323
|
||||
#: authentik/events/models.py:317
|
||||
msgid ""
|
||||
"Only send notification once, for example when sending a webhook into a chat "
|
||||
"channel."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:388
|
||||
#: authentik/events/models.py:382
|
||||
msgid "Severity"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:393
|
||||
#: authentik/events/models.py:387
|
||||
msgid "Dispatched for user"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:402
|
||||
#: authentik/events/models.py:396
|
||||
msgid "Event user"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:496
|
||||
#: authentik/events/models.py:490
|
||||
msgid "Notification Transport"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:497
|
||||
#: authentik/events/models.py:491
|
||||
msgid "Notification Transports"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:503
|
||||
#: authentik/events/models.py:497
|
||||
msgid "Notice"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:504
|
||||
#: authentik/events/models.py:498
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:505
|
||||
#: authentik/events/models.py:499
|
||||
msgid "Alert"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:530
|
||||
#: authentik/events/models.py:524
|
||||
msgid "Notification"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:531
|
||||
#: authentik/events/models.py:525
|
||||
msgid "Notifications"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:541
|
||||
#: authentik/events/models.py:535
|
||||
msgid ""
|
||||
"Select which transports should be used to notify the user. If none are "
|
||||
"selected, the notification will only be shown in the authentik UI."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:549
|
||||
#: authentik/events/models.py:543
|
||||
msgid "Controls which severity level the created notifications will have."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:554
|
||||
#: authentik/events/models.py:548
|
||||
msgid ""
|
||||
"Define which group of users this notification should be sent and shown to. "
|
||||
"If left empty, Notification won't ben sent."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:572
|
||||
#: authentik/events/models.py:566
|
||||
msgid "Notification Rule"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:573
|
||||
#: authentik/events/models.py:567
|
||||
msgid "Notification Rules"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:593
|
||||
#: authentik/events/models.py:587
|
||||
msgid "Webhook Mapping"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py:594
|
||||
#: authentik/events/models.py:588
|
||||
msgid "Webhook Mappings"
|
||||
msgstr ""
|
||||
|
||||
|
@ -920,11 +920,11 @@ msgstr ""
|
|||
msgid "Reputation Policies"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/policies/reputation/models.py:95
|
||||
#: authentik/policies/reputation/models.py:96
|
||||
msgid "Reputation Score"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/policies/reputation/models.py:96
|
||||
#: authentik/policies/reputation/models.py:97
|
||||
msgid "Reputation Scores"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1187,63 +1187,63 @@ msgid "OAuth2/OpenID Providers"
|
|||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:297
|
||||
#: authentik/providers/oauth2/models.py:429
|
||||
#: authentik/providers/oauth2/models.py:430
|
||||
msgid "Scopes"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:316
|
||||
#: authentik/providers/oauth2/models.py:317
|
||||
msgid "Code"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:317
|
||||
#: authentik/providers/oauth2/models.py:318
|
||||
msgid "Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:318
|
||||
#: authentik/providers/oauth2/models.py:319
|
||||
msgid "Code Challenge"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:320
|
||||
#: authentik/providers/oauth2/models.py:321
|
||||
msgid "Code Challenge Method"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:340
|
||||
#: authentik/providers/oauth2/models.py:341
|
||||
msgid "Authorization Code"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:341
|
||||
#: authentik/providers/oauth2/models.py:342
|
||||
msgid "Authorization Codes"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:383
|
||||
#: authentik/providers/oauth2/models.py:384
|
||||
msgid "OAuth2 Access Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:384
|
||||
#: authentik/providers/oauth2/models.py:385
|
||||
msgid "OAuth2 Access Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:394
|
||||
#: authentik/providers/oauth2/models.py:395
|
||||
msgid "ID Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:413
|
||||
#: authentik/providers/oauth2/models.py:414
|
||||
msgid "OAuth2 Refresh Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:414
|
||||
#: authentik/providers/oauth2/models.py:415
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:441
|
||||
#: authentik/providers/oauth2/models.py:442
|
||||
msgid "Device Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py:442
|
||||
#: authentik/providers/oauth2/models.py:443
|
||||
msgid "Device Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/views/authorize.py:448
|
||||
#: authentik/providers/oauth2/views/authorize.py:450
|
||||
#: authentik/providers/saml/views/flows.py:87
|
||||
#, python-format
|
||||
msgid "Redirecting to %(app)s..."
|
||||
|
@ -1494,59 +1494,59 @@ msgstr ""
|
|||
msgid "SAML Property Mappings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:20
|
||||
#: authentik/providers/scim/models.py:23
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:21
|
||||
#: authentik/providers/scim/models.py:24
|
||||
msgid "Authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:27 authentik/sources/ldap/models.py:98
|
||||
#: authentik/providers/scim/models.py:30 authentik/sources/ldap/models.py:98
|
||||
msgid "Property mappings used for group creation/updating."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:60
|
||||
#: authentik/providers/scim/models.py:72
|
||||
msgid "SCIM Provider"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:61
|
||||
#: authentik/providers/scim/models.py:73
|
||||
msgid "SCIM Providers"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:81
|
||||
#: authentik/providers/scim/models.py:93
|
||||
msgid "SCIM Mapping"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py:82
|
||||
#: authentik/providers/scim/models.py:94
|
||||
msgid "SCIM Mappings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/tasks.py:52
|
||||
#: authentik/providers/scim/tasks.py:56
|
||||
msgid "Starting full SCIM sync"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/tasks.py:62
|
||||
#, python-format
|
||||
msgid "Syncing page %(page)d of users"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/tasks.py:66
|
||||
#, python-format
|
||||
msgid "Syncing page %(page)d of users"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/tasks.py:70
|
||||
#, python-format
|
||||
msgid "Syncing page %(page)d of groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/tasks.py:98
|
||||
#: authentik/providers/scim/tasks.py:102
|
||||
#, python-format
|
||||
msgid "Failed to sync user %(user_name)s due to remote error: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/tasks.py:109 authentik/providers/scim/tasks.py:150
|
||||
#: authentik/providers/scim/tasks.py:113 authentik/providers/scim/tasks.py:154
|
||||
#, python-format
|
||||
msgid "Stopping sync due to error: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/tasks.py:139
|
||||
#: authentik/providers/scim/tasks.py:143
|
||||
#, python-format
|
||||
msgid "Failed to sync group %(group_name)s due to remote error: %(error)s"
|
||||
msgstr ""
|
||||
|
@ -1583,6 +1583,14 @@ msgstr ""
|
|||
msgid "Can access admin interface"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/rbac/models.py:73
|
||||
msgid "Can view system settings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/rbac/models.py:74
|
||||
msgid "Can edit system settings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/recovery/management/commands/create_admin_group.py:11
|
||||
msgid "Create admin group if the default group gets deleted."
|
||||
msgstr ""
|
||||
|
@ -2245,6 +2253,7 @@ msgid "Email Successfully sent."
|
|||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/account_confirmation.html:10
|
||||
#: authentik/stages/email/templates/email/account_confirmation.txt:1
|
||||
msgid "Welcome!"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2267,6 +2276,12 @@ msgid ""
|
|||
" "
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/account_confirmation.txt:3
|
||||
msgid ""
|
||||
"We're excited to have you get started. First, you need to confirm your "
|
||||
"account. Just open the link below."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/event_notification.html:46
|
||||
#, python-format
|
||||
msgid ""
|
||||
|
@ -2276,6 +2291,25 @@ msgid ""
|
|||
" "
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/event_notification.txt:1
|
||||
msgid "Dear authentik user,"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/event_notification.txt:3
|
||||
msgid "The following notification was created:"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/event_notification.txt:8
|
||||
msgid "Additional attributes:"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/event_notification.txt:13
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"This email was sent from the notification transport %(name)s.\n"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.html:10
|
||||
#, python-format
|
||||
msgid ""
|
||||
|
@ -2301,6 +2335,26 @@ msgid ""
|
|||
" "
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.txt:1
|
||||
#, python-format
|
||||
msgid "Hi %(username)s,"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.txt:3
|
||||
msgid ""
|
||||
"\n"
|
||||
"You recently requested to change your password for your authentik account. "
|
||||
"Use the link below to set a new password.\n"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.txt:7
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"If you did not request a password change, please ignore this Email. The link "
|
||||
"above is valid for %(expires)s.\n"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/setup.html:9
|
||||
msgid "authentik Test-Email"
|
||||
msgstr ""
|
||||
|
@ -2313,6 +2367,13 @@ msgid ""
|
|||
" "
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/templates/email/setup.txt:2
|
||||
msgid ""
|
||||
"\n"
|
||||
"This is a test email to inform you, that you've successfully configured "
|
||||
"authentik emails.\n"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/identification/api.py:20
|
||||
msgid "When no user fields are selected, at least one source must be selected"
|
||||
msgstr ""
|
||||
|
@ -2557,36 +2618,44 @@ msgstr ""
|
|||
msgid "No Pending User."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/models.py:19
|
||||
#: authentik/stages/user_login/models.py:39
|
||||
msgid ""
|
||||
"Determines how long a session lasts. Default of 0 means that the sessions "
|
||||
"lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/models.py:25
|
||||
#: authentik/stages/user_login/models.py:47
|
||||
msgid "Bind sessions created by this stage to the configured network"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/models.py:52
|
||||
msgid "Bind sessions created by this stage to the configured GeoIP location"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/models.py:55
|
||||
msgid "Terminate all other sessions of the user logging in."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/models.py:31
|
||||
#: authentik/stages/user_login/models.py:61
|
||||
msgid ""
|
||||
"Offset the session will be extended by when the user picks the remember me "
|
||||
"option. Default of 0 means that the remember me option will not be shown. "
|
||||
"(Format: hours=-1;minutes=-2;seconds=-3)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/models.py:54
|
||||
#: authentik/stages/user_login/models.py:84
|
||||
msgid "User Login Stage"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/models.py:55
|
||||
#: authentik/stages/user_login/models.py:85
|
||||
msgid "User Login Stages"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/stage.py:57
|
||||
#: authentik/stages/user_login/stage.py:85
|
||||
msgid "No Pending user to login."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/stage.py:90
|
||||
#: authentik/stages/user_login/stage.py:112
|
||||
msgid "Successfully logged in!"
|
||||
msgstr ""
|
||||
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -17,12 +17,12 @@ pythonPlatform = "All"
|
|||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py311']
|
||||
target-version = ['py312']
|
||||
exclude = 'node_modules'
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
target-version = "py312"
|
||||
exclude = ["**/migrations/**", "**/node_modules/**"]
|
||||
|
||||
[tool.isort]
|
||||
|
@ -113,7 +113,7 @@ filterwarnings = [
|
|||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2023.10.4"
|
||||
version = "2023.10.5"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
|
163
schema.yml
163
schema.yml
|
@ -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
|
||||
|
@ -17164,7 +17164,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Task'
|
||||
$ref: '#/components/schemas/SCIMSyncStatus'
|
||||
description: ''
|
||||
'404':
|
||||
description: Task not found
|
||||
|
@ -27047,10 +27047,42 @@ paths:
|
|||
operationId: stages_user_login_list
|
||||
description: UserLoginStage Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: geoip_binding
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- bind_continent
|
||||
- bind_continent_country
|
||||
- bind_continent_country_city
|
||||
- no_binding
|
||||
description: |-
|
||||
Bind sessions created by this stage to the configured GeoIP location
|
||||
|
||||
* `no_binding` - No Binding
|
||||
* `bind_continent` - Bind Continent
|
||||
* `bind_continent_country` - Bind Continent Country
|
||||
* `bind_continent_country_city` - Bind Continent Country City
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: network_binding
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- bind_asn
|
||||
- bind_asn_network
|
||||
- bind_asn_network_ip
|
||||
- no_binding
|
||||
description: |-
|
||||
Bind sessions created by this stage to the configured network
|
||||
|
||||
* `no_binding` - No Binding
|
||||
* `bind_asn` - Bind Asn
|
||||
* `bind_asn_network` - Bind Asn Network
|
||||
* `bind_asn_network_ip` - Bind Asn Network Ip
|
||||
- name: ordering
|
||||
required: false
|
||||
in: query
|
||||
|
@ -28802,7 +28834,7 @@ components:
|
|||
readOnly: true
|
||||
geo_ip:
|
||||
type: object
|
||||
description: Get parsed user agent
|
||||
description: Get GeoIP Data
|
||||
properties:
|
||||
continent:
|
||||
type: string
|
||||
|
@ -28824,6 +28856,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:
|
||||
|
@ -28838,6 +28888,7 @@ components:
|
|||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- asn
|
||||
- current
|
||||
- geo_ip
|
||||
- last_ip
|
||||
|
@ -29910,6 +29961,7 @@ components:
|
|||
enum:
|
||||
- can_save_media
|
||||
- can_geo_ip
|
||||
- can_asn
|
||||
- can_impersonate
|
||||
- can_debug
|
||||
- is_enterprise
|
||||
|
@ -29917,6 +29969,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
|
||||
|
@ -32475,6 +32528,18 @@ components:
|
|||
type: string
|
||||
required:
|
||||
- detail
|
||||
GeoipBindingEnum:
|
||||
enum:
|
||||
- no_binding
|
||||
- bind_continent
|
||||
- bind_continent_country
|
||||
- bind_continent_country_city
|
||||
type: string
|
||||
description: |-
|
||||
* `no_binding` - No Binding
|
||||
* `bind_continent` - Bind Continent
|
||||
* `bind_continent_country` - Bind Continent Country
|
||||
* `bind_continent_country_city` - Bind Continent Country City
|
||||
Group:
|
||||
type: object
|
||||
description: Group Serializer
|
||||
|
@ -34055,6 +34120,18 @@ components:
|
|||
* `urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName` - X509
|
||||
* `urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName` - Windows
|
||||
* `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` - Transient
|
||||
NetworkBindingEnum:
|
||||
enum:
|
||||
- no_binding
|
||||
- bind_asn
|
||||
- bind_asn_network
|
||||
- bind_asn_network_ip
|
||||
type: string
|
||||
description: |-
|
||||
* `no_binding` - No Binding
|
||||
* `bind_asn` - Bind Asn
|
||||
* `bind_asn_network` - Bind Asn Network
|
||||
* `bind_asn_network_ip` - Bind Asn Network Ip
|
||||
NotConfiguredActionEnum:
|
||||
enum:
|
||||
- skip
|
||||
|
@ -38810,6 +38887,26 @@ components:
|
|||
description: 'Offset the session will be extended by when the user picks
|
||||
the remember me option. Default of 0 means that the remember me option
|
||||
will not be shown. (Format: hours=-1;minutes=-2;seconds=-3)'
|
||||
network_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/NetworkBindingEnum'
|
||||
description: |-
|
||||
Bind sessions created by this stage to the configured network
|
||||
|
||||
* `no_binding` - No Binding
|
||||
* `bind_asn` - Bind Asn
|
||||
* `bind_asn_network` - Bind Asn Network
|
||||
* `bind_asn_network_ip` - Bind Asn Network Ip
|
||||
geoip_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/GeoipBindingEnum'
|
||||
description: |-
|
||||
Bind sessions created by this stage to the configured GeoIP location
|
||||
|
||||
* `no_binding` - No Binding
|
||||
* `bind_continent` - Bind Continent
|
||||
* `bind_continent_country` - Bind Continent Country
|
||||
* `bind_continent_country_city` - Bind Continent Country City
|
||||
PatchedUserLogoutStageRequest:
|
||||
type: object
|
||||
description: UserLogoutStage Serializer
|
||||
|
@ -39841,13 +39938,13 @@ components:
|
|||
ProviderTypeEnum:
|
||||
enum:
|
||||
- apple
|
||||
- openidconnect
|
||||
- azuread
|
||||
- discord
|
||||
- facebook
|
||||
- github
|
||||
- google
|
||||
- mailcow
|
||||
- openidconnect
|
||||
- okta
|
||||
- patreon
|
||||
- reddit
|
||||
|
@ -39856,13 +39953,13 @@ components:
|
|||
type: string
|
||||
description: |-
|
||||
* `apple` - Apple
|
||||
* `openidconnect` - OpenID Connect
|
||||
* `azuread` - Azure AD
|
||||
* `discord` - Discord
|
||||
* `facebook` - Facebook
|
||||
* `github` - GitHub
|
||||
* `google` - Google
|
||||
* `mailcow` - Mailcow
|
||||
* `openidconnect` - OpenID Connect
|
||||
* `okta` - Okta
|
||||
* `patreon` - Patreon
|
||||
* `reddit` - Reddit
|
||||
|
@ -40418,6 +40515,7 @@ components:
|
|||
ip:
|
||||
type: string
|
||||
ip_geo_data: {}
|
||||
ip_asn_data: {}
|
||||
score:
|
||||
type: integer
|
||||
maximum: 9223372036854775807
|
||||
|
@ -41374,6 +41472,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
|
||||
|
@ -42749,6 +42862,26 @@ components:
|
|||
description: 'Offset the session will be extended by when the user picks
|
||||
the remember me option. Default of 0 means that the remember me option
|
||||
will not be shown. (Format: hours=-1;minutes=-2;seconds=-3)'
|
||||
network_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/NetworkBindingEnum'
|
||||
description: |-
|
||||
Bind sessions created by this stage to the configured network
|
||||
|
||||
* `no_binding` - No Binding
|
||||
* `bind_asn` - Bind Asn
|
||||
* `bind_asn_network` - Bind Asn Network
|
||||
* `bind_asn_network_ip` - Bind Asn Network Ip
|
||||
geoip_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/GeoipBindingEnum'
|
||||
description: |-
|
||||
Bind sessions created by this stage to the configured GeoIP location
|
||||
|
||||
* `no_binding` - No Binding
|
||||
* `bind_continent` - Bind Continent
|
||||
* `bind_continent_country` - Bind Continent Country
|
||||
* `bind_continent_country_city` - Bind Continent Country City
|
||||
required:
|
||||
- component
|
||||
- meta_model_name
|
||||
|
@ -42781,6 +42914,26 @@ components:
|
|||
description: 'Offset the session will be extended by when the user picks
|
||||
the remember me option. Default of 0 means that the remember me option
|
||||
will not be shown. (Format: hours=-1;minutes=-2;seconds=-3)'
|
||||
network_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/NetworkBindingEnum'
|
||||
description: |-
|
||||
Bind sessions created by this stage to the configured network
|
||||
|
||||
* `no_binding` - No Binding
|
||||
* `bind_asn` - Bind Asn
|
||||
* `bind_asn_network` - Bind Asn Network
|
||||
* `bind_asn_network_ip` - Bind Asn Network Ip
|
||||
geoip_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/GeoipBindingEnum'
|
||||
description: |-
|
||||
Bind sessions created by this stage to the configured GeoIP location
|
||||
|
||||
* `no_binding` - No Binding
|
||||
* `bind_continent` - Bind Continent
|
||||
* `bind_continent_country` - Bind Continent Country
|
||||
* `bind_continent_country_city` - Bind Continent Country City
|
||||
required:
|
||||
- name
|
||||
UserLogoutStage:
|
||||
|
|
|
@ -18,7 +18,12 @@ with open("local.env.yml", "w", encoding="utf-8") as _config:
|
|||
},
|
||||
"blueprints_dir": "./blueprints",
|
||||
"cert_discovery_dir": "./certs",
|
||||
"events": {
|
||||
"processors": {
|
||||
"geoip": "tests/GeoLite2-City-Test.mmdb",
|
||||
"asn": "tests/GeoLite2-ASN-Test.mmdb",
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"media": {
|
||||
"backend": "file",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -7,12 +7,12 @@
|
|||
"name": "@goauthentik/web-tests",
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"@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",
|
||||
|
@ -946,16 +946,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.15.0.tgz",
|
||||
"integrity": "sha512-j5qoikQqPccq9QoBAupOP+CBu8BaJ8BLjaXSioDISeTZkVO3ig7oSIKh3H+rEpee7xCXtWwSB4KIL5l6hWZzpg==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz",
|
||||
"integrity": "sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "6.15.0",
|
||||
"@typescript-eslint/type-utils": "6.15.0",
|
||||
"@typescript-eslint/utils": "6.15.0",
|
||||
"@typescript-eslint/visitor-keys": "6.15.0",
|
||||
"@typescript-eslint/scope-manager": "6.16.0",
|
||||
"@typescript-eslint/type-utils": "6.16.0",
|
||||
"@typescript-eslint/utils": "6.16.0",
|
||||
"@typescript-eslint/visitor-keys": "6.16.0",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
|
@ -981,15 +981,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.15.0.tgz",
|
||||
"integrity": "sha512-MkgKNnsjC6QwcMdlNAel24jjkEO/0hQaMDLqP4S9zq5HBAUJNQB6y+3DwLjX7b3l2b37eNAxMPLwb3/kh8VKdA==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.16.0.tgz",
|
||||
"integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.15.0",
|
||||
"@typescript-eslint/types": "6.15.0",
|
||||
"@typescript-eslint/typescript-estree": "6.15.0",
|
||||
"@typescript-eslint/visitor-keys": "6.15.0",
|
||||
"@typescript-eslint/scope-manager": "6.16.0",
|
||||
"@typescript-eslint/types": "6.16.0",
|
||||
"@typescript-eslint/typescript-estree": "6.16.0",
|
||||
"@typescript-eslint/visitor-keys": "6.16.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -1009,13 +1009,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.15.0.tgz",
|
||||
"integrity": "sha512-+BdvxYBltqrmgCNu4Li+fGDIkW9n//NrruzG9X1vBzaNK+ExVXPoGB71kneaVw/Jp+4rH/vaMAGC6JfMbHstVg==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz",
|
||||
"integrity": "sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.15.0",
|
||||
"@typescript-eslint/visitor-keys": "6.15.0"
|
||||
"@typescript-eslint/types": "6.16.0",
|
||||
"@typescript-eslint/visitor-keys": "6.16.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
|
@ -1026,13 +1026,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.15.0.tgz",
|
||||
"integrity": "sha512-CnmHKTfX6450Bo49hPg2OkIm/D/TVYV7jO1MCfPYGwf6x3GO0VU8YMO5AYMn+u3X05lRRxA4fWCz87GFQV6yVQ==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.16.0.tgz",
|
||||
"integrity": "sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "6.15.0",
|
||||
"@typescript-eslint/utils": "6.15.0",
|
||||
"@typescript-eslint/typescript-estree": "6.16.0",
|
||||
"@typescript-eslint/utils": "6.16.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
|
@ -1053,9 +1053,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.15.0.tgz",
|
||||
"integrity": "sha512-yXjbt//E4T/ee8Ia1b5mGlbNj9fB9lJP4jqLbZualwpP2BCQ5is6BcWwxpIsY4XKAhmdv3hrW92GdtJbatC6dQ==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.16.0.tgz",
|
||||
"integrity": "sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
|
@ -1066,16 +1066,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.15.0.tgz",
|
||||
"integrity": "sha512-7mVZJN7Hd15OmGuWrp2T9UvqR2Ecg+1j/Bp1jXUEY2GZKV6FXlOIoqVDmLpBiEiq3katvj/2n2mR0SDwtloCew==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz",
|
||||
"integrity": "sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.15.0",
|
||||
"@typescript-eslint/visitor-keys": "6.15.0",
|
||||
"@typescript-eslint/types": "6.16.0",
|
||||
"@typescript-eslint/visitor-keys": "6.16.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "9.0.3",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
|
@ -1092,18 +1093,42 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.15.0.tgz",
|
||||
"integrity": "sha512-eF82p0Wrrlt8fQSRL0bGXzK5nWPRV2dYQZdajcfzOD9+cQz9O7ugifrJxclB+xVOvWvagXfqS4Es7vpLP4augw==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.16.0.tgz",
|
||||
"integrity": "sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "6.15.0",
|
||||
"@typescript-eslint/types": "6.15.0",
|
||||
"@typescript-eslint/typescript-estree": "6.15.0",
|
||||
"@typescript-eslint/scope-manager": "6.16.0",
|
||||
"@typescript-eslint/types": "6.16.0",
|
||||
"@typescript-eslint/typescript-estree": "6.16.0",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -1118,12 +1143,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.15.0.tgz",
|
||||
"integrity": "sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz",
|
||||
"integrity": "sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.15.0",
|
||||
"@typescript-eslint/types": "6.16.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -1141,18 +1166,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 +1192,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 +1215,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 +1233,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 +1292,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 +1327,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 +1343,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 +1393,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 +1405,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 +8520,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 +8542,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 +8570,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"
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"@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",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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-1703290840",
|
||||
"@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.91.0",
|
||||
"@sentry/tracing": "^7.91.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^4.4.1",
|
||||
|
@ -86,19 +86,19 @@
|
|||
"@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",
|
||||
"@types/grecaptcha": "^3.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
||||
"@typescript-eslint/parser": "^6.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-tsconfig-paths": "^1.0.3",
|
||||
"cross-env": "^7.0.3",
|
||||
|
@ -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",
|
||||
|
|
|
@ -93,15 +93,16 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> {
|
|||
const health = await api.providersScimSyncStatusRetrieve({
|
||||
id: element.pk,
|
||||
});
|
||||
|
||||
if (health.status !== TaskStatusEnum.Successful) {
|
||||
health.tasks.forEach((task) => {
|
||||
if (task.status !== TaskStatusEnum.Successful) {
|
||||
sourceKey = "failed";
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const maxDelta = 3600000; // 1 hour
|
||||
if (!health || now - health.taskFinishTimestamp.getTime() > maxDelta) {
|
||||
if (!health || now - task.taskFinishTimestamp.getTime() > maxDelta) {
|
||||
sourceKey = "unsynced";
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
sourceKey = "unsynced";
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import "@goauthentik/elements/Tabs";
|
|||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
|
@ -31,7 +31,8 @@ import {
|
|||
ProvidersApi,
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
SCIMProvider,
|
||||
Task,
|
||||
SCIMSyncStatus,
|
||||
TaskStatusEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-scim-view")
|
||||
|
@ -54,7 +55,7 @@ export class SCIMProviderViewPage extends AKElement {
|
|||
provider?: SCIMProvider;
|
||||
|
||||
@state()
|
||||
syncState?: Task;
|
||||
syncState?: SCIMSyncStatus;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
|
@ -128,6 +129,41 @@ export class SCIMProviderViewPage extends AKElement {
|
|||
</ak-tabs>`;
|
||||
}
|
||||
|
||||
renderSyncStatus(): TemplateResult {
|
||||
if (!this.syncState) {
|
||||
return html`${msg("No sync status.")}`;
|
||||
}
|
||||
if (this.syncState.isRunning) {
|
||||
return html`${msg("Sync currently running.")}`;
|
||||
}
|
||||
if (this.syncState.tasks.length < 1) {
|
||||
return html`${msg("Not synced yet.")}`;
|
||||
}
|
||||
return html`
|
||||
<ul class="pf-c-list">
|
||||
${this.syncState.tasks.map((task) => {
|
||||
let header = "";
|
||||
if (task.status === TaskStatusEnum.Warning) {
|
||||
header = msg("Task finished with warnings");
|
||||
} else if (task.status === TaskStatusEnum.Error) {
|
||||
header = msg("Task finished with errors");
|
||||
} else {
|
||||
header = msg(str`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`);
|
||||
}
|
||||
return html`<li>
|
||||
<p>${task.taskName}</p>
|
||||
<ul class="pf-c-list">
|
||||
<li>${header}</li>
|
||||
${task.messages.map((m) => {
|
||||
return html`<li>${m}</li>`;
|
||||
})}
|
||||
</ul>
|
||||
</li> `;
|
||||
})}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
renderTabOverview(): TemplateResult {
|
||||
if (!this.provider) {
|
||||
return html``;
|
||||
|
@ -186,16 +222,7 @@ export class SCIMProviderViewPage extends AKElement {
|
|||
<div class="pf-c-card__title">
|
||||
<p>${msg("Sync status")}</p>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
${this.syncState
|
||||
? html` <ul class="pf-c-list">
|
||||
${this.syncState.messages.map((m) => {
|
||||
return html`<li>${m}</li>`;
|
||||
})}
|
||||
</ul>`
|
||||
: html` ${msg("Sync not run yet.")} `}
|
||||
</div>
|
||||
|
||||
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
|
|
|
@ -10,7 +10,7 @@ import { msg } from "@lit/localize";
|
|||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import { StagesApi, UserLoginStage } from "@goauthentik/api";
|
||||
import { GeoipBindingEnum, NetworkBindingEnum, StagesApi, UserLoginStage } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-user-login-form")
|
||||
export class UserLoginStageForm extends BaseStageForm<UserLoginStage> {
|
||||
|
@ -93,6 +93,74 @@ export class UserLoginStageForm extends BaseStageForm<UserLoginStage> {
|
|||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Network binding")}
|
||||
?required=${true}
|
||||
name="networkBinding"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("No binding"),
|
||||
value: NetworkBindingEnum.NoBinding,
|
||||
},
|
||||
{
|
||||
label: msg("Bind ASN"),
|
||||
value: NetworkBindingEnum.BindAsn,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("Bind ASN and Network"),
|
||||
value: NetworkBindingEnum.BindAsnNetwork,
|
||||
},
|
||||
{
|
||||
label: msg("Bind ASN, Network and IP"),
|
||||
value: NetworkBindingEnum.BindAsnNetworkIp,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.networkBinding}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure if sessions created by this stage should be bound to the Networks they were created in.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("GeoIP binding")}
|
||||
?required=${true}
|
||||
name="geoipBinding"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("No binding"),
|
||||
value: GeoipBindingEnum.NoBinding,
|
||||
},
|
||||
{
|
||||
label: msg("Bind Continent"),
|
||||
value: GeoipBindingEnum.BindContinent,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("Bind Continent and Country"),
|
||||
value: GeoipBindingEnum.BindContinentCountry,
|
||||
},
|
||||
{
|
||||
label: msg("Bind Continent, Country and City"),
|
||||
value: GeoipBindingEnum.BindContinentCountryCity,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.geoipBinding}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure if sessions created by this stage should be bound to their GeoIP-based location",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="terminateOtherSessions">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
|
|
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2023.10.4";
|
||||
export const VERSION = "2023.10.5";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
|
|
@ -285,10 +285,12 @@ export class EventInfo extends AKElement {
|
|||
}
|
||||
|
||||
renderEmailSent() {
|
||||
let body = this.event.context.body as string;
|
||||
body = body.replace("cid:logo.png", "/static/dist/assets/icons/icon_left_brand.png");
|
||||
return html`<div class="pf-c-card__title">${msg("Email info:")}</div>
|
||||
<div class="pf-c-card__body">${this.getEmailInfo(this.event.context)}</div>
|
||||
<ak-expand>
|
||||
<iframe srcdoc=${this.event.context.body}></iframe>
|
||||
<iframe srcdoc=${body}></iframe>
|
||||
</ak-expand>`;
|
||||
}
|
||||
|
||||
|
|
|
@ -1585,15 +1585,30 @@
|
|||
<trans-unit id="sea3bfc143ced73db">
|
||||
<source>NameID attribute</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f0f6691de0b0388">
|
||||
<source>Warning: Provider is not assigned to an application as backchannel provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
</trans-unit>
|
||||
|
@ -2325,24 +2340,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52b500138a2d2b8a">
|
||||
<source>Update LDAP Source</source>
|
||||
</trans-unit>
|
||||
|
@ -3848,6 +3845,39 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
<source>Terminate other sessions</source>
|
||||
</trans-unit>
|
||||
|
|
|
@ -1585,15 +1585,30 @@
|
|||
<trans-unit id="sea3bfc143ced73db">
|
||||
<source>NameID attribute</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f0f6691de0b0388">
|
||||
<source>Warning: Provider is not assigned to an application as backchannel provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
</trans-unit>
|
||||
|
@ -2325,24 +2340,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52b500138a2d2b8a">
|
||||
<source>Update LDAP Source</source>
|
||||
</trans-unit>
|
||||
|
@ -3848,6 +3845,39 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
<source>Terminate other sessions</source>
|
||||
</trans-unit>
|
||||
|
|
|
@ -1585,15 +1585,30 @@
|
|||
<trans-unit id="sea3bfc143ced73db">
|
||||
<source>NameID attribute</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f0f6691de0b0388">
|
||||
<source>Warning: Provider is not assigned to an application as backchannel provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
</trans-unit>
|
||||
|
@ -2325,24 +2340,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52b500138a2d2b8a">
|
||||
<source>Update LDAP Source</source>
|
||||
</trans-unit>
|
||||
|
@ -3848,6 +3845,39 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
<source>Terminate other sessions</source>
|
||||
</trans-unit>
|
||||
|
|
|
@ -1585,15 +1585,30 @@
|
|||
<trans-unit id="sea3bfc143ced73db">
|
||||
<source>NameID attribute</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f0f6691de0b0388">
|
||||
<source>Warning: Provider is not assigned to an application as backchannel provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
</trans-unit>
|
||||
|
@ -2325,24 +2340,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52b500138a2d2b8a">
|
||||
<source>Update LDAP Source</source>
|
||||
</trans-unit>
|
||||
|
@ -3848,6 +3845,39 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
<source>Terminate other sessions</source>
|
||||
</trans-unit>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1585,15 +1585,30 @@
|
|||
<trans-unit id="sea3bfc143ced73db">
|
||||
<source>NameID attribute</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f0f6691de0b0388">
|
||||
<source>Warning: Provider is not assigned to an application as backchannel provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
</trans-unit>
|
||||
|
@ -2325,24 +2340,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52b500138a2d2b8a">
|
||||
<source>Update LDAP Source</source>
|
||||
</trans-unit>
|
||||
|
@ -3848,6 +3845,39 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
<source>Terminate other sessions</source>
|
||||
</trans-unit>
|
||||
|
|
|
@ -1585,15 +1585,30 @@
|
|||
<trans-unit id="sea3bfc143ced73db">
|
||||
<source>NameID attribute</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f0f6691de0b0388">
|
||||
<source>Warning: Provider is not assigned to an application as backchannel provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
</trans-unit>
|
||||
|
@ -2325,24 +2340,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52b500138a2d2b8a">
|
||||
<source>Update LDAP Source</source>
|
||||
</trans-unit>
|
||||
|
@ -3848,6 +3845,39 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
<source>Terminate other sessions</source>
|
||||
</trans-unit>
|
||||
|
|
|
@ -1585,15 +1585,30 @@
|
|||
<trans-unit id="sea3bfc143ced73db">
|
||||
<source>NameID attribute</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f0f6691de0b0388">
|
||||
<source>Warning: Provider is not assigned to an application as backchannel provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
</trans-unit>
|
||||
|
@ -2325,24 +2340,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52b500138a2d2b8a">
|
||||
<source>Update LDAP Source</source>
|
||||
</trans-unit>
|
||||
|
@ -3848,6 +3845,39 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
<source>Terminate other sessions</source>
|
||||
</trans-unit>
|
||||
|
|
|
@ -1585,15 +1585,30 @@
|
|||
<trans-unit id="sea3bfc143ced73db">
|
||||
<source>NameID attribute</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f0f6691de0b0388">
|
||||
<source>Warning: Provider is not assigned to an application as backchannel provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
</trans-unit>
|
||||
|
@ -2325,24 +2340,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52b500138a2d2b8a">
|
||||
<source>Update LDAP Source</source>
|
||||
</trans-unit>
|
||||
|
@ -3848,6 +3845,39 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
<source>Terminate other sessions</source>
|
||||
</trans-unit>
|
||||
|
|
|
@ -1585,15 +1585,30 @@
|
|||
<trans-unit id="sea3bfc143ced73db">
|
||||
<source>NameID attribute</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f0f6691de0b0388">
|
||||
<source>Warning: Provider is not assigned to an application as backchannel provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
</trans-unit>
|
||||
|
@ -2325,24 +2340,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52b500138a2d2b8a">
|
||||
<source>Update LDAP Source</source>
|
||||
</trans-unit>
|
||||
|
@ -3848,6 +3845,39 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
<source>Terminate other sessions</source>
|
||||
</trans-unit>
|
||||
|
|
|
@ -1585,15 +1585,30 @@
|
|||
<trans-unit id="sea3bfc143ced73db">
|
||||
<source>NameID attribute</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f0f6691de0b0388">
|
||||
<source>Warning: Provider is not assigned to an application as backchannel provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
</trans-unit>
|
||||
|
@ -2325,24 +2340,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31a2d43bc1cf1790">
|
||||
<source>Not synced yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s388ee787bbf2271b">
|
||||
<source>Task finished with warnings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s949826fad0fe0909">
|
||||
<source>Task finished with errors</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbedb77365a066648">
|
||||
<source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52b500138a2d2b8a">
|
||||
<source>Update LDAP Source</source>
|
||||
</trans-unit>
|
||||
|
@ -3848,6 +3845,39 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
<source>Terminate other sessions</source>
|
||||
</trans-unit>
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
---
|
||||
title: "Building the dream infrastructure stack for a security startup: preparing for human and technical scaling"
|
||||
description: "What's in our stack: the tools we use to build authentik (and why we chose them)."
|
||||
slug: 2023-12-21-five-lessons-from-choosing-infrastructure-tooling
|
||||
authors:
|
||||
- name: Marc Schmitt
|
||||
title: Infrastructure Engineer at Authentik Security Inc
|
||||
url: https://github.com/rissson
|
||||
image_url: https://github.com/rissson.png
|
||||
- name: Rebecca Dodd
|
||||
title: Contributing Writer
|
||||
url: https://www.thebasementoffice.co.uk
|
||||
image_url: https://github.com/rebeccadee.png
|
||||
tags:
|
||||
- authentik
|
||||
- startups
|
||||
- infrastructure tooling
|
||||
- tools
|
||||
- technology stack
|
||||
- Loki
|
||||
- Argo CD
|
||||
- Prometheus
|
||||
- Thanos
|
||||
- Transifex
|
||||
- Lit
|
||||
- Redis
|
||||
- Grafana
|
||||
- authentication
|
||||
- Authentik Security
|
||||
hide_table_of_contents: false
|
||||
image: ./tech-stack1.png
|
||||
---
|
||||
|
||||
> **_authentik is an open source Identity Provider that unifies your identity needs into a single platform, replacing Okta, Active Directory, and auth0. Authentik Security is a [public benefit company](https://github.com/OpenCoreVentures/ocv-public-benefit-company/blob/main/ocv-public-benefit-company-charter.md) building on top of the open source project._**
|
||||
|
||||
---
|
||||
|
||||
With great power (to choose your own tools) comes great responsibility. Not inheriting a legacy toolchain is an infrastructure engineer’s dream, but it can be hard to know where to start.
|
||||
|
||||
As the first infrastructure engineer hired to work on authentik, I saw the greenfield opportunities, but also the responsibility and long-term importance of choosing the best stack of tools and build processes. From my past roles, I already knew many of the considerations we would need to factor in.
|
||||
|
||||
For example, we know that ease of maintenance is a primary consideration, as is the stability and probable longevity of the tool, how well the tools integrate, and of course the level of support we were likely to get for each tool.
|
||||
|
||||
In this post we share some of what we are using to build authentik, and the lessons behind those choices.
|
||||
![technology stack for startups](./tech-stack1.png)
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
## #1 Choices are often human, not technical
|
||||
|
||||
If there isn’t much difference between two tools, the choice isn’t a technical decision. It’s going to come down to human factors like ease of use or the team’s familiarity with the tool. This is why we use [GitHub Actions](https://docs.github.com/en/actions) for our CI—[we’re already on GitHub](https://github.com/goauthentik) so it just makes sense.
|
||||
|
||||
> Familiarity with a tool means that you and your team can move faster, leading to higher business efficiency and a happier team.
|
||||
|
||||
### We use Argo CD for GitOps
|
||||
|
||||
When I joined Authentik Security, we were using [Flux CD](https://fluxcd.io/). Jens, our founder and CTO, had set up a small Kubernetes cluster to run an authentik instance for us to log into different services (some monitoring tools), and he was deploying all of this using Flux CD.
|
||||
|
||||
If you’re not familiar, Flux and [Argo CD](https://argo-cd.readthedocs.io/en/stable/) enable you to do GitOps: whatever you want to deploy, you push that to a Git repository and then synchronize whatever is in production from that Git repository. Everything is committed and tracked in the Git history (helping you to understand what has changed and why).
|
||||
|
||||
You also don’t need to do anything manually on the production servers or clusters—it’s all done in Git. This helps with auditing, as history is tracked, and you can easily find who made a change. You don’t need to give access to your production servers and cluster to whoever is conducting the audit, since they can see how everything is configured in the Git repo.
|
||||
|
||||
#### Flux and Argo CD essentially do the same thing
|
||||
|
||||
Despite Flux and Argo CD both being good at what they do, I advocated for switching to Argo CD because I have always worked with it and that familiarity with the tool meant I’d be able to work with much greater efficiency and velocity.
|
||||
|
||||
Since switching to Argo CD, we’ve automated deployment of new pull requests with the `deploy me` label. A developer can add that label to one of their open PRs, and the changes get deployed to a production-like environment so they can test those changes with a real domain and real certificates—it’s exactly the same as how a client would interact with those changes. It’s especially useful for mobile app development because instead of launching an authentik instance locally, you can test the mobile app against a production-like environment. This ability to access this “test deployment” is great for QA, tech writers, technical marketing teams, and anyone else who needs early access to a feature before it even gets merged.
|
||||
|
||||
#### Setting us up to scale
|
||||
|
||||
Argo CD also comes with a built-in UI, which Flux does not. This is useful because as we grow as a company, we will have more developers and we want to enable self-service and a culture of “you build it, you run it.”
|
||||
|
||||
With the Argo CD UI, a developer can make changes in Git, view the changes in the UI, and validate if the application started correctly and if everything is running. There’s no need to build another tool or set up Grafana dashboards or some other solution for developers to check if the application is running correctly.
|
||||
|
||||
“You build it, you run it” in this case isn’t about operations or infrastructure leaving developers to figure things out on their own. What we actually want is to empower devs to run things themselves so that:
|
||||
|
||||
1. Everyone shares the burden of production.
|
||||
2. Developers have a shorter feedback loop to see how their app behaves in production.
|
||||
|
||||
This type of choice is about setting things up for scalability down the road, which leads me to our next lesson.
|
||||
|
||||
## #2 Build with scale in mind
|
||||
|
||||
Our founder, Jens, has written before about [building apps with scale in mind](https://goauthentik.io/blog/2023-06-13-building-apps-with-scale-in-mind) and [doing things the ‘right’ way first time](https://goauthentik.io/blog/2023/10/26/you-might-be-doing-containers-wrong/).
|
||||
|
||||
As an infrastructure engineer especially, it can be so hard to deal with legacy tools and solutions (sometimes you just want to burn it all down and start over). It’s just so much easier to maintain things if you do it properly from the beginning. Part of why I wanted to join Authentik Security was because there wasn’t any legacy to deal with!
|
||||
|
||||
Yes, premature optimization is the root of all evil, but that doesn’t mean you can’t think about scalability when designing something. Having a design that can scale up if we need it to, but that can also run with few resources (human or machine)—even if a few compromises are necessary to allow it to do so—is oftentimes better that having a design that wasn’t built with scale in mind. This can spare you having to redesign it later (and then on top of that, migrate the old one).
|
||||
|
||||
### We use Transifex for translation
|
||||
|
||||
Internationalization isn’t often high on the list for open source projects or developer tool companies, but we’ve been doing it with [Transifex](https://www.transifex.com/).
|
||||
|
||||
If your users are developers they are probably used to working with tools in English. Whoever administers authentik for a company in France, for example, probably knows enough English to get by. But that company’s users may not need to speak English at all in their role because they’re on the legal or finance side. Those users still need to log in using authentik, so it’s great to be able to provide it in their language.
|
||||
|
||||
We use [Lit](https://lit.dev/) for our frontend (Jens has written about [choosing Lit over React](https://goauthentik.io/blog/2023-05-04-i-gambled-against-react-and-lost)), which supports translation by default:
|
||||
|
||||
- With Lit, we’re able to extract strings of text that we want to translate.
|
||||
- Those strings are sent to Transifex, where we can crowdsource translations.
|
||||
- We do this by marking strings as “source strings” with just three extra characters per string, which is not that much of an effort if you’re doing it from the outset vs implementing afterwards.
|
||||
|
||||
Native speakers of a given language can help us polish our translations; this is a great way to enable people to contribute to the project (not everyone can or wants to contribute code, for example).
|
||||
|
||||
## #3 Think about product-specific requirements
|
||||
|
||||
As a security company, some of our choices are influenced by product- and industry-specific needs.
|
||||
|
||||
As you’re building your own stack, you may need to think about the requirements of your own product space or industry. You might have customer expectations to meet or compliance requirements, for example.
|
||||
|
||||
### We use Redis for reputation data
|
||||
|
||||
Most of our storage is done in [PostgreSQL](https://www.postgresql.org/), but for some types of storage we use [Redis](https://redis.io/) for latency reasons, as it’s much faster to fetch data from.
|
||||
|
||||
We have two use cases for Redis:
|
||||
|
||||
#### Reputation data
|
||||
|
||||
If someone tries to log in and fails, we temporarily store ‘bad reputation’ weights associated with their IP address in Redis. This enables authentik admins to manage logins more securely; if someone has a reputation of less than a certain threshold (because they tried bad login details a few too many times), the authentik admin can block them.
|
||||
|
||||
That data is stored in Redis temporarily; we have a subsequent task that fetches it from Redis and stores it in the database. That way, if you want to keep updating the reputation data for a user (because they keep trying to log in with bad inputs), we’re just updating Redis and not PostgreSQL every time. Then when that data is moved from Redis to PostgreSQL, it’s compacted.
|
||||
|
||||
#### Session data
|
||||
|
||||
This use case is more common: with every request, we check that the user is still logged in or if they need to log in. Again, we store this data in Redis for latency reasons.
|
||||
|
||||
# #4 Your choices will cost you, one way or another
|
||||
|
||||
Of course, budget is going to play a role in the tools you choose. You have to balance your investments of money OR time spent on your tooling.
|
||||
|
||||
### We use Loki for logging
|
||||
|
||||
We talked about this in our recent [post about building a security stack with mostly free and open source software](https://goauthentik.io/blog/2023-11-22-how-we-saved-over-100k). As we wrote, [Loki](https://grafana.com/oss/loki/) is free, open source, and cheap to run. We could have gone with something like Elasticsearch (and the whole Elastic Stack) but it’s so expensive to run in terms of processing power and memory resources. Loki isn’t as easy to run, but we save on costs.
|
||||
|
||||
> It comes back to the idea of “you either pay in time or money” for software, and for most of authentik’s tooling I’ve already paid in time for it.
|
||||
|
||||
## #5 Optimize for stability and support
|
||||
|
||||
Infrastructure people just want to be able to sleep at night and not get paged at all hours (for that we use [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/), just in case). I just wanted to set up things that work and are reliable—pragmatic reasons similar to [why we chose Python and Django](https://goauthentik.io/blog/2023-03-16-authentik-on-django-500-slower-to-run-but-200-faster-to-build).
|
||||
|
||||
These next tools (all open source) were easy choices because I’ve already used them and know how to configure them properly and what pitfalls to watch out for.
|
||||
|
||||
### We use Grafana and Prometheus for monitoring and metrics
|
||||
|
||||
[Grafana](https://grafana.com/grafana/) is widely used and comes with a lot of existing resources, so we don’t have to build everything ourselves. We use it to display our logs and metrics in dashboards so we can view and correlate data from Loki and [Prometheus](https://grafana.com/oss/prometheus/) together. Prometheus scrapes metrics from our infrastructure tools, authentik instances, and other applications.
|
||||
|
||||
#### We use Thanos to manage metrics data
|
||||
|
||||
I used Thanos for a large-scale deployment in a previous role—storing something like 20TB of metrics per six months—so I knew it would work for us now and later as we scale.
|
||||
|
||||
Prometheus stores metrics for ~1 day before [Thanos](https://thanos.io/) fetches them and pushes to S3 storage for longer-term retention. We do this because Prometheus doesn’t do well storing large amounts of data. We’re also running Prometheus in highly available fashion, which would mean storing the data twice, which would be expensive.
|
||||
|
||||
Thanos compresses the metrics data and also does downsampling:
|
||||
|
||||
- For 30 days we keep everything that’s scraped (every 30/60 seconds)
|
||||
- Beyond that, for 90 days we keep only a metric point every five minutes
|
||||
- For a year, we keep just one metric point per hour
|
||||
|
||||
By retaining less data as time passes, queries are faster and storage is cheaper. Why keep metrics for such a long time? It gives us a view of the seasonality of traffic so we can do better capacity planning.
|
||||
|
||||
## Final thoughts
|
||||
|
||||
This isn’t a groundbreaking stack. We aren’t optimizing for the latest edge technology. (The only somewhat controversial choice we’ve made has been moving to an [IPv6 only](https://goauthentik.io/blog/2023-11-09-IPv6-addresses) network.) For the most part we’ve gone with options that are tried and tested, well known to the team, and play nicely with the other parts of our stack.
|
||||
|
||||
As always, we would be interested in hearing your thoughts about the stack we use, and about the tools that you and your team have chosen and the reasons behind those choices. Send us an email to hello@goauthentik.io or chime in on [Discord](https://discord.com/channels/809154715984199690/809154716507963434).
|
Binary file not shown.
After Width: | Height: | Size: 150 KiB |
|
@ -1,38 +1,9 @@
|
|||
---
|
||||
hide_table_of_contents: true
|
||||
---
|
||||
|
||||
# API Browser
|
||||
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||
import BrowserOnly from "@docusaurus/core/lib/client/exports/BrowserOnly";
|
||||
import { useColorMode } from "@docusaurus/theme-common";
|
||||
|
||||
export function APIBrowser() {
|
||||
const context = useDocusaurusContext();
|
||||
const { siteConfig = {} } = context;
|
||||
const { colorMode, setColorMode } = useColorMode();
|
||||
let bg = "#1b1b1d";
|
||||
if (colorMode === "light") {
|
||||
bg = "#fff";
|
||||
}
|
||||
return (
|
||||
<BrowserOnly>
|
||||
{() => {
|
||||
import("rapidoc");
|
||||
return (
|
||||
<rapi-doc
|
||||
spec-url={useBaseUrl("schema.yaml")}
|
||||
allow-try="false"
|
||||
show-header="false"
|
||||
theme={colorMode}
|
||||
bg-color={bg}
|
||||
render-style="view"
|
||||
primary-color="#fd4b2d"
|
||||
allow-spec-url-load="false"
|
||||
allow-spec-file-load="false"
|
||||
></rapi-doc>
|
||||
);
|
||||
}}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
import APIBrowser from "../../src/components/APIBrowser";
|
||||
|
||||
<APIBrowser />
|
||||
|
|
|
@ -27,7 +27,8 @@ import TabItem from "@theme/TabItem";
|
|||
Add the following block to your `.env` file:
|
||||
|
||||
```shell
|
||||
AUTHENTIK_GEOIP=/tmp/non-existent-file
|
||||
AUTHENTIK_EVENTS__CONTEXT_PROCESSORS__GEOIP=/tmp/non-existent-file
|
||||
AUTHENTIK_EVENTS__CONTEXT_PROCESSORS__ASN=/tmp/non-existent-file
|
||||
```
|
||||
|
||||
Afterwards, run the upgrade commands from the latest release notes.
|
||||
|
@ -38,7 +39,10 @@ Add the following block to your `values.yml` file:
|
|||
|
||||
```yaml
|
||||
authentik:
|
||||
geoip: /tmp/non-existent-file
|
||||
events:
|
||||
context_processors:
|
||||
geoip: "/tmp/non-existent-file"
|
||||
asn: "/tmp/non-existent-file"
|
||||
```
|
||||
|
||||
Afterwards, run the upgrade commands from the latest release notes.
|
||||
|
@ -74,7 +78,7 @@ services:
|
|||
volumes:
|
||||
- "geoip:/usr/share/GeoIP"
|
||||
environment:
|
||||
GEOIPUPDATE_EDITION_IDS: "GeoLite2-City"
|
||||
GEOIPUPDATE_EDITION_IDS: "GeoLite2-City GeoLite2-ASN"
|
||||
GEOIPUPDATE_FREQUENCY: "8"
|
||||
GEOIPUPDATE_ACCOUNT_ID: "*your account ID*"
|
||||
GEOIPUPDATE_LICENSE_KEY: "*your license key*"
|
||||
|
@ -94,7 +98,7 @@ geoip:
|
|||
enabled: true
|
||||
accountId: "*your account ID*"
|
||||
licenseKey: "*your license key*"
|
||||
editionIds: "GeoLite2-City"
|
||||
editionIds: "GeoLite2-City GeoLite2-ASN"
|
||||
image: maxmindinc/geoipupdate:v4.8
|
||||
updateInterval: 8
|
||||
```
|
||||
|
|
|
@ -231,6 +231,11 @@ A user authorizes an application.
|
|||
"action": "authorize_application",
|
||||
"app": "authentik.providers.oauth2.views.authorize",
|
||||
"context": {
|
||||
"asn": {
|
||||
"asn": 6805,
|
||||
"as_org": "Telefonica Germany",
|
||||
"network": "5.4.0.0/14"
|
||||
},
|
||||
"geo": {
|
||||
"lat": 42.0,
|
||||
"city": "placeholder",
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
|
@ -25,6 +25,10 @@ return True
|
|||
|
||||
You can also use custom email templates, to use your own design or layout.
|
||||
|
||||
:::info
|
||||
Starting with authentik 2024.1, it is possible to create `.txt` files with the same name as the `.html` template. If a matching `.txt` file exists, the email sent will be a multipart email with both the text and HTML template.
|
||||
:::
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
|
@ -81,13 +85,17 @@ Templates are rendered using Django's templating engine. The following variables
|
|||
- `user`: The pending user object.
|
||||
- `expires`: The timestamp when the token expires.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
```html
|
||||
{# This is how you can write comments which aren't rendered. #} {# Extend this
|
||||
template from the base email template, which includes base layout and CSS. #} {%
|
||||
extends "email/base.html" %} {# Load the internationalization module to
|
||||
translate strings, and humanize to show date-time #} {% load i18n %} {% load
|
||||
humanize %} {# The email/base.html template uses a single "content" block #} {%
|
||||
block content %}
|
||||
{# This is how you can write comments which aren't rendered. #}
|
||||
{# Extend this template from the base email template, which includes base layout and CSS. #}
|
||||
{% extends "email/base.html" %}
|
||||
{# Load the internationalization module to translate strings, and humanize to show date-time #}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{# The email/base.html template uses a single "content" block #}
|
||||
{% block content %}
|
||||
<tr>
|
||||
<td class="alert alert-success">
|
||||
{% blocktrans with username=user.username %} Hi {{ username }}, {%
|
||||
|
@ -99,9 +107,9 @@ block content %}
|
|||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
{% blocktrans %} You recently requested to change your
|
||||
password for you authentik account. Use the button below to
|
||||
set a new password. {% endblocktrans %}
|
||||
{% blocktrans %}
|
||||
You recently requested to change your password for you authentik account. Use the button below to set a new password.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -130,8 +138,7 @@ block content %}
|
|||
href="{{ url }}"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>{% trans 'Reset
|
||||
Password' %}</a
|
||||
>{% trans 'Reset Password' %}</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -145,9 +152,9 @@ block content %}
|
|||
</tr>
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -155,3 +162,5 @@ block content %}
|
|||
</tr>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
|
|
@ -32,6 +32,51 @@ When this is set to a higher value than the default _seconds=0_, a prompt is sho
|
|||
|
||||
![](./stay_signed_in.png)
|
||||
|
||||
## Network binding/GeoIP binding
|
||||
|
||||
When configured, all sessions authenticated by this stage will be bound to the selected network/GeoIP criteria.
|
||||
|
||||
Sessions which break this binding will be terminated on use. The created [`logout`](../../../events/index.md#logout) event will contain additional data related to what caused the binding to be broken:
|
||||
|
||||
```json
|
||||
|
||||
Context
|
||||
{
|
||||
"asn": {
|
||||
"asn": 6805,
|
||||
"as_org": "Telefonica Germany",
|
||||
"network": "5.4.0.0/14"
|
||||
},
|
||||
"geo": {
|
||||
"lat": 51.2993,
|
||||
"city": "",
|
||||
"long": 9.491,
|
||||
"country": "DE",
|
||||
"continent": "EU"
|
||||
},
|
||||
"binding": {
|
||||
"reason": "network.missing",
|
||||
"new_value": {
|
||||
"asn": 6805,
|
||||
"as_org": "Telefonica Germany",
|
||||
"network": "5.4.0.0/14"
|
||||
},
|
||||
"previous_value": {}
|
||||
},
|
||||
"ip": {
|
||||
"previous": "1.2.3.4",
|
||||
"new": "5.6.7.8",
|
||||
},
|
||||
"http_request": {
|
||||
"args": {},
|
||||
"path": "/if/admin/",
|
||||
"method": "GET",
|
||||
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
},
|
||||
"logout_reason": "Session binding broken"
|
||||
}
|
||||
```
|
||||
|
||||
## Terminate other sessions
|
||||
|
||||
When enabled, previous sessions of the user logging in will be revoked. This has no affect on OAuth refresh tokens.
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue