Merge branch 'main' into multi-tenant-django-tenants

This commit is contained in:
Marc 'risson' Schmitt 2023-12-27 11:56:50 +01:00
commit 2af782c023
No known key found for this signature in database
GPG Key ID: 9C3FA22FABF1AA8D
109 changed files with 13494 additions and 1105 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

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

View File

@ -0,0 +1,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()

View File

@ -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)

View File

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

View File

@ -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,

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -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,25 +157,23 @@ 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:
message = (
f"'{deprecation}' has been deprecated in favor of '{replacement}'! "
+ "Please update your configuration."
)
self.log(
"warning",
message,
)
try:
from authentik.events.models import Event, EventAction
if self.get(deprecation, default=UNSET) is UNSET:
continue
message = (
f"'{deprecation}' has been deprecated in favor of '{replacement}'! "
+ "Please update your configuration."
)
self.log(
"warning",
message,
)
deprecation_replacements[(deprecation, replacement)] = message
Event.new(EventAction.CONFIGURATION_ERROR, message=message).save()
except ImportError:
continue
deprecated_attr = _pop_deprecated_key(self.__config, deprecation.split("."), 0)
self.set(replacement, deprecated_attr.value)
deprecated_attr = _pop_deprecated_key(self.__config, deprecation.split("."), 0)
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()

View File

@ -37,8 +37,8 @@ redis:
tls_reqs: "none"
# broker:
# url: ""
# transport_options: ""
# url: ""
# transport_options: ""
cache:
# url: ""
@ -48,10 +48,10 @@ cache:
timeout_reputation: 300
# channel:
# url: ""
# url: ""
# result_backend:
# url: ""
# url: ""
debug: false
remote_debug: false
@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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=""),
),
]

View File

@ -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]:

View File

@ -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)

View File

@ -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,

View File

@ -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,
},
)

View File

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

View File

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

View File

@ -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"))

View File

@ -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)

View File

@ -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",

View File

@ -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",

View File

@ -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:

View File

@ -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"
)

View File

@ -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,

View File

@ -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],

View File

@ -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],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,8 @@ class UserLoginStageSerializer(StageSerializer):
"session_duration",
"terminate_other_sessions",
"remember_me_offset",
"network_binding",
"geoip_binding",
]

View File

@ -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,
)

View File

@ -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",
),
),
]

View File

@ -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.")
)

View File

@ -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,

View File

@ -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": []

View File

@ -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,

View File

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

4
go.mod
View File

@ -23,11 +23,11 @@ require (
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.17.0
github.com/redis/go-redis/v9 v9.3.0
github.com/redis/go-redis/v9 v9.3.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
goauthentik.io/api/v3 v3.2023104.3
goauthentik.io/api/v3 v3.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
View File

@ -256,8 +256,8 @@ github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdO
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
@ -309,8 +309,8 @@ go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYO
go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2023104.3 h1:MzwdB21Q+G+wACEZiX0T1iVV4l7PjopjaVv6muqJE1M=
goauthentik.io/api/v3 v3.2023104.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.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=

View File

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

View File

@ -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
})

View File

@ -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.

View File

@ -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>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2023.10.4
version: 2023.10.5
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -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:

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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"

View File

@ -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",

799
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -93,15 +93,16 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> {
const health = await api.providersScimSyncStatusRetrieve({
id: element.pk,
});
if (health.status !== TaskStatusEnum.Successful) {
sourceKey = "failed";
}
const now = new Date().getTime();
const maxDelta = 3600000; // 1 hour
if (!health || now - health.taskFinishTimestamp.getTime() > maxDelta) {
sourceKey = "unsynced";
}
health.tasks.forEach((task) => {
if (task.status !== TaskStatusEnum.Successful) {
sourceKey = "failed";
}
const now = new Date().getTime();
const maxDelta = 3600000; // 1 hour
if (!health || now - task.taskFinishTimestamp.getTime() > maxDelta) {
sourceKey = "unsynced";
}
});
} catch {
sourceKey = "unsynced";
}

View File

@ -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"

View File

@ -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

View File

@ -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 = ";";

View File

@ -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>`;
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

8010
web/xliff/ko.xlf Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 engineers 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 isnt much difference between two tools, the choice isnt a technical decision. Its going to come down to human factors like ease of use or the teams familiarity with the tool. This is why we use [GitHub Actions](https://docs.github.com/en/actions) for our CI—[were 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 youre 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 dont need to do anything manually on the production servers or clusters—its all done in Git. This helps with auditing, as history is tracked, and you can easily find who made a change. You dont 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 Id be able to work with much greater efficiency and velocity.
Since switching to Argo CD, weve 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—its exactly the same as how a client would interact with those changes. Its 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. Theres 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 isnt 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). Its 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 wasnt any legacy to deal with!
Yes, premature optimization is the root of all evil, but that doesnt mean you cant 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 wasnt 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 isnt often high on the list for open source projects or developer tool companies, but weve 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 companys users may not need to speak English at all in their role because theyre on the legal or finance side. Those users still need to log in using authentik, so its 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, were 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 youre 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 youre 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 its 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), were just updating Redis and not PostgreSQL every time. Then when that data is moved from Redis to PostgreSQL, its 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 its so expensive to run in terms of processing power and memory resources. Loki isnt 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 authentiks tooling Ive 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 Ive 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 dont 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 doesnt do well storing large amounts of data. Were 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 thats 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 isnt a groundbreaking stack. We arent optimizing for the latest edge technology. (The only somewhat controversial choice weve made has been moving to an [IPv6 only](https://goauthentik.io/blog/2023-11-09-IPv6-addresses) network.) For the most part weve 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

View File

@ -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 />

View File

@ -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
```

View File

@ -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

View File

@ -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 -->

View File

@ -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