Compare commits
6 Commits
version/20
...
policies/p
Author | SHA1 | Date | |
---|---|---|---|
b3883f7fbf | |||
87c6b0128a | |||
b243c97916 | |||
3f66527521 | |||
2f7c258657 | |||
917c90374f |
@ -1,16 +1,16 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.2.0-rc1
|
||||
current_version = 2024.12.3
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
serialize =
|
||||
serialize =
|
||||
{major}.{minor}.{patch}-{rc_t}{rc_n}
|
||||
{major}.{minor}.{patch}
|
||||
message = release: {new_version}
|
||||
tag_name = version/{new_version}
|
||||
|
||||
[bumpversion:part:rc_t]
|
||||
values =
|
||||
values =
|
||||
rc
|
||||
final
|
||||
optional_value = final
|
||||
|
2
Makefile
2
Makefile
@ -21,7 +21,7 @@ pg_name := $(shell python -m authentik.lib.config postgresql.name 2>/dev/null)
|
||||
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
|
||||
-I .github/codespell-words.txt \
|
||||
-S 'web/src/locales/**' \
|
||||
-S 'website/docs/developer-docs/api/reference/**' \
|
||||
-S 'website/developer-docs/api/reference/**' \
|
||||
-S '**/node_modules/**' \
|
||||
-S '**/dist/**' \
|
||||
$(PY_SOURCES) \
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.2.0"
|
||||
__version__ = "2024.12.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -4,7 +4,6 @@ from json import loads
|
||||
|
||||
from django.db.models import Prefetch
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.utils import (
|
||||
@ -82,37 +81,9 @@ class GroupSerializer(ModelSerializer):
|
||||
if not self.instance or not parent:
|
||||
return parent
|
||||
if str(parent.group_uuid) == str(self.instance.group_uuid):
|
||||
raise ValidationError(_("Cannot set group as parent of itself."))
|
||||
raise ValidationError("Cannot set group as parent of itself.")
|
||||
return parent
|
||||
|
||||
def validate_is_superuser(self, superuser: bool):
|
||||
"""Ensure that the user creating this group has permissions to set the superuser flag"""
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return superuser
|
||||
# If we're updating an instance, and the state hasn't changed, we don't need to check perms
|
||||
if self.instance and superuser == self.instance.is_superuser:
|
||||
return superuser
|
||||
user: User = request.user
|
||||
perm = (
|
||||
"authentik_core.enable_group_superuser"
|
||||
if superuser
|
||||
else "authentik_core.disable_group_superuser"
|
||||
)
|
||||
has_perm = user.has_perm(perm)
|
||||
if self.instance and not has_perm:
|
||||
has_perm = user.has_perm(perm, self.instance)
|
||||
if not has_perm:
|
||||
raise ValidationError(
|
||||
_(
|
||||
(
|
||||
"User does not have permission to set "
|
||||
"superuser status to {superuser_status}."
|
||||
).format_map({"superuser_status": superuser})
|
||||
)
|
||||
)
|
||||
return superuser
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = [
|
||||
|
@ -1,26 +0,0 @@
|
||||
# Generated by Django 5.0.11 on 2025-01-30 23:55
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="group",
|
||||
options={
|
||||
"permissions": [
|
||||
("add_user_to_group", "Add user to group"),
|
||||
("remove_user_from_group", "Remove user from group"),
|
||||
("enable_group_superuser", "Enable superuser status"),
|
||||
("disable_group_superuser", "Disable superuser status"),
|
||||
],
|
||||
"verbose_name": "Group",
|
||||
"verbose_name_plural": "Groups",
|
||||
},
|
||||
),
|
||||
]
|
@ -204,8 +204,6 @@ class Group(SerializerModel, AttributesMixin):
|
||||
permissions = [
|
||||
("add_user_to_group", _("Add user to group")),
|
||||
("remove_user_from_group", _("Remove user from group")),
|
||||
("enable_group_superuser", _("Enable superuser status")),
|
||||
("disable_group_superuser", _("Disable superuser status")),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
|
@ -67,8 +67,6 @@ def clean_expired_models(self: SystemTask):
|
||||
raise ImproperlyConfigured(
|
||||
"Invalid session_storage setting, allowed values are db and cache"
|
||||
)
|
||||
if CONFIG.get("session_storage", "cache") == "db":
|
||||
DBSessionStore.clear_expired()
|
||||
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
||||
|
||||
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
||||
|
@ -4,7 +4,7 @@ from django.urls.base import reverse
|
||||
from guardian.shortcuts import assign_perm
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
@ -14,7 +14,7 @@ class TestGroupsAPI(APITestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.login_user = create_test_user()
|
||||
self.user = create_test_user()
|
||||
self.user = User.objects.create(username="test-user")
|
||||
|
||||
def test_list_with_users(self):
|
||||
"""Test listing with users"""
|
||||
@ -109,57 +109,3 @@ class TestGroupsAPI(APITestCase):
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
def test_superuser_no_perm(self):
|
||||
"""Test creating a superuser group without permission"""
|
||||
assign_perm("authentik_core.add_group", self.login_user)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:group-list"),
|
||||
data={"name": generate_id(), "is_superuser": True},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
res.content,
|
||||
{"is_superuser": ["User does not have permission to set superuser status to True."]},
|
||||
)
|
||||
|
||||
def test_superuser_update_no_perm(self):
|
||||
"""Test updating a superuser group without permission"""
|
||||
group = Group.objects.create(name=generate_id(), is_superuser=True)
|
||||
assign_perm("view_group", self.login_user, group)
|
||||
assign_perm("change_group", self.login_user, group)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
||||
data={"is_superuser": False},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
res.content,
|
||||
{"is_superuser": ["User does not have permission to set superuser status to False."]},
|
||||
)
|
||||
|
||||
def test_superuser_update_no_change(self):
|
||||
"""Test updating a superuser group without permission
|
||||
and without changing the superuser status"""
|
||||
group = Group.objects.create(name=generate_id(), is_superuser=True)
|
||||
assign_perm("view_group", self.login_user, group)
|
||||
assign_perm("change_group", self.login_user, group)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
||||
data={"name": generate_id(), "is_superuser": True},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_superuser_create(self):
|
||||
"""Test creating a superuser group with permission"""
|
||||
assign_perm("authentik_core.add_group", self.login_user)
|
||||
assign_perm("authentik_core.enable_group_superuser", self.login_user)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:group-list"),
|
||||
data={"name": generate_id(), "is_superuser": True},
|
||||
)
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
@ -97,8 +97,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
||||
thread_kwargs: dict | None = None,
|
||||
**_,
|
||||
):
|
||||
if not self.enabled:
|
||||
return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)
|
||||
if not should_log_model(instance):
|
||||
return None
|
||||
thread_kwargs = {}
|
||||
@ -124,8 +122,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
||||
):
|
||||
thread_kwargs = {}
|
||||
m2m_field = None
|
||||
if not self.enabled:
|
||||
return super().m2m_changed_handler(request, sender, instance, action, thread_kwargs)
|
||||
# For the audit log we don't care about `pre_` or `post_` so we trim that part off
|
||||
_, _, action_direction = action.partition("_")
|
||||
# resolve the "through" model to an actual field
|
||||
|
@ -1,54 +0,0 @@
|
||||
"""Email utility functions"""
|
||||
|
||||
|
||||
def mask_email(email: str | None) -> str | None:
|
||||
"""Mask email address for privacy
|
||||
|
||||
Args:
|
||||
email: Email address to mask
|
||||
Returns:
|
||||
Masked email address or None if input is None
|
||||
Example:
|
||||
mask_email("myname@company.org")
|
||||
'm*****@c******.org'
|
||||
"""
|
||||
if not email:
|
||||
return None
|
||||
|
||||
# Basic email format validation
|
||||
if email.count("@") != 1:
|
||||
raise ValueError("Invalid email format: Must contain exactly one '@' symbol")
|
||||
|
||||
local, domain = email.split("@")
|
||||
if not local or not domain:
|
||||
raise ValueError("Invalid email format: Local and domain parts cannot be empty")
|
||||
|
||||
domain_parts = domain.split(".")
|
||||
if len(domain_parts) < 2: # noqa: PLR2004
|
||||
raise ValueError("Invalid email format: Domain must contain at least one dot")
|
||||
|
||||
limit = 2
|
||||
|
||||
# Mask local part (keep first char)
|
||||
if len(local) <= limit:
|
||||
masked_local = "*" * len(local)
|
||||
else:
|
||||
masked_local = local[0] + "*" * (len(local) - 1)
|
||||
|
||||
# Mask each domain part except the last one (TLD)
|
||||
masked_domain_parts = []
|
||||
for _i, part in enumerate(domain_parts[:-1]): # Process all parts except TLD
|
||||
if not part: # Check for empty parts (consecutive dots)
|
||||
raise ValueError("Invalid email format: Domain parts cannot be empty")
|
||||
if len(part) <= limit:
|
||||
masked_part = "*" * len(part)
|
||||
else:
|
||||
masked_part = part[0] + "*" * (len(part) - 1)
|
||||
masked_domain_parts.append(masked_part)
|
||||
|
||||
# Add TLD unchanged
|
||||
if not domain_parts[-1]: # Check if TLD is empty
|
||||
raise ValueError("Invalid email format: TLD cannot be empty")
|
||||
masked_domain_parts.append(domain_parts[-1])
|
||||
|
||||
return f"{masked_local}@{'.'.join(masked_domain_parts)}"
|
@ -1,11 +1,26 @@
|
||||
"""Expression Policy API"""
|
||||
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.events.logs import LogEventSerializer, capture_logs
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer
|
||||
from authentik.policies.api.policies import PolicySerializer
|
||||
from authentik.policies.expression.evaluator import PolicyEvaluator
|
||||
from authentik.policies.expression.models import ExpressionPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.policies.process import PolicyProcess
|
||||
from authentik.policies.types import PolicyRequest
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class ExpressionPolicySerializer(PolicySerializer):
|
||||
@ -30,3 +45,50 @@ class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
class ExpressionPolicyTestSerializer(PolicyTestSerializer):
|
||||
"""Expression policy test serializer"""
|
||||
|
||||
expression = CharField()
|
||||
|
||||
@permission_required("authentik_policies.view_policy")
|
||||
@extend_schema(
|
||||
request=ExpressionPolicyTestSerializer(),
|
||||
responses={
|
||||
200: PolicyTestResultSerializer(),
|
||||
400: OpenApiResponse(description="Invalid parameters"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
def test(self, request: Request, pk: str) -> Response:
|
||||
"""Test policy"""
|
||||
policy = self.get_object()
|
||||
test_params = self.ExpressionPolicyTestSerializer(data=request.data)
|
||||
if not test_params.is_valid():
|
||||
return Response(test_params.errors, status=400)
|
||||
|
||||
# User permission check, only allow policy testing for users that are readable
|
||||
users = get_objects_for_user(request.user, "authentik_core.view_user").filter(
|
||||
pk=test_params.validated_data["user"].pk
|
||||
)
|
||||
if not users.exists():
|
||||
return Response(status=400)
|
||||
|
||||
policy.expression = test_params.validated_data["expression"]
|
||||
|
||||
p_request = PolicyRequest(users.first())
|
||||
p_request.debug = True
|
||||
p_request.set_http_request(self.request)
|
||||
p_request.context = test_params.validated_data.get("context", {})
|
||||
|
||||
proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None)
|
||||
with capture_logs() as logs:
|
||||
result = proc.execute()
|
||||
log_messages = []
|
||||
for log in logs:
|
||||
if log.attributes.get("process", "") == "PolicyProcess":
|
||||
continue
|
||||
log_messages.append(LogEventSerializer(log).data)
|
||||
result.log_messages = log_messages
|
||||
response = PolicyTestResultSerializer(result)
|
||||
return Response(response.data)
|
||||
|
@ -42,12 +42,6 @@ class GeoIPPolicySerializer(CountryFieldMixin, PolicySerializer):
|
||||
"asns",
|
||||
"countries",
|
||||
"countries_obj",
|
||||
"check_history_distance",
|
||||
"history_max_distance_km",
|
||||
"distance_tolerance_km",
|
||||
"history_login_count",
|
||||
"check_impossible_travel",
|
||||
"impossible_tolerance_km",
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,43 +0,0 @@
|
||||
# Generated by Django 5.0.10 on 2025-01-02 20:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_geoip", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="geoippolicy",
|
||||
name="check_history_distance",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="geoippolicy",
|
||||
name="check_impossible_travel",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="geoippolicy",
|
||||
name="distance_tolerance_km",
|
||||
field=models.PositiveIntegerField(default=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="geoippolicy",
|
||||
name="history_login_count",
|
||||
field=models.PositiveIntegerField(default=5),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="geoippolicy",
|
||||
name="history_max_distance_km",
|
||||
field=models.PositiveBigIntegerField(default=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="geoippolicy",
|
||||
name="impossible_tolerance_km",
|
||||
field=models.PositiveIntegerField(default=100),
|
||||
),
|
||||
]
|
@ -4,21 +4,15 @@ from itertools import chain
|
||||
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from django_countries.fields import CountryField
|
||||
from geopy import distance
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.events.context_processors.geoip import GeoIPDict
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.policies.exceptions import PolicyException
|
||||
from authentik.policies.geoip.exceptions import GeoIPNotFoundException
|
||||
from authentik.policies.models import Policy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
MAX_DISTANCE_HOUR_KM = 1000
|
||||
|
||||
|
||||
class GeoIPPolicy(Policy):
|
||||
"""Ensure the user satisfies requirements of geography or network topology, based on IP
|
||||
@ -27,15 +21,6 @@ class GeoIPPolicy(Policy):
|
||||
asns = ArrayField(models.IntegerField(), blank=True, default=list)
|
||||
countries = CountryField(multiple=True, blank=True)
|
||||
|
||||
distance_tolerance_km = models.PositiveIntegerField(default=50)
|
||||
|
||||
check_history_distance = models.BooleanField(default=False)
|
||||
history_max_distance_km = models.PositiveBigIntegerField(default=100)
|
||||
history_login_count = models.PositiveIntegerField(default=5)
|
||||
|
||||
check_impossible_travel = models.BooleanField(default=False)
|
||||
impossible_tolerance_km = models.PositiveIntegerField(default=100)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
from authentik.policies.geoip.api import GeoIPPolicySerializer
|
||||
@ -52,27 +37,21 @@ class GeoIPPolicy(Policy):
|
||||
- the client IP is advertised by an autonomous system with ASN in the `asns`
|
||||
- the client IP is geolocated in a country of `countries`
|
||||
"""
|
||||
static_results: list[PolicyResult] = []
|
||||
dynamic_results: list[PolicyResult] = []
|
||||
results: list[PolicyResult] = []
|
||||
|
||||
if self.asns:
|
||||
static_results.append(self.passes_asn(request))
|
||||
results.append(self.passes_asn(request))
|
||||
if self.countries:
|
||||
static_results.append(self.passes_country(request))
|
||||
results.append(self.passes_country(request))
|
||||
|
||||
if self.check_history_distance or self.check_impossible_travel:
|
||||
dynamic_results.append(self.passes_distance(request))
|
||||
|
||||
if not static_results and not dynamic_results:
|
||||
if not results:
|
||||
return PolicyResult(True)
|
||||
|
||||
passing = any(r.passing for r in static_results) and all(r.passing for r in dynamic_results)
|
||||
messages = chain(
|
||||
*[r.messages for r in static_results], *[r.messages for r in dynamic_results]
|
||||
)
|
||||
passing = any(r.passing for r in results)
|
||||
messages = chain(*[r.messages for r in results])
|
||||
|
||||
result = PolicyResult(passing, *messages)
|
||||
result.source_results = list(chain(static_results, dynamic_results))
|
||||
result.source_results = results
|
||||
|
||||
return result
|
||||
|
||||
@ -94,7 +73,7 @@ class GeoIPPolicy(Policy):
|
||||
|
||||
def passes_country(self, request: PolicyRequest) -> PolicyResult:
|
||||
# This is not a single get chain because `request.context` can contain `{ "geoip": None }`.
|
||||
geoip_data: GeoIPDict | None = request.context.get("geoip")
|
||||
geoip_data = request.context.get("geoip")
|
||||
country = geoip_data.get("country") if geoip_data else None
|
||||
|
||||
if not country:
|
||||
@ -108,42 +87,6 @@ class GeoIPPolicy(Policy):
|
||||
|
||||
return PolicyResult(True)
|
||||
|
||||
def passes_distance(self, request: PolicyRequest) -> PolicyResult:
|
||||
"""Check if current policy execution is out of distance range compared
|
||||
to previous authentication requests"""
|
||||
# Get previous login event and GeoIP data
|
||||
previous_logins = Event.objects.filter(
|
||||
action=EventAction.LOGIN, user__pk=request.user.pk, context__geo__isnull=False
|
||||
).order_by("-created")[: self.history_login_count]
|
||||
_now = now()
|
||||
geoip_data: GeoIPDict | None = request.context.get("geoip")
|
||||
if not geoip_data:
|
||||
return PolicyResult(False)
|
||||
for previous_login in previous_logins:
|
||||
previous_login_geoip: GeoIPDict = previous_login.context["geo"]
|
||||
|
||||
# Figure out distance
|
||||
dist = distance.geodesic(
|
||||
(previous_login_geoip["lat"], previous_login_geoip["long"]),
|
||||
(geoip_data["lat"], geoip_data["long"]),
|
||||
)
|
||||
if self.check_history_distance and dist.km >= (
|
||||
self.history_max_distance_km - self.distance_tolerance_km
|
||||
):
|
||||
return PolicyResult(
|
||||
False, _("Distance from previous authentication is larger than threshold.")
|
||||
)
|
||||
# Check if distance between `previous_login` and now is more
|
||||
# than max distance per hour times the amount of hours since the previous login
|
||||
# (round down to the lowest closest time of hours)
|
||||
# clamped to be at least 1 hour
|
||||
rel_time_hours = max(int((_now - previous_login.created).total_seconds() / 3600), 1)
|
||||
if self.check_impossible_travel and dist.km >= (
|
||||
(MAX_DISTANCE_HOUR_KM * rel_time_hours) - self.distance_tolerance_km
|
||||
):
|
||||
return PolicyResult(False, _("Distance is further than possible."))
|
||||
return PolicyResult(True)
|
||||
|
||||
class Meta(Policy.PolicyMeta):
|
||||
verbose_name = _("GeoIP Policy")
|
||||
verbose_name_plural = _("GeoIP Policies")
|
||||
|
@ -1,10 +1,8 @@
|
||||
"""geoip policy tests"""
|
||||
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import get_user
|
||||
from authentik.policies.engine import PolicyRequest, PolicyResult
|
||||
from authentik.policies.exceptions import PolicyException
|
||||
from authentik.policies.geoip.exceptions import GeoIPNotFoundException
|
||||
@ -16,8 +14,8 @@ class TestGeoIPPolicy(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = create_test_user()
|
||||
self.request = PolicyRequest(self.user)
|
||||
|
||||
self.request = PolicyRequest(get_anonymous_user())
|
||||
|
||||
self.context_disabled_geoip = {}
|
||||
self.context_unknown_ip = {"asn": None, "geoip": None}
|
||||
@ -128,70 +126,3 @@ class TestGeoIPPolicy(TestCase):
|
||||
result: PolicyResult = policy.passes(self.request)
|
||||
|
||||
self.assertTrue(result.passing)
|
||||
|
||||
def test_history(self):
|
||||
"""Test history checks"""
|
||||
Event.objects.create(
|
||||
action=EventAction.LOGIN,
|
||||
user=get_user(self.user),
|
||||
context={
|
||||
# Random location in Canada
|
||||
"geo": {"lat": 55.868351, "long": -104.441011},
|
||||
},
|
||||
)
|
||||
# Random location in Poland
|
||||
self.request.context["geoip"] = {"lat": 50.950613, "long": 20.363679}
|
||||
|
||||
policy = GeoIPPolicy.objects.create(check_history_distance=True)
|
||||
|
||||
result: PolicyResult = policy.passes(self.request)
|
||||
self.assertFalse(result.passing)
|
||||
|
||||
def test_history_no_data(self):
|
||||
"""Test history checks (with no geoip data in context)"""
|
||||
Event.objects.create(
|
||||
action=EventAction.LOGIN,
|
||||
user=get_user(self.user),
|
||||
context={
|
||||
# Random location in Canada
|
||||
"geo": {"lat": 55.868351, "long": -104.441011},
|
||||
},
|
||||
)
|
||||
|
||||
policy = GeoIPPolicy.objects.create(check_history_distance=True)
|
||||
|
||||
result: PolicyResult = policy.passes(self.request)
|
||||
self.assertFalse(result.passing)
|
||||
|
||||
def test_history_impossible_travel(self):
|
||||
"""Test history checks"""
|
||||
Event.objects.create(
|
||||
action=EventAction.LOGIN,
|
||||
user=get_user(self.user),
|
||||
context={
|
||||
# Random location in Canada
|
||||
"geo": {"lat": 55.868351, "long": -104.441011},
|
||||
},
|
||||
)
|
||||
# Random location in Poland
|
||||
self.request.context["geoip"] = {"lat": 50.950613, "long": 20.363679}
|
||||
|
||||
policy = GeoIPPolicy.objects.create(check_impossible_travel=True)
|
||||
|
||||
result: PolicyResult = policy.passes(self.request)
|
||||
self.assertFalse(result.passing)
|
||||
|
||||
def test_history_no_geoip(self):
|
||||
"""Test history checks (previous login with no geoip data)"""
|
||||
Event.objects.create(
|
||||
action=EventAction.LOGIN,
|
||||
user=get_user(self.user),
|
||||
context={},
|
||||
)
|
||||
# Random location in Poland
|
||||
self.request.context["geoip"] = {"lat": 50.950613, "long": 20.363679}
|
||||
|
||||
policy = GeoIPPolicy.objects.create(check_history_distance=True)
|
||||
|
||||
result: PolicyResult = policy.passes(self.request)
|
||||
self.assertFalse(result.passing)
|
||||
|
@ -100,7 +100,6 @@ TENANT_APPS = [
|
||||
"authentik.sources.scim",
|
||||
"authentik.stages.authenticator",
|
||||
"authentik.stages.authenticator_duo",
|
||||
"authentik.stages.authenticator_email",
|
||||
"authentik.stages.authenticator_sms",
|
||||
"authentik.stages.authenticator_static",
|
||||
"authentik.stages.authenticator_totp",
|
||||
|
@ -1,85 +0,0 @@
|
||||
"""AuthenticatorEmailStage API Views"""
|
||||
|
||||
from rest_framework import mixins
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.core.api.groups import GroupMemberSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage, EmailDevice
|
||||
|
||||
|
||||
class AuthenticatorEmailStageSerializer(StageSerializer):
|
||||
"""AuthenticatorEmailStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = AuthenticatorEmailStage
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"configure_flow",
|
||||
"friendly_name",
|
||||
"use_global_settings",
|
||||
"host",
|
||||
"port",
|
||||
"username",
|
||||
"password",
|
||||
"use_tls",
|
||||
"use_ssl",
|
||||
"timeout",
|
||||
"from_address",
|
||||
"subject",
|
||||
"token_expiry",
|
||||
"template",
|
||||
]
|
||||
|
||||
|
||||
class AuthenticatorEmailStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""AuthenticatorEmailStage Viewset"""
|
||||
|
||||
queryset = AuthenticatorEmailStage.objects.all()
|
||||
serializer_class = AuthenticatorEmailStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
class EmailDeviceSerializer(ModelSerializer):
|
||||
"""Serializer for email authenticator devices"""
|
||||
|
||||
user = GroupMemberSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = EmailDevice
|
||||
fields = ["name", "pk", "email", "user"]
|
||||
depth = 2
|
||||
extra_kwargs = {
|
||||
"email": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
class EmailDeviceViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
"""Viewset for email authenticator devices"""
|
||||
|
||||
queryset = EmailDevice.objects.all()
|
||||
serializer_class = EmailDeviceSerializer
|
||||
search_fields = ["name"]
|
||||
filterset_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
owner_field = "user"
|
||||
|
||||
|
||||
class EmailAdminDeviceViewSet(ModelViewSet):
|
||||
"""Viewset for email authenticator devices (for admins)"""
|
||||
|
||||
queryset = EmailDevice.objects.all()
|
||||
serializer_class = EmailDeviceSerializer
|
||||
search_fields = ["name"]
|
||||
filterset_fields = ["name"]
|
||||
ordering = ["name"]
|
@ -1,12 +0,0 @@
|
||||
"""Email Authenticator"""
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikStageAuthenticatorEmailConfig(ManagedAppConfig):
|
||||
"""Email Authenticator App config"""
|
||||
|
||||
name = "authentik.stages.authenticator_email"
|
||||
label = "authentik_stages_authenticator_email"
|
||||
verbose_name = "authentik Stages.Authenticator.Email"
|
||||
default = True
|
@ -1,132 +0,0 @@
|
||||
# Generated by Django 5.0.10 on 2025-01-27 20:05
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.lib.utils.time
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0027_auto_20231028_1424"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AuthenticatorEmailStage",
|
||||
fields=[
|
||||
(
|
||||
"stage_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_flows.stage",
|
||||
),
|
||||
),
|
||||
("friendly_name", models.TextField(null=True)),
|
||||
(
|
||||
"use_global_settings",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="When enabled, global Email connection settings will be used and connection settings below will be ignored.",
|
||||
),
|
||||
),
|
||||
("host", models.TextField(default="localhost")),
|
||||
("port", models.IntegerField(default=25)),
|
||||
("username", models.TextField(blank=True, default="")),
|
||||
("password", models.TextField(blank=True, default="")),
|
||||
("use_tls", models.BooleanField(default=False)),
|
||||
("use_ssl", models.BooleanField(default=False)),
|
||||
("timeout", models.IntegerField(default=10)),
|
||||
(
|
||||
"from_address",
|
||||
models.EmailField(default="system@authentik.local", max_length=254),
|
||||
),
|
||||
(
|
||||
"token_expiry",
|
||||
models.TextField(
|
||||
default="minutes=30",
|
||||
help_text="Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
("subject", models.TextField(default="authentik Sign-in code")),
|
||||
("template", models.TextField(default="email/email_otp.html")),
|
||||
(
|
||||
"configure_flow",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Email Authenticator Setup Stage",
|
||||
"verbose_name_plural": "Email Authenticator Setup Stages",
|
||||
},
|
||||
bases=("authentik_flows.stage", models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EmailDevice",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("last_updated", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
help_text="The human-readable name of this device.", max_length=64
|
||||
),
|
||||
),
|
||||
(
|
||||
"confirmed",
|
||||
models.BooleanField(default=True, help_text="Is this device ready for use?"),
|
||||
),
|
||||
("token", models.CharField(blank=True, max_length=16, null=True)),
|
||||
(
|
||||
"valid_until",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now,
|
||||
help_text="The timestamp of the moment of expiry of the saved token.",
|
||||
),
|
||||
),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("last_used", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"stage",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_stages_authenticator_email.authenticatoremailstage",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Email Device",
|
||||
"verbose_name_plural": "Email Devices",
|
||||
"unique_together": {("user", "email")},
|
||||
},
|
||||
),
|
||||
]
|
@ -1,167 +0,0 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.db import models
|
||||
from django.template import TemplateSyntaxError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.exceptions import StageInvalidException
|
||||
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.stages.authenticator.models import SideChannelDevice
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
|
||||
class EmailTemplates(models.TextChoices):
|
||||
"""Templates used for rendering the Email"""
|
||||
|
||||
EMAIL_OTP = (
|
||||
"email/email_otp.html",
|
||||
_("Email OTP"),
|
||||
) # nosec
|
||||
|
||||
|
||||
class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Use Email-based authentication instead of authenticator-based."""
|
||||
|
||||
use_global_settings = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"When enabled, global Email connection settings will be used and "
|
||||
"connection settings below will be ignored."
|
||||
),
|
||||
)
|
||||
|
||||
host = models.TextField(default="localhost")
|
||||
port = models.IntegerField(default=25)
|
||||
username = models.TextField(default="", blank=True)
|
||||
password = models.TextField(default="", blank=True)
|
||||
use_tls = models.BooleanField(default=False)
|
||||
use_ssl = models.BooleanField(default=False)
|
||||
timeout = models.IntegerField(default=10)
|
||||
from_address = models.EmailField(default="system@authentik.local")
|
||||
|
||||
token_expiry = models.TextField(
|
||||
default="minutes=30",
|
||||
validators=[timedelta_string_validator],
|
||||
help_text=_("Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."),
|
||||
)
|
||||
subject = models.TextField(default="authentik Sign-in code")
|
||||
template = models.TextField(default=EmailTemplates.EMAIL_OTP)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
from authentik.stages.authenticator_email.api import AuthenticatorEmailStageSerializer
|
||||
|
||||
return AuthenticatorEmailStageSerializer
|
||||
|
||||
@property
|
||||
def view(self) -> type[View]:
|
||||
from authentik.stages.authenticator_email.stage import AuthenticatorEmailStageView
|
||||
|
||||
return AuthenticatorEmailStageView
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-stage-authenticator-email-form"
|
||||
|
||||
@property
|
||||
def backend_class(self) -> type[BaseEmailBackend]:
|
||||
"""Get the email backend class to use"""
|
||||
return EmailBackend
|
||||
|
||||
@property
|
||||
def backend(self) -> BaseEmailBackend:
|
||||
"""Get fully configured Email Backend instance"""
|
||||
if self.use_global_settings:
|
||||
CONFIG.refresh("email.password")
|
||||
return self.backend_class(
|
||||
host=CONFIG.get("email.host"),
|
||||
port=CONFIG.get_int("email.port"),
|
||||
username=CONFIG.get("email.username"),
|
||||
password=CONFIG.get("email.password"),
|
||||
use_tls=CONFIG.get_bool("email.use_tls", False),
|
||||
use_ssl=CONFIG.get_bool("email.use_ssl", False),
|
||||
timeout=CONFIG.get_int("email.timeout"),
|
||||
)
|
||||
return self.backend_class(
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
use_tls=self.use_tls,
|
||||
use_ssl=self.use_ssl,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
|
||||
def send(self, device: "EmailDevice"):
|
||||
# Lazy import here to avoid circular import
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
|
||||
# Compose the message using templates
|
||||
message = device._compose_email()
|
||||
return send_mails(device.stage, message)
|
||||
|
||||
def __str__(self):
|
||||
return f"Email Authenticator Stage {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Email Authenticator Setup Stage")
|
||||
verbose_name_plural = _("Email Authenticator Setup Stages")
|
||||
|
||||
|
||||
class EmailDevice(SerializerModel, SideChannelDevice):
|
||||
"""Email Device"""
|
||||
|
||||
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||
email = models.EmailField()
|
||||
stage = models.ForeignKey(AuthenticatorEmailStage, on_delete=models.CASCADE)
|
||||
last_used = models.DateTimeField(auto_now=True)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
from authentik.stages.authenticator_email.api import EmailDeviceSerializer
|
||||
|
||||
return EmailDeviceSerializer
|
||||
|
||||
def _compose_email(self) -> TemplateEmailMessage:
|
||||
try:
|
||||
pending_user = self.user
|
||||
stage = self.stage
|
||||
email = self.email
|
||||
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(stage.subject),
|
||||
to=[(pending_user.name, email)],
|
||||
template_name=stage.template,
|
||||
template_context={
|
||||
"user": pending_user,
|
||||
"expires": self.valid_until,
|
||||
"token": self.token,
|
||||
},
|
||||
)
|
||||
return message
|
||||
except TemplateSyntaxError as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=_("Exception occurred while rendering E-mail template"),
|
||||
error=exception_to_string(exc),
|
||||
template=stage.template,
|
||||
).from_http(self.request)
|
||||
raise StageInvalidException from exc
|
||||
|
||||
def __str__(self):
|
||||
if not self.pk:
|
||||
return "New Email Device"
|
||||
return f"Email Device for {self.user_id}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Email Device")
|
||||
verbose_name_plural = _("Email Devices")
|
||||
unique_together = (("user", "email"),)
|
@ -1,177 +0,0 @@
|
||||
"""Email Setup stage"""
|
||||
|
||||
from django.db.models import Q
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.request import QueryDict
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import BooleanField, CharField, IntegerField
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.challenge import (
|
||||
Challenge,
|
||||
ChallengeResponse,
|
||||
WithUserInfoChallenge,
|
||||
)
|
||||
from authentik.flows.exceptions import StageInvalidException
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.lib.utils.email import mask_email
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.authenticator_email.models import (
|
||||
AuthenticatorEmailStage,
|
||||
EmailDevice,
|
||||
)
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
SESSION_KEY_EMAIL_DEVICE = "authentik/stages/authenticator_email/email_device"
|
||||
PLAN_CONTEXT_EMAIL = "email"
|
||||
PLAN_CONTEXT_EMAIL_SENT = "email_sent"
|
||||
PLAN_CONTEXT_EMAIL_OVERRIDE = "email"
|
||||
|
||||
|
||||
class AuthenticatorEmailChallenge(WithUserInfoChallenge):
|
||||
"""Authenticator Email Setup challenge"""
|
||||
|
||||
# Set to true if no previous prompt stage set the email
|
||||
# this stage will also check prompt_data.email
|
||||
email = CharField(default=None, allow_blank=True, allow_null=True)
|
||||
email_required = BooleanField(default=True)
|
||||
component = CharField(default="ak-stage-authenticator-email")
|
||||
|
||||
|
||||
class AuthenticatorEmailChallengeResponse(ChallengeResponse):
|
||||
"""Authenticator Email Challenge response, device is set by get_response_instance"""
|
||||
|
||||
device: EmailDevice
|
||||
|
||||
code = IntegerField(required=False)
|
||||
email = CharField(required=False)
|
||||
|
||||
component = CharField(default="ak-stage-authenticator-email")
|
||||
|
||||
def validate(self, attrs: dict) -> dict:
|
||||
"""Check"""
|
||||
if "code" not in attrs:
|
||||
if "email" not in attrs:
|
||||
raise ValidationError("email required")
|
||||
self.device.email = attrs["email"]
|
||||
self.stage.validate_and_send(attrs["email"])
|
||||
return super().validate(attrs)
|
||||
if not self.device.verify_token(str(attrs["code"])):
|
||||
raise ValidationError(_("Code does not match"))
|
||||
self.device.confirmed = True
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class AuthenticatorEmailStageView(ChallengeStageView):
|
||||
"""Authenticator Email Setup stage"""
|
||||
|
||||
response_class = AuthenticatorEmailChallengeResponse
|
||||
|
||||
def validate_and_send(self, email: str):
|
||||
"""Validate email and send message"""
|
||||
pending_user = self.get_pending_user()
|
||||
|
||||
stage: AuthenticatorEmailStage = self.executor.current_stage
|
||||
if EmailDevice.objects.filter(Q(email=email), stage=stage.pk).exists():
|
||||
raise ValidationError(_("Invalid email"))
|
||||
|
||||
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
|
||||
try:
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(stage.subject),
|
||||
to=[(pending_user.name, email)],
|
||||
language=pending_user.locale(self.request),
|
||||
template_name=stage.template,
|
||||
template_context={
|
||||
"user": pending_user,
|
||||
"expires": device.valid_until,
|
||||
"token": device.token,
|
||||
},
|
||||
)
|
||||
|
||||
send_mails(stage, message)
|
||||
except TemplateSyntaxError as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=_("Exception occurred while rendering E-mail template"),
|
||||
error=exception_to_string(exc),
|
||||
template=stage.template,
|
||||
).from_http(self.request)
|
||||
raise StageInvalidException from exc
|
||||
|
||||
def _has_email(self) -> str | None:
|
||||
context = self.executor.plan.context
|
||||
|
||||
# Check user's email attribute
|
||||
user = self.get_pending_user()
|
||||
if user.email:
|
||||
self.logger.debug("got email from user attributes")
|
||||
return user.email
|
||||
# Check plan context for email
|
||||
if PLAN_CONTEXT_EMAIL in context.get(PLAN_CONTEXT_PROMPT, {}):
|
||||
self.logger.debug("got email from plan context")
|
||||
return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_EMAIL)
|
||||
# Check device for email
|
||||
if SESSION_KEY_EMAIL_DEVICE in self.request.session:
|
||||
self.logger.debug("got email from device in session")
|
||||
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
if device.email == "":
|
||||
return None
|
||||
return device.email
|
||||
return None
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
email = self._has_email()
|
||||
return AuthenticatorEmailChallenge(
|
||||
data={
|
||||
"email": mask_email(email),
|
||||
"email_required": email is None,
|
||||
}
|
||||
)
|
||||
|
||||
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
|
||||
response = super().get_response_instance(data)
|
||||
response.device = self.request.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
return response
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
user = self.get_pending_user()
|
||||
|
||||
stage: AuthenticatorEmailStage = self.executor.current_stage
|
||||
if SESSION_KEY_EMAIL_DEVICE not in self.request.session:
|
||||
device = EmailDevice(user=user, confirmed=False, stage=stage, name="Email Device")
|
||||
valid_secs: int = timedelta_from_string(stage.token_expiry).total_seconds()
|
||||
device.generate_token(valid_secs=valid_secs, commit=False)
|
||||
self.request.session[SESSION_KEY_EMAIL_DEVICE] = device
|
||||
if email := self._has_email():
|
||||
device.email = email
|
||||
try:
|
||||
self.validate_and_send(email)
|
||||
except ValidationError as exc:
|
||||
# We had an email given already (at this point only possible from flow
|
||||
# context), but an error occurred while sending (most likely)
|
||||
# due to a duplicate device, so delete the email we got given, reset the state
|
||||
# (ish) and retry
|
||||
device.email = ""
|
||||
self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop(
|
||||
PLAN_CONTEXT_EMAIL, None
|
||||
)
|
||||
self.request.session.pop(SESSION_KEY_EMAIL_DEVICE, None)
|
||||
self.logger.warning("failed to send email to pre-set address", exc=exc)
|
||||
return self.get(request, *args, **kwargs)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
"""Email Token is validated by challenge"""
|
||||
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
if not device.confirmed:
|
||||
return self.challenge_invalid(response)
|
||||
device.save()
|
||||
del self.request.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
return self.executor.stage_ok()
|
@ -1,44 +0,0 @@
|
||||
{% extends "email/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<tr>
|
||||
<td align="center">
|
||||
<h1>
|
||||
{% blocktrans with username=user.username %}
|
||||
Hi {{ username }},
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td align="center" style="max-width: 300px; padding: 20px 0; color: #212124;">
|
||||
{% blocktrans %}
|
||||
Email MFA code.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" class="btn btn-primary">
|
||||
{{ token }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endblock %}
|
||||
|
||||
{% block sub_content %}
|
||||
<tr>
|
||||
<td style="padding: 20px; font-size: 12px; color: #212124;" align="center">
|
||||
{% blocktrans with expires=expires|timeuntil %}
|
||||
If you did not request this code, please ignore this email. The code above is valid for {{ expires }}.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endblock %}
|
@ -1,13 +0,0 @@
|
||||
{% load i18n %}{% load humanize %}{% autoescape off %}{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %}
|
||||
|
||||
{% blocktrans %}
|
||||
Email MFA code
|
||||
{% endblocktrans %}
|
||||
{{ token }}
|
||||
{% blocktrans with expires=expires|timeuntil %}
|
||||
If you did not request this code, please ignore this email. The code above is valid for {{ expires }}.
|
||||
{% endblocktrans %}
|
||||
|
||||
--
|
||||
Powered by goauthentik.io.
|
||||
{% endautoescape %}
|
@ -1,340 +0,0 @@
|
||||
"""Test Email Authenticator API"""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from django.core import mail
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.db.utils import IntegrityError
|
||||
from django.template.exceptions import TemplateDoesNotExist
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
|
||||
from authentik.flows.models import FlowStageBinding
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.email import mask_email
|
||||
from authentik.stages.authenticator_email.api import (
|
||||
AuthenticatorEmailStageSerializer,
|
||||
EmailDeviceSerializer,
|
||||
)
|
||||
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage, EmailDevice
|
||||
from authentik.stages.authenticator_email.stage import (
|
||||
SESSION_KEY_EMAIL_DEVICE,
|
||||
)
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
|
||||
class TestAuthenticatorEmailStage(FlowTestCase):
|
||||
"""Test Email Authenticator stage"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.flow = create_test_flow()
|
||||
self.user = create_test_admin_user()
|
||||
self.user_noemail = create_test_user(email="")
|
||||
self.stage = AuthenticatorEmailStage.objects.create(
|
||||
name="email-authenticator",
|
||||
use_global_settings=True,
|
||||
from_address="test@authentik.local",
|
||||
configure_flow=self.flow,
|
||||
token_expiry="minutes=30",
|
||||
) # nosec
|
||||
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=0)
|
||||
self.device = EmailDevice.objects.create(
|
||||
user=self.user,
|
||||
stage=self.stage,
|
||||
email="test@authentik.local",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_device_str(self):
|
||||
"""Test string representation of device"""
|
||||
self.assertEqual(str(self.device), f"Email Device for {self.user.pk}")
|
||||
# Test unsaved device
|
||||
unsaved_device = EmailDevice(
|
||||
user=self.user,
|
||||
stage=self.stage,
|
||||
email="test@authentik.local",
|
||||
)
|
||||
self.assertEqual(str(unsaved_device), "New Email Device")
|
||||
|
||||
def test_stage_str(self):
|
||||
"""Test string representation of stage"""
|
||||
self.assertEqual(str(self.stage), f"Email Authenticator Stage {self.stage.name}")
|
||||
|
||||
def test_token_lifecycle(self):
|
||||
"""Test token generation, validation and expiry"""
|
||||
# Initially no token
|
||||
self.assertIsNone(self.device.token)
|
||||
|
||||
# Generate token
|
||||
self.device.generate_token()
|
||||
token = self.device.token
|
||||
self.assertIsNotNone(token)
|
||||
self.assertIsNotNone(self.device.valid_until)
|
||||
self.assertTrue(self.device.valid_until > now())
|
||||
|
||||
# Verify invalid token
|
||||
self.assertFalse(self.device.verify_token("000000"))
|
||||
|
||||
# Verify correct token (should clear token after verification)
|
||||
self.assertTrue(self.device.verify_token(token))
|
||||
self.assertIsNone(self.device.token)
|
||||
|
||||
def test_stage_no_prefill(self):
|
||||
"""Test stage without prefilled email"""
|
||||
self.client.force_login(self.user_noemail)
|
||||
with patch(
|
||||
"authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
self.user_noemail,
|
||||
component="ak-stage-authenticator-email",
|
||||
email_required=True,
|
||||
)
|
||||
|
||||
def test_stage_submit(self):
|
||||
"""Test stage email submission"""
|
||||
# Initialize the flow
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
self.user,
|
||||
component="ak-stage-authenticator-email",
|
||||
email_required=False,
|
||||
)
|
||||
|
||||
# Test email submission with locmem backend
|
||||
def mock_send_mails(stage, *messages):
|
||||
"""Mock send_mails to send directly"""
|
||||
for message in messages:
|
||||
message.send()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class",
|
||||
return_value=EmailBackend,
|
||||
),
|
||||
patch(
|
||||
"authentik.stages.authenticator_email.stage.send_mails",
|
||||
side_effect=mock_send_mails,
|
||||
),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
data={"component": "ak-stage-authenticator-email", "email": "test@example.com"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
sent_mail = mail.outbox[0]
|
||||
self.assertEqual(sent_mail.subject, self.stage.subject)
|
||||
self.assertEqual(sent_mail.to, [f"{self.user} <test@example.com>"])
|
||||
# Get from_address from global email config to test if global settings are being used
|
||||
from_address_global = CONFIG.get("email.from")
|
||||
self.assertEqual(sent_mail.from_email, from_address_global)
|
||||
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
self.user,
|
||||
component="ak-stage-authenticator-email",
|
||||
response_errors={},
|
||||
email_required=False,
|
||||
)
|
||||
|
||||
def test_email_template(self):
|
||||
"""Test email template rendering"""
|
||||
self.device.generate_token()
|
||||
message = self.device._compose_email()
|
||||
|
||||
self.assertIsInstance(message, TemplateEmailMessage)
|
||||
self.assertEqual(message.subject, self.stage.subject)
|
||||
self.assertEqual(message.to, [f"{self.user.name} <{self.device.email}>"])
|
||||
self.assertTrue(self.device.token in message.body)
|
||||
|
||||
def test_duplicate_email(self):
|
||||
"""Test attempting to use same email twice"""
|
||||
email = "test2@authentik.local"
|
||||
# First device
|
||||
EmailDevice.objects.create(
|
||||
user=self.user,
|
||||
stage=self.stage,
|
||||
email=email,
|
||||
)
|
||||
# Attempt to create second device with same email
|
||||
with self.assertRaises(IntegrityError):
|
||||
EmailDevice.objects.create(
|
||||
user=self.user,
|
||||
stage=self.stage,
|
||||
email=email,
|
||||
)
|
||||
|
||||
def test_token_expiry(self):
|
||||
"""Test token expiration behavior"""
|
||||
self.device.generate_token()
|
||||
token = self.device.token
|
||||
# Set token as expired
|
||||
self.device.valid_until = now() - timedelta(minutes=1)
|
||||
self.device.save()
|
||||
# Verify expired token fails
|
||||
self.assertFalse(self.device.verify_token(token))
|
||||
|
||||
def test_template_errors(self):
|
||||
"""Test handling of template errors"""
|
||||
self.stage.template = "{% invalid template %}"
|
||||
with self.assertRaises(TemplateDoesNotExist):
|
||||
self.stage.send(self.device)
|
||||
|
||||
def test_challenge_response_validation(self):
|
||||
"""Test challenge response validation"""
|
||||
# Initialize the flow
|
||||
self.client.force_login(self.user_noemail)
|
||||
with patch(
|
||||
"authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
|
||||
# Test missing code and email
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
data={"component": "ak-stage-authenticator-email"},
|
||||
)
|
||||
self.assertIn("email required", str(response.content))
|
||||
|
||||
# Test invalid code
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
data={"component": "ak-stage-authenticator-email", "code": "000000"},
|
||||
)
|
||||
self.assertIn("Code does not match", str(response.content))
|
||||
|
||||
# Test valid code
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
device = self.device
|
||||
token = device.token
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
data={"component": "ak-stage-authenticator-email", "code": token},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(device.confirmed)
|
||||
|
||||
def test_challenge_generation(self):
|
||||
"""Test challenge generation"""
|
||||
# Test with masked email
|
||||
with patch(
|
||||
"authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
self.user,
|
||||
component="ak-stage-authenticator-email",
|
||||
email_required=False,
|
||||
)
|
||||
masked_email = mask_email(self.user.email)
|
||||
self.assertEqual(masked_email, response.json()["email"])
|
||||
|
||||
# Test without email
|
||||
self.client.force_login(self.user_noemail)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
self.user_noemail,
|
||||
component="ak-stage-authenticator-email",
|
||||
email_required=True,
|
||||
)
|
||||
self.assertIsNone(response.json()["email"])
|
||||
|
||||
def test_session_management(self):
|
||||
"""Test session device management"""
|
||||
# Test device creation in session
|
||||
with patch(
|
||||
"authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
# Delete any existing devices for this test
|
||||
EmailDevice.objects.filter(user=self.user).delete()
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
self.assertIn(SESSION_KEY_EMAIL_DEVICE, self.client.session)
|
||||
device = self.client.session[SESSION_KEY_EMAIL_DEVICE]
|
||||
self.assertIsInstance(device, EmailDevice)
|
||||
self.assertFalse(device.confirmed)
|
||||
self.assertEqual(device.user, self.user)
|
||||
|
||||
# Test device confirmation and cleanup
|
||||
device.confirmed = True
|
||||
device.email = "new_test@authentik.local" # Use a different email
|
||||
self.client.session[SESSION_KEY_EMAIL_DEVICE] = device
|
||||
self.client.session.save()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
data={"component": "ak-stage-authenticator-email", "code": device.token},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(device.confirmed)
|
||||
# Session key should be removed after device is saved
|
||||
device.save()
|
||||
self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, self.client.session)
|
||||
|
||||
def test_model_properties_and_methods(self):
|
||||
"""Test model properties"""
|
||||
device = self.device
|
||||
stage = self.stage
|
||||
|
||||
self.assertEqual(stage.serializer, AuthenticatorEmailStageSerializer)
|
||||
self.assertIsInstance(stage.backend, EmailBackend)
|
||||
self.assertEqual(device.serializer, EmailDeviceSerializer)
|
||||
|
||||
# Test AuthenticatorEmailStage send method
|
||||
with patch(
|
||||
"authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class",
|
||||
return_value=EmailBackend,
|
||||
):
|
||||
self.device.generate_token()
|
||||
# Test EmailDevice _compose_email method
|
||||
message = self.device._compose_email()
|
||||
self.assertIsInstance(message, TemplateEmailMessage)
|
||||
self.assertEqual(message.subject, self.stage.subject)
|
||||
self.assertEqual(message.to, [f"{self.user.name} <{self.device.email}>"])
|
||||
self.assertTrue(self.device.token in message.body)
|
||||
# Test AuthenticatorEmailStage send method
|
||||
self.stage.send(device)
|
||||
|
||||
def test_email_tasks(self):
|
||||
|
||||
email_send_mock = MagicMock()
|
||||
with patch(
|
||||
"authentik.stages.email.tasks.send_mails",
|
||||
email_send_mock,
|
||||
):
|
||||
# Test AuthenticatorEmailStage send method
|
||||
self.stage.send(self.device)
|
||||
email_send_mock.assert_called_once()
|
@ -1,17 +0,0 @@
|
||||
"""API URLs"""
|
||||
|
||||
from authentik.stages.authenticator_email.api import (
|
||||
AuthenticatorEmailStageViewSet,
|
||||
EmailAdminDeviceViewSet,
|
||||
EmailDeviceViewSet,
|
||||
)
|
||||
|
||||
api_urlpatterns = [
|
||||
("authenticators/email", EmailDeviceViewSet),
|
||||
(
|
||||
"authenticators/admin/email",
|
||||
EmailAdminDeviceViewSet,
|
||||
"admin-emaildevice",
|
||||
),
|
||||
("stages/authenticator/email", AuthenticatorEmailStageViewSet),
|
||||
]
|
@ -26,13 +26,10 @@ from authentik.events.middleware import audit_ignore
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
|
||||
from authentik.lib.utils.email import mask_email
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.root.middleware import ClientIPMiddleware
|
||||
from authentik.stages.authenticator import match_token
|
||||
from authentik.stages.authenticator.models import Device
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
from authentik.stages.authenticator_email.models import EmailDevice
|
||||
from authentik.stages.authenticator_sms.models import SMSDevice
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
||||
@ -57,8 +54,6 @@ def get_challenge_for_device(
|
||||
"""Generate challenge for a single device"""
|
||||
if isinstance(device, WebAuthnDevice):
|
||||
return get_webauthn_challenge(request, stage, device)
|
||||
if isinstance(device, EmailDevice):
|
||||
return {"email": mask_email(device.email)}
|
||||
# Code-based challenges have no hints
|
||||
return {}
|
||||
|
||||
@ -108,8 +103,6 @@ def select_challenge(request: HttpRequest, device: Device):
|
||||
"""Callback when the user selected a challenge in the frontend."""
|
||||
if isinstance(device, SMSDevice):
|
||||
select_challenge_sms(request, device)
|
||||
elif isinstance(device, EmailDevice):
|
||||
select_challenge_email(request, device)
|
||||
|
||||
|
||||
def select_challenge_sms(request: HttpRequest, device: SMSDevice):
|
||||
@ -118,13 +111,6 @@ def select_challenge_sms(request: HttpRequest, device: SMSDevice):
|
||||
device.stage.send(device.token, device)
|
||||
|
||||
|
||||
def select_challenge_email(request: HttpRequest, device: EmailDevice):
|
||||
"""Send Email"""
|
||||
valid_secs: int = timedelta_from_string(device.stage.token_expiry).total_seconds()
|
||||
device.generate_token(valid_secs=valid_secs)
|
||||
device.stage.send(device)
|
||||
|
||||
|
||||
def validate_challenge_code(code: str, stage_view: StageView, user: User) -> Device:
|
||||
"""Validate code-based challenges. We test against every device, on purpose, as
|
||||
the user mustn't choose between totp and static devices."""
|
||||
|
@ -1,37 +0,0 @@
|
||||
# Generated by Django 5.0.10 on 2025-01-16 02:48
|
||||
|
||||
import authentik.stages.authenticator_validate.models
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_stages_authenticator_validate",
|
||||
"0013_authenticatorvalidatestage_webauthn_allowed_device_types",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="authenticatorvalidatestage",
|
||||
name="device_classes",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(
|
||||
choices=[
|
||||
("static", "Static"),
|
||||
("totp", "TOTP"),
|
||||
("webauthn", "WebAuthn"),
|
||||
("duo", "Duo"),
|
||||
("sms", "SMS"),
|
||||
("email", "Email"),
|
||||
]
|
||||
),
|
||||
default=authentik.stages.authenticator_validate.models.default_device_classes,
|
||||
help_text="Device classes which can be used to authenticate",
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
@ -20,7 +20,6 @@ class DeviceClasses(models.TextChoices):
|
||||
WEBAUTHN = "webauthn", _("WebAuthn")
|
||||
DUO = "duo", _("Duo")
|
||||
SMS = "sms", _("SMS")
|
||||
EMAIL = "email", _("Email")
|
||||
|
||||
|
||||
def default_device_classes() -> list:
|
||||
@ -31,7 +30,6 @@ def default_device_classes() -> list:
|
||||
DeviceClasses.WEBAUTHN,
|
||||
DeviceClasses.DUO,
|
||||
DeviceClasses.SMS,
|
||||
DeviceClasses.EMAIL,
|
||||
]
|
||||
|
||||
|
||||
|
@ -23,7 +23,6 @@ from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.authenticator import devices_for_user
|
||||
from authentik.stages.authenticator.models import Device
|
||||
from authentik.stages.authenticator_email.models import EmailDevice
|
||||
from authentik.stages.authenticator_sms.models import SMSDevice
|
||||
from authentik.stages.authenticator_validate.challenge import (
|
||||
DeviceChallenge,
|
||||
@ -85,9 +84,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||
|
||||
def validate_code(self, code: str) -> str:
|
||||
"""Validate code-based response, raise error if code isn't allowed"""
|
||||
self._challenge_allowed(
|
||||
[DeviceClasses.TOTP, DeviceClasses.STATIC, DeviceClasses.SMS, DeviceClasses.EMAIL]
|
||||
)
|
||||
self._challenge_allowed([DeviceClasses.TOTP, DeviceClasses.STATIC, DeviceClasses.SMS])
|
||||
self.device = validate_challenge_code(code, self.stage, self.stage.get_pending_user())
|
||||
return code
|
||||
|
||||
@ -120,17 +117,12 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||
if not allowed:
|
||||
raise ValidationError("invalid challenge selected")
|
||||
|
||||
device_class = challenge.get("device_class", "")
|
||||
if device_class == "sms":
|
||||
devices = SMSDevice.objects.filter(pk=int(challenge.get("device_uid", "0")))
|
||||
if not devices.exists():
|
||||
raise ValidationError("invalid challenge selected")
|
||||
select_challenge(self.stage.request, devices.first())
|
||||
elif device_class == "email":
|
||||
devices = EmailDevice.objects.filter(pk=int(challenge.get("device_uid", "0")))
|
||||
if not devices.exists():
|
||||
raise ValidationError("invalid challenge selected")
|
||||
select_challenge(self.stage.request, devices.first())
|
||||
if challenge.get("device_class", "") != "sms":
|
||||
return challenge
|
||||
devices = SMSDevice.objects.filter(pk=int(challenge.get("device_uid", "0")))
|
||||
if not devices.exists():
|
||||
raise ValidationError("invalid challenge selected")
|
||||
select_challenge(self.stage.request, devices.first())
|
||||
return challenge
|
||||
|
||||
def validate_selected_stage(self, stage_pk: str) -> str:
|
||||
|
@ -1,183 +0,0 @@
|
||||
"""Test validator stage for Email devices"""
|
||||
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls.base import reverse
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.flows.models import FlowStageBinding, NotConfiguredAction
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.email import mask_email
|
||||
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage, EmailDevice
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
|
||||
|
||||
class AuthenticatorValidateStageEmailTests(FlowTestCase):
|
||||
"""Test validator stage for Email devices"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_admin_user()
|
||||
self.request_factory = RequestFactory()
|
||||
# Create email authenticator stage
|
||||
self.stage = AuthenticatorEmailStage.objects.create(
|
||||
name="email-authenticator",
|
||||
use_global_settings=True,
|
||||
from_address="test@authentik.local",
|
||||
)
|
||||
# Create identification stage
|
||||
self.ident_stage = IdentificationStage.objects.create(
|
||||
name=generate_id(),
|
||||
user_fields=[UserFields.USERNAME],
|
||||
)
|
||||
# Create validation stage
|
||||
self.validate_stage = AuthenticatorValidateStage.objects.create(
|
||||
name=generate_id(),
|
||||
device_classes=[DeviceClasses.EMAIL],
|
||||
)
|
||||
# Create flow with both stages
|
||||
self.flow = create_test_flow()
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.ident_stage, order=0)
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.validate_stage, order=1)
|
||||
|
||||
def _identify_user(self):
|
||||
"""Helper to identify user in flow"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
{"uid_field": self.user.username},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
def _send_challenge(self, device):
|
||||
"""Helper to send challenge for device"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
{
|
||||
"component": "ak-stage-authenticator-validate",
|
||||
"selected_challenge": {
|
||||
"device_class": "email",
|
||||
"device_uid": str(device.pk),
|
||||
"challenge": {},
|
||||
"last_used": device.last_used.isoformat() if device.last_used else None,
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
def test_happy_path(self):
|
||||
"""Test validator stage with valid code"""
|
||||
# Create a device for our user
|
||||
device = EmailDevice.objects.create(
|
||||
user=self.user,
|
||||
confirmed=True,
|
||||
stage=self.stage,
|
||||
email="xx@0.co",
|
||||
) # Short email for testing purposes
|
||||
|
||||
# First identify the user
|
||||
self._identify_user()
|
||||
|
||||
# Send the challenge
|
||||
response = self._send_challenge(device)
|
||||
response_data = self.assertStageResponse(
|
||||
response,
|
||||
flow=self.flow,
|
||||
component="ak-stage-authenticator-validate",
|
||||
)
|
||||
|
||||
# Get the device challenge from the response and verify it matches
|
||||
device_challenge = response_data["device_challenges"][0]
|
||||
self.assertEqual(device_challenge["device_class"], "email")
|
||||
self.assertEqual(device_challenge["device_uid"], str(device.pk))
|
||||
self.assertEqual(device_challenge["challenge"], {"email": mask_email(device.email)})
|
||||
|
||||
# Generate a token for the device
|
||||
device.generate_token()
|
||||
|
||||
# Submit the valid code
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
{"component": "ak-stage-authenticator-validate", "code": device.token},
|
||||
)
|
||||
# Should redirect to root since this is the last stage
|
||||
self.assertStageRedirects(response, "/")
|
||||
|
||||
def test_no_device(self):
|
||||
"""Test validator stage without configured device"""
|
||||
configuration_stage = AuthenticatorEmailStage.objects.create(
|
||||
name=generate_id(),
|
||||
use_global_settings=True,
|
||||
from_address="test@authentik.local",
|
||||
)
|
||||
stage = AuthenticatorValidateStage.objects.create(
|
||||
name=generate_id(),
|
||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||
device_classes=[DeviceClasses.EMAIL],
|
||||
)
|
||||
stage.configuration_stages.set([configuration_stage])
|
||||
flow = create_test_flow()
|
||||
FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
{"component": "ak-stage-authenticator-validate"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_data = self.assertStageResponse(
|
||||
response,
|
||||
flow=flow,
|
||||
component="ak-stage-authenticator-validate",
|
||||
)
|
||||
self.assertEqual(response_data["configuration_stages"], [])
|
||||
self.assertEqual(response_data["device_challenges"], [])
|
||||
self.assertEqual(
|
||||
response_data["response_errors"],
|
||||
{"non_field_errors": [{"code": "invalid", "string": "Empty response"}]},
|
||||
)
|
||||
|
||||
def test_invalid_code(self):
|
||||
"""Test validator stage with invalid code"""
|
||||
# Create a device for our user
|
||||
device = EmailDevice.objects.create(
|
||||
user=self.user,
|
||||
confirmed=True,
|
||||
stage=self.stage,
|
||||
email="test@authentik.local",
|
||||
)
|
||||
|
||||
# First identify the user
|
||||
self._identify_user()
|
||||
|
||||
# Send the challenge
|
||||
self._send_challenge(device)
|
||||
|
||||
# Generate a token for the device
|
||||
device.generate_token()
|
||||
|
||||
# Try invalid code and verify error message
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
{"component": "ak-stage-authenticator-validate", "code": "invalid"},
|
||||
)
|
||||
response_data = self.assertStageResponse(
|
||||
response,
|
||||
flow=self.flow,
|
||||
component="ak-stage-authenticator-validate",
|
||||
)
|
||||
self.assertEqual(
|
||||
response_data["response_errors"],
|
||||
{
|
||||
"code": [
|
||||
{
|
||||
"code": "invalid",
|
||||
"string": (
|
||||
"Invalid Token. Please ensure the time on your device "
|
||||
"is accurate and try again."
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
@ -13,28 +13,17 @@ from structlog.stdlib import get_logger
|
||||
from authentik.events.models import Event, EventAction, TaskStatus
|
||||
from authentik.events.system_tasks import SystemTask
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.utils import logo_data
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def send_mails(
|
||||
stage: EmailStage | AuthenticatorEmailStage, *messages: list[EmailMultiAlternatives]
|
||||
):
|
||||
"""Wrapper to convert EmailMessage to dict and send it from worker
|
||||
|
||||
Args:
|
||||
stage: Either an EmailStage or AuthenticatorEmailStage instance
|
||||
messages: List of email messages to send
|
||||
Returns:
|
||||
Celery group promise for the email sending tasks
|
||||
"""
|
||||
def send_mails(stage: EmailStage, *messages: list[EmailMultiAlternatives]):
|
||||
"""Wrapper to convert EmailMessage to dict and send it from worker"""
|
||||
tasks = []
|
||||
stage_class = stage.__class__
|
||||
for message in messages:
|
||||
tasks.append(send_mail.s(message.__dict__, stage_class, str(stage.pk)))
|
||||
tasks.append(send_mail.s(message.__dict__, str(stage.pk)))
|
||||
lazy_group = group(*tasks)
|
||||
promise = lazy_group()
|
||||
return promise
|
||||
@ -58,28 +47,23 @@ def get_email_body(email: EmailMultiAlternatives) -> str:
|
||||
retry_backoff=True,
|
||||
base=SystemTask,
|
||||
)
|
||||
def send_mail(
|
||||
self: SystemTask,
|
||||
message: dict[Any, Any],
|
||||
stage_class: EmailStage | AuthenticatorEmailStage = EmailStage,
|
||||
email_stage_pk: str | None = None,
|
||||
):
|
||||
def send_mail(self: SystemTask, message: dict[Any, Any], email_stage_pk: str | None = None):
|
||||
"""Send Email for Email Stage. Retries are scheduled automatically."""
|
||||
self.save_on_success = False
|
||||
message_id = make_msgid(domain=DNS_NAME)
|
||||
self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_")))
|
||||
try:
|
||||
if not email_stage_pk:
|
||||
stage: EmailStage | AuthenticatorEmailStage = stage_class(use_global_settings=True)
|
||||
stage: EmailStage = EmailStage(use_global_settings=True)
|
||||
else:
|
||||
stages = stage_class.objects.filter(pk=email_stage_pk)
|
||||
stages = EmailStage.objects.filter(pk=email_stage_pk)
|
||||
if not stages.exists():
|
||||
self.set_status(
|
||||
TaskStatus.WARNING,
|
||||
"Email stage does not exist anymore. Discarding message.",
|
||||
)
|
||||
return
|
||||
stage: EmailStage | AuthenticatorEmailStage = stages.first()
|
||||
stage: EmailStage = stages.first()
|
||||
try:
|
||||
backend = stage.backend
|
||||
except ValueError as exc:
|
||||
|
@ -1,30 +0,0 @@
|
||||
version: 1
|
||||
metadata:
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "false"
|
||||
name: Example - Email MFA setup flow
|
||||
entries:
|
||||
- attrs:
|
||||
designation: stage_configuration
|
||||
name: Default Email Authenticator Flow
|
||||
title: Setup Email Two-Factor Authentication
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-authenticator-email-setup
|
||||
model: authentik_flows.flow
|
||||
id: flow
|
||||
- attrs:
|
||||
configure_flow: !KeyOf flow
|
||||
friendly_name: Email Authenticator
|
||||
use_global_settings: true
|
||||
token_expiry: minutes=30
|
||||
subject: authentik Sign-in code
|
||||
identifiers:
|
||||
name: default-authenticator-email-setup
|
||||
id: default-authenticator-email-setup
|
||||
model: authentik_stages_authenticator_email.authenticatoremailstage
|
||||
- identifiers:
|
||||
order: 0
|
||||
stage: !KeyOf default-authenticator-email-setup
|
||||
target: !KeyOf flow
|
||||
model: authentik_flows.flowstagebinding
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2025.2.0 Blueprint schema",
|
||||
"title": "authentik 2024.12.3 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@ -1961,86 +1961,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_stages_authenticator_email.authenticatoremailstage"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"present",
|
||||
"created",
|
||||
"must_created"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_email.authenticatoremailstage_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_email.authenticatoremailstage"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_email.authenticatoremailstage"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_stages_authenticator_email.emaildevice"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"present",
|
||||
"created",
|
||||
"must_created"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_email.emaildevice_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_email.emaildevice"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_email.emaildevice"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -4676,7 +4596,6 @@
|
||||
"authentik.sources.scim",
|
||||
"authentik.stages.authenticator",
|
||||
"authentik.stages.authenticator_duo",
|
||||
"authentik.stages.authenticator_email",
|
||||
"authentik.stages.authenticator_sms",
|
||||
"authentik.stages.authenticator_static",
|
||||
"authentik.stages.authenticator_totp",
|
||||
@ -4767,8 +4686,6 @@
|
||||
"authentik_sources_scim.scimsourcepropertymapping",
|
||||
"authentik_stages_authenticator_duo.authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.duodevice",
|
||||
"authentik_stages_authenticator_email.authenticatoremailstage",
|
||||
"authentik_stages_authenticator_email.emaildevice",
|
||||
"authentik_stages_authenticator_sms.authenticatorsmsstage",
|
||||
"authentik_stages_authenticator_sms.smsdevice",
|
||||
"authentik_stages_authenticator_static.authenticatorstaticstage",
|
||||
@ -5232,38 +5149,6 @@
|
||||
},
|
||||
"maxItems": 249,
|
||||
"title": "Countries"
|
||||
},
|
||||
"check_history_distance": {
|
||||
"type": "boolean",
|
||||
"title": "Check history distance"
|
||||
},
|
||||
"history_max_distance_km": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9223372036854775807,
|
||||
"title": "History max distance km"
|
||||
},
|
||||
"distance_tolerance_km": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 2147483647,
|
||||
"title": "Distance tolerance km"
|
||||
},
|
||||
"history_login_count": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 2147483647,
|
||||
"title": "History login count"
|
||||
},
|
||||
"check_impossible_travel": {
|
||||
"type": "boolean",
|
||||
"title": "Check impossible travel"
|
||||
},
|
||||
"impossible_tolerance_km": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 2147483647,
|
||||
"title": "Impossible tolerance km"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@ -6597,8 +6482,6 @@
|
||||
"authentik_core.delete_token",
|
||||
"authentik_core.delete_user",
|
||||
"authentik_core.delete_usersourceconnection",
|
||||
"authentik_core.disable_group_superuser",
|
||||
"authentik_core.enable_group_superuser",
|
||||
"authentik_core.impersonate",
|
||||
"authentik_core.preview_user",
|
||||
"authentik_core.remove_user_from_group",
|
||||
@ -6964,14 +6847,6 @@
|
||||
"authentik_stages_authenticator_duo.delete_duodevice",
|
||||
"authentik_stages_authenticator_duo.view_authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.view_duodevice",
|
||||
"authentik_stages_authenticator_email.add_authenticatoremailstage",
|
||||
"authentik_stages_authenticator_email.add_emaildevice",
|
||||
"authentik_stages_authenticator_email.change_authenticatoremailstage",
|
||||
"authentik_stages_authenticator_email.change_emaildevice",
|
||||
"authentik_stages_authenticator_email.delete_authenticatoremailstage",
|
||||
"authentik_stages_authenticator_email.delete_emaildevice",
|
||||
"authentik_stages_authenticator_email.view_authenticatoremailstage",
|
||||
"authentik_stages_authenticator_email.view_emaildevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_authenticatorendpointgdtcstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_endpointdevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_endpointdeviceconnection",
|
||||
@ -9097,239 +8972,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_stages_authenticator_email.authenticatoremailstage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
"flow_set": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 50,
|
||||
"minLength": 1,
|
||||
"pattern": "^[-a-zA-Z0-9_]+$",
|
||||
"title": "Slug",
|
||||
"description": "Visible in the URL."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Title",
|
||||
"description": "Shown as the Title in Flow pages."
|
||||
},
|
||||
"designation": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"authentication",
|
||||
"authorization",
|
||||
"invalidation",
|
||||
"enrollment",
|
||||
"unenrollment",
|
||||
"recovery",
|
||||
"stage_configuration"
|
||||
],
|
||||
"title": "Designation",
|
||||
"description": "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
|
||||
},
|
||||
"policy_engine_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"all",
|
||||
"any"
|
||||
],
|
||||
"title": "Policy engine mode"
|
||||
},
|
||||
"compatibility_mode": {
|
||||
"type": "boolean",
|
||||
"title": "Compatibility mode",
|
||||
"description": "Enable compatibility mode, increases compatibility with password managers on mobile devices."
|
||||
},
|
||||
"layout": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"stacked",
|
||||
"content_left",
|
||||
"content_right",
|
||||
"sidebar_left",
|
||||
"sidebar_right"
|
||||
],
|
||||
"title": "Layout"
|
||||
},
|
||||
"denied_action": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"message_continue",
|
||||
"message",
|
||||
"continue"
|
||||
],
|
||||
"title": "Denied action",
|
||||
"description": "Configure what should happen when a flow denies access to a user."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"slug",
|
||||
"title",
|
||||
"designation"
|
||||
]
|
||||
},
|
||||
"title": "Flow set"
|
||||
},
|
||||
"configure_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Configure flow",
|
||||
"description": "Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage."
|
||||
},
|
||||
"friendly_name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"minLength": 1,
|
||||
"title": "Friendly name"
|
||||
},
|
||||
"use_global_settings": {
|
||||
"type": "boolean",
|
||||
"title": "Use global settings",
|
||||
"description": "When enabled, global Email connection settings will be used and connection settings below will be ignored."
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Host"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"minimum": -2147483648,
|
||||
"maximum": 2147483647,
|
||||
"title": "Port"
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"title": "Username"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"title": "Password"
|
||||
},
|
||||
"use_tls": {
|
||||
"type": "boolean",
|
||||
"title": "Use tls"
|
||||
},
|
||||
"use_ssl": {
|
||||
"type": "boolean",
|
||||
"title": "Use ssl"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"minimum": -2147483648,
|
||||
"maximum": 2147483647,
|
||||
"title": "Timeout"
|
||||
},
|
||||
"from_address": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"maxLength": 254,
|
||||
"minLength": 1,
|
||||
"title": "From address"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Subject"
|
||||
},
|
||||
"token_expiry": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Token expiry",
|
||||
"description": "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Template"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_stages_authenticator_email.authenticatoremailstage_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_authenticatoremailstage",
|
||||
"change_authenticatoremailstage",
|
||||
"delete_authenticatoremailstage",
|
||||
"view_authenticatoremailstage"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_stages_authenticator_email.emaildevice": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 1,
|
||||
"title": "Name",
|
||||
"description": "The human-readable name of this device."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_stages_authenticator_email.emaildevice_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_emaildevice",
|
||||
"change_emaildevice",
|
||||
"delete_emaildevice",
|
||||
"view_emaildevice"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_stages_authenticator_sms.authenticatorsmsstage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -10019,8 +9661,7 @@
|
||||
"totp",
|
||||
"webauthn",
|
||||
"duo",
|
||||
"sms",
|
||||
"email"
|
||||
"sms"
|
||||
],
|
||||
"title": "Device classes"
|
||||
},
|
||||
@ -12970,8 +12611,6 @@
|
||||
"enum": [
|
||||
"add_user_to_group",
|
||||
"remove_user_from_group",
|
||||
"enable_group_superuser",
|
||||
"disable_group_superuser",
|
||||
"add_group",
|
||||
"change_group",
|
||||
"delete_group",
|
||||
@ -13104,8 +12743,6 @@
|
||||
"authentik_core.delete_token",
|
||||
"authentik_core.delete_user",
|
||||
"authentik_core.delete_usersourceconnection",
|
||||
"authentik_core.disable_group_superuser",
|
||||
"authentik_core.enable_group_superuser",
|
||||
"authentik_core.impersonate",
|
||||
"authentik_core.preview_user",
|
||||
"authentik_core.remove_user_from_group",
|
||||
@ -13471,14 +13108,6 @@
|
||||
"authentik_stages_authenticator_duo.delete_duodevice",
|
||||
"authentik_stages_authenticator_duo.view_authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.view_duodevice",
|
||||
"authentik_stages_authenticator_email.add_authenticatoremailstage",
|
||||
"authentik_stages_authenticator_email.add_emaildevice",
|
||||
"authentik_stages_authenticator_email.change_authenticatoremailstage",
|
||||
"authentik_stages_authenticator_email.change_emaildevice",
|
||||
"authentik_stages_authenticator_email.delete_authenticatoremailstage",
|
||||
"authentik_stages_authenticator_email.delete_emaildevice",
|
||||
"authentik_stages_authenticator_email.view_authenticatoremailstage",
|
||||
"authentik_stages_authenticator_email.view_emaildevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_authenticatorendpointgdtcstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_endpointdevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_endpointdeviceconnection",
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.3}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -54,7 +54,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.3}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
6
go.mod
6
go.mod
@ -26,10 +26,10 @@ require (
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/sethvargo/go-envconfig v1.1.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2024123.6
|
||||
goauthentik.io/api/v3 v3.2024123.4
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.26.0
|
||||
golang.org/x/sync v0.11.0
|
||||
@ -71,7 +71,7 @@ require (
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
|
14
go.sum
14
go.sum
@ -57,7 +57,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -259,10 +259,10 @@ github.com/sethvargo/go-envconfig v1.1.1/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2024123.6 h1:AGOCa7Fc/9eONCPEW4sEhTiyEBvxN57Lfqz1zm6Gy98=
|
||||
goauthentik.io/api/v3 v3.2024123.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2024123.4 h1:JYLsUjkJ7kT+jHO72DyFTXFwKEGAcOOlLh36SRG9BDw=
|
||||
goauthentik.io/api/v3 v3.2024123.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2025.2.0"
|
||||
const VERSION = "2024.12.3"
|
||||
|
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.179.0",
|
||||
"aws-cdk": "^2.178.2",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,9 +17,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.179.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.179.0.tgz",
|
||||
"integrity": "sha512-aA2+8S2g4UBQHkUEt0mYd16VLt/ucR+QfyUJi34LDKRAhOCNDjPCZ4z9z/JEDyuni0BdzsYA55pnpDN9tMULpA==",
|
||||
"version": "2.178.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.178.2.tgz",
|
||||
"integrity": "sha512-ojMCMnBGinvDUD6+BOOlUOB9pjsYXoQdFVbf4bvi3dy3nwn557r0j6qDUcJMeikzPJ6YWzfAdL0fYxBZg4xcOg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.179.0",
|
||||
"aws-cdk": "^2.178.2",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2025.2.0
|
||||
Default: 2024.12.3
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
Binary file not shown.
@ -8,7 +8,7 @@
|
||||
# 刘松, 2022
|
||||
# Tianhao Chai <cth451@gmail.com>, 2024
|
||||
# Jens L. <jens@goauthentik.io>, 2024
|
||||
# deluxghost, 2025
|
||||
# deluxghost, 2024
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
@ -17,7 +17,7 @@ msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-14 14:49+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Last-Translator: deluxghost, 2024\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@ -568,39 +568,39 @@ msgstr "签名密钥"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Key used to sign the SSF Events."
|
||||
msgstr "用于签名 SSF 时间的密钥。"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Shared Signals Framework Provider"
|
||||
msgstr "Shared Signals Framework 提供程序"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Shared Signals Framework Providers"
|
||||
msgstr "Shared Signals Framework 提供程序"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Add stream to SSF provider"
|
||||
msgstr "向 SSF 提供程序添加流"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream"
|
||||
msgstr "SSF 流"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Streams"
|
||||
msgstr "SSF 流"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream Event"
|
||||
msgstr "SSF 流事件"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream Events"
|
||||
msgstr "SSF 流事件"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/tasks.py
|
||||
msgid "Failed to send request"
|
||||
msgstr "发送请求失败"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
||||
@ -878,7 +878,7 @@ msgstr "在流程规划过程中评估策略。"
|
||||
|
||||
#: authentik/flows/models.py
|
||||
msgid "Evaluate policies when the Stage is presented to the user."
|
||||
msgstr "在阶段呈现给用户时评估策略。"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/models.py
|
||||
msgid ""
|
||||
|
@ -7,7 +7,7 @@
|
||||
# Chen Zhikai, 2022
|
||||
# 刘松, 2022
|
||||
# Jens L. <jens@goauthentik.io>, 2024
|
||||
# deluxghost, 2025
|
||||
# deluxghost, 2024
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
@ -16,7 +16,7 @@ msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-14 14:49+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Last-Translator: deluxghost, 2024\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@ -567,39 +567,39 @@ msgstr "签名密钥"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Key used to sign the SSF Events."
|
||||
msgstr "用于签名 SSF 时间的密钥。"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Shared Signals Framework Provider"
|
||||
msgstr "Shared Signals Framework 提供程序"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Shared Signals Framework Providers"
|
||||
msgstr "Shared Signals Framework 提供程序"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Add stream to SSF provider"
|
||||
msgstr "向 SSF 提供程序添加流"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream"
|
||||
msgstr "SSF 流"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Streams"
|
||||
msgstr "SSF 流"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream Event"
|
||||
msgstr "SSF 流事件"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream Events"
|
||||
msgstr "SSF 流事件"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/providers/ssf/tasks.py
|
||||
msgid "Failed to send request"
|
||||
msgstr "发送请求失败"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
||||
@ -877,7 +877,7 @@ msgstr "在流程规划过程中评估策略。"
|
||||
|
||||
#: authentik/flows/models.py
|
||||
msgid "Evaluate policies when the Stage is presented to the user."
|
||||
msgstr "在阶段呈现给用户时评估策略。"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/models.py
|
||||
msgid ""
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.2.0",
|
||||
"version": "2024.12.3",
|
||||
"private": true
|
||||
}
|
||||
|
88
poetry.lock
generated
88
poetry.lock
generated
@ -358,6 +358,22 @@ jsii = ">=1.105.0,<2.0.0"
|
||||
publication = ">=0.0.3"
|
||||
typeguard = ">=2.13.3,<4.3.0"
|
||||
|
||||
[[package]]
|
||||
name = "aws-cdk-asset-kubectl-v20"
|
||||
version = "2.1.3"
|
||||
description = "A Lambda Layer that contains kubectl v1.20"
|
||||
optional = false
|
||||
python-versions = "~=3.8"
|
||||
files = [
|
||||
{file = "aws_cdk.asset_kubectl_v20-2.1.3-py3-none-any.whl", hash = "sha256:d5612e5bd03c215a28ce53193b1144ecf4e93b3b6779563c046a8a74d83a3979"},
|
||||
{file = "aws_cdk_asset_kubectl_v20-2.1.3.tar.gz", hash = "sha256:237cd8530d9e8be0bbc7159af927dbb6b7f91bf3f4099c8ef4d9a213b34264be"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
jsii = ">=1.103.1,<2.0.0"
|
||||
publication = ">=0.0.3"
|
||||
typeguard = ">=2.13.3,<5.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "aws-cdk-asset-node-proxy-agent-v6"
|
||||
version = "2.1.0"
|
||||
@ -392,17 +408,18 @@ typeguard = ">=2.13.3,<4.3.0"
|
||||
|
||||
[[package]]
|
||||
name = "aws-cdk-lib"
|
||||
version = "2.179.0"
|
||||
version = "2.178.2"
|
||||
description = "Version 2 of the AWS Cloud Development Kit library"
|
||||
optional = false
|
||||
python-versions = "~=3.8"
|
||||
files = [
|
||||
{file = "aws_cdk_lib-2.179.0-py3-none-any.whl", hash = "sha256:1d7b88ee69067b8d58dac9eeb6697bbaf5d5c032a3070898389c41e7c4f3e3d7"},
|
||||
{file = "aws_cdk_lib-2.179.0.tar.gz", hash = "sha256:b653a55754f4020a4b36e4ae183d213e76e27b18b842cbf9e430e9eccb700550"},
|
||||
{file = "aws_cdk_lib-2.178.2-py3-none-any.whl", hash = "sha256:624383e57fe2b32f7d0fc098b78b4cd21d19ae3af3f24b01f32ec4795baaee25"},
|
||||
{file = "aws_cdk_lib-2.178.2.tar.gz", hash = "sha256:c00757885b74023350bb34f388f6447155e802ecf827e595bda917098a4925fe"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
"aws-cdk.asset-awscli-v1" = ">=2.2.208,<3.0.0"
|
||||
"aws-cdk.asset-kubectl-v20" = ">=2.1.3,<3.0.0"
|
||||
"aws-cdk.asset-node-proxy-agent-v6" = ">=2.1.0,<3.0.0"
|
||||
"aws-cdk.cloud-assembly-schema" = ">=39.2.0,<40.0.0"
|
||||
constructs = ">=10.0.0,<11.0.0"
|
||||
@ -449,13 +466,13 @@ typing-extensions = ">=4.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "bandit"
|
||||
version = "1.8.3"
|
||||
version = "1.8.2"
|
||||
description = "Security oriented static analyser for python code."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "bandit-1.8.3-py3-none-any.whl", hash = "sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8"},
|
||||
{file = "bandit-1.8.3.tar.gz", hash = "sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a"},
|
||||
{file = "bandit-1.8.2-py3-none-any.whl", hash = "sha256:df6146ad73dd30e8cbda4e29689ddda48364e36ff655dbfc86998401fcf1721f"},
|
||||
{file = "bandit-1.8.2.tar.gz", hash = "sha256:e00ad5a6bc676c0954669fe13818024d66b70e42cf5adb971480cf3b671e835f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1400,13 +1417,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "django-filter"
|
||||
version = "25.1"
|
||||
version = "24.3"
|
||||
description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "django_filter-25.1-py3-none-any.whl", hash = "sha256:4fa48677cf5857b9b1347fed23e355ea792464e0fe07244d1fdfb8a806215b80"},
|
||||
{file = "django_filter-25.1.tar.gz", hash = "sha256:1ec9eef48fa8da1c0ac9b411744b16c3f4c31176c867886e4c48da369c407153"},
|
||||
{file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"},
|
||||
{file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1503,13 +1520,13 @@ hiredis = ["redis[hiredis] (>=3,!=4.0.0,!=4.0.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "django-storages"
|
||||
version = "1.14.5"
|
||||
version = "1.14.4"
|
||||
description = "Support for many storage backends in Django"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "django_storages-1.14.5-py3-none-any.whl", hash = "sha256:5ce9c69426f24f379821fd688442314e4aa03de87ae43183c4e16915f4c165d4"},
|
||||
{file = "django_storages-1.14.5.tar.gz", hash = "sha256:ace80dbee311258453e30cd5cfd91096b834180ccf09bc1f4d2cb6d38d68571a"},
|
||||
{file = "django-storages-1.14.4.tar.gz", hash = "sha256:69aca94d26e6714d14ad63f33d13619e697508ee33ede184e462ed766dc2a73f"},
|
||||
{file = "django_storages-1.14.4-py3-none-any.whl", hash = "sha256:d61930acb4a25e3aebebc6addaf946a3b1df31c803a6bf1af2f31c9047febaa3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1520,7 +1537,7 @@ Django = ">=3.2"
|
||||
azure = ["azure-core (>=1.13)", "azure-storage-blob (>=12)"]
|
||||
boto3 = ["boto3 (>=1.4.4)"]
|
||||
dropbox = ["dropbox (>=7.2.1)"]
|
||||
google = ["google-cloud-storage (>=1.32)"]
|
||||
google = ["google-cloud-storage (>=1.27)"]
|
||||
libcloud = ["apache-libcloud"]
|
||||
s3 = ["boto3 (>=1.4.4)"]
|
||||
sftp = ["paramiko (>=1.15)"]
|
||||
@ -1867,17 +1884,6 @@ files = [
|
||||
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "geographiclib"
|
||||
version = "2.0"
|
||||
description = "The geodesic routines from GeographicLib"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "geographiclib-2.0-py3-none-any.whl", hash = "sha256:6b7225248e45ff7edcee32becc4e0a1504c606ac5ee163a5656d482e0cd38734"},
|
||||
{file = "geographiclib-2.0.tar.gz", hash = "sha256:f7f41c85dc3e1c2d3d935ec86660dc3b2c848c83e17f9a9e51ba9d5146a15859"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "geoip2"
|
||||
version = "5.0.1"
|
||||
@ -1897,29 +1903,6 @@ requests = ">=2.24.0,<3.0.0"
|
||||
[package.extras]
|
||||
test = ["pytest-httpserver (>=1.0.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "geopy"
|
||||
version = "2.4.1"
|
||||
description = "Python Geocoding Toolbox"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7"},
|
||||
{file = "geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
geographiclib = ">=1.52,<3"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp"]
|
||||
dev = ["coverage", "flake8 (>=5.0,<5.1)", "isort (>=5.10.0,<5.11.0)", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "readme-renderer", "sphinx (<=4.3.2)", "sphinx-issues", "sphinx-rtd-theme (>=0.5.0)"]
|
||||
dev-docs = ["readme-renderer", "sphinx (<=4.3.2)", "sphinx-issues", "sphinx-rtd-theme (>=0.5.0)"]
|
||||
dev-lint = ["flake8 (>=5.0,<5.1)", "isort (>=5.10.0,<5.11.0)"]
|
||||
dev-test = ["coverage", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "sphinx (<=4.3.2)"]
|
||||
requests = ["requests (>=2.16.2)", "urllib3 (>=1.24.2)"]
|
||||
timezone = ["pytz"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-core"
|
||||
version = "2.19.1"
|
||||
@ -4628,13 +4611,13 @@ websocket-client = ">=1.8,<2.0"
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.22.0"
|
||||
version = "2.21.0"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "sentry_sdk-2.22.0-py2.py3-none-any.whl", hash = "sha256:3d791d631a6c97aad4da7074081a57073126c69487560c6f8bffcf586461de66"},
|
||||
{file = "sentry_sdk-2.22.0.tar.gz", hash = "sha256:b4bf43bb38f547c84b2eadcefbe389b36ef75f3f38253d7a74d6b928c07ae944"},
|
||||
{file = "sentry_sdk-2.21.0-py2.py3-none-any.whl", hash = "sha256:7623cfa9e2c8150948a81ca253b8e2bfe4ce0b96ab12f8cd78e3ac9c490fd92f"},
|
||||
{file = "sentry_sdk-2.21.0.tar.gz", hash = "sha256:a6d38e0fb35edda191acf80b188ec713c863aaa5ad8d5798decb8671d02077b6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4678,7 +4661,6 @@ sanic = ["sanic (>=0.8)"]
|
||||
sqlalchemy = ["sqlalchemy (>=1.2)"]
|
||||
starlette = ["starlette (>=0.19.1)"]
|
||||
starlite = ["starlite (>=1.48)"]
|
||||
statsig = ["statsig (>=0.55.3)"]
|
||||
tornado = ["tornado (>=6)"]
|
||||
unleash = ["UnleashClient (>=6.0.1)"]
|
||||
|
||||
@ -5865,4 +5847,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "~3.12"
|
||||
content-hash = "8a6bfd4833e415a9f4f613ab4f33e60c8332b9f5743583222cdb7190f6286216"
|
||||
content-hash = "a3915ac2ef2bb53f7cd67070912cdaf717c3bf73ed972fa337a9b07fce162451"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2025.2.0"
|
||||
version = "2024.12.3"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
@ -113,7 +113,6 @@ duo-client = "*"
|
||||
fido2 = "*"
|
||||
flower = "*"
|
||||
geoip2 = "*"
|
||||
geopy = "*"
|
||||
google-api-python-client = "*"
|
||||
gunicorn = "*"
|
||||
gssapi = "*"
|
||||
|
1230
schema.yml
1230
schema.yml
File diff suppressed because it is too large
Load Diff
350
web/package-lock.json
generated
350
web/package-lock.json
generated
@ -23,7 +23,7 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2024.12.3-1739814462",
|
||||
"@goauthentik/api": "^2024.12.3-1739449824",
|
||||
"@lit-labs/ssr": "^3.2.2",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
@ -42,12 +42,12 @@
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.38.1",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"dompurify": "^3.2.4",
|
||||
"dompurify": "^3.1.7",
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"lit": "^3.2.0",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.4.1",
|
||||
"mermaid": "^11.2.1",
|
||||
"rapidoc": "^9.3.7",
|
||||
"showdown": "^2.1.0",
|
||||
"style-mod": "^4.1.2",
|
||||
@ -1814,9 +1814,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2024.12.3-1739814462",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739814462.tgz",
|
||||
"integrity": "sha512-qWGsq7zP0rG1PfjZA+iimaX4cVkd1n2JA/WceTOKgBmqnomQSI7SJNkdSpD+Qdy76PI0UuQWN73PInq/3rmm5Q=="
|
||||
"version": "2024.12.3-1739449824",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739449824.tgz",
|
||||
"integrity": "sha512-0M2SkvqpdjYgWOtaRLO41gTTyo43WPXlWbcfqCxfCJUoi1c3VGT5mozFCgRM21mY6+a3tKPHh4O28qDuz5gthw=="
|
||||
},
|
||||
"node_modules/@goauthentik/web": {
|
||||
"resolved": "",
|
||||
@ -5728,259 +5728,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "*",
|
||||
"@types/d3-axis": "*",
|
||||
"@types/d3-brush": "*",
|
||||
"@types/d3-chord": "*",
|
||||
"@types/d3-color": "*",
|
||||
"@types/d3-contour": "*",
|
||||
"@types/d3-delaunay": "*",
|
||||
"@types/d3-dispatch": "*",
|
||||
"@types/d3-drag": "*",
|
||||
"@types/d3-dsv": "*",
|
||||
"@types/d3-ease": "*",
|
||||
"@types/d3-fetch": "*",
|
||||
"@types/d3-force": "*",
|
||||
"@types/d3-format": "*",
|
||||
"@types/d3-geo": "*",
|
||||
"@types/d3-hierarchy": "*",
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-path": "*",
|
||||
"@types/d3-polygon": "*",
|
||||
"@types/d3-quadtree": "*",
|
||||
"@types/d3-random": "*",
|
||||
"@types/d3-scale": "*",
|
||||
"@types/d3-scale-chromatic": "*",
|
||||
"@types/d3-selection": "*",
|
||||
"@types/d3-shape": "*",
|
||||
"@types/d3-time": "*",
|
||||
"@types/d3-time-format": "*",
|
||||
"@types/d3-timer": "*",
|
||||
"@types/d3-transition": "*",
|
||||
"@types/d3-zoom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-axis": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
|
||||
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-brush": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
|
||||
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-chord": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
|
||||
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-contour": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
|
||||
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "*",
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-delaunay": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-dispatch": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz",
|
||||
"integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-dsv": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
||||
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-fetch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
||||
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-dsv": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-force": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
||||
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-format": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
|
||||
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-geo": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
||||
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-hierarchy": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
||||
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-polygon": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
|
||||
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-quadtree": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
||||
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-random": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
|
||||
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-time-format": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
|
||||
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dompurify": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||
@ -6055,12 +5802,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
|
||||
@ -10241,7 +9982,6 @@
|
||||
"version": "7.9.0",
|
||||
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
|
||||
"integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "3",
|
||||
"d3-axis": "3",
|
||||
@ -10282,7 +10022,6 @@
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
@ -10294,7 +10033,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
|
||||
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10303,7 +10041,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
|
||||
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
@ -10319,7 +10056,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
|
||||
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "1 - 3"
|
||||
},
|
||||
@ -10331,7 +10067,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10340,7 +10075,6 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
|
||||
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "^3.2.0"
|
||||
},
|
||||
@ -10352,7 +10086,6 @@
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"delaunator": "5"
|
||||
},
|
||||
@ -10364,7 +10097,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10373,7 +10105,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
@ -10386,7 +10117,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
||||
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "7",
|
||||
"iconv-lite": "0.6",
|
||||
@ -10411,7 +10141,6 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
@ -10420,7 +10149,6 @@
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
@ -10432,7 +10160,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10441,7 +10168,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
|
||||
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dsv": "1 - 3"
|
||||
},
|
||||
@ -10453,7 +10179,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
|
||||
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-quadtree": "1 - 3",
|
||||
@ -10467,7 +10192,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10476,7 +10200,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
|
||||
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.5.0 - 3"
|
||||
},
|
||||
@ -10488,7 +10211,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
|
||||
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10497,7 +10219,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
@ -10509,7 +10230,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10518,7 +10238,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
|
||||
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10527,7 +10246,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10536,7 +10254,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
|
||||
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10580,7 +10297,6 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
@ -10596,7 +10312,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-interpolate": "1 - 3"
|
||||
@ -10609,7 +10324,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10618,7 +10332,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
@ -10630,7 +10343,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
@ -10642,7 +10354,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
@ -10654,7 +10365,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -10663,7 +10373,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
@ -10682,7 +10391,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
@ -10695,12 +10403,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dagre-d3-es": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz",
|
||||
"integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==",
|
||||
"license": "MIT",
|
||||
"version": "7.0.10",
|
||||
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz",
|
||||
"integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==",
|
||||
"dependencies": {
|
||||
"d3": "^7.9.0",
|
||||
"d3": "^7.8.2",
|
||||
"lodash-es": "^4.17.21"
|
||||
}
|
||||
},
|
||||
@ -10939,7 +10646,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
|
||||
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"robust-predicates": "^3.0.2"
|
||||
}
|
||||
@ -11091,13 +10797,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
|
||||
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
|
||||
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)"
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.1.0",
|
||||
@ -13955,7 +13658,6 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -16080,23 +15782,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid": {
|
||||
"version": "11.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.1.tgz",
|
||||
"integrity": "sha512-Mb01JT/x6CKDWaxigwfZYuYmDZ6xtrNwNlidKZwkSrDaY9n90tdrJTV5Umk+wP1fZscGptmKFXHsXMDEVZ+Q6A==",
|
||||
"license": "MIT",
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.3.0.tgz",
|
||||
"integrity": "sha512-fFmf2gRXLtlGzug4wpIGN+rQdZ30M8IZEB1D3eZkXNqC7puhqeURBcD/9tbwXsqBO+A6Nzzo3MSSepmnw5xSeg==",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.1",
|
||||
"@iconify/utils": "^2.1.32",
|
||||
"@mermaid-js/parser": "^0.3.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"cytoscape": "^3.29.2",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"dagre-d3-es": "7.0.11",
|
||||
"dagre-d3-es": "7.0.10",
|
||||
"dayjs": "^1.11.10",
|
||||
"dompurify": "^3.2.1",
|
||||
"dompurify": "^3.0.11 <3.1.7",
|
||||
"katex": "^0.16.9",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
@ -16107,6 +15807,12 @@
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid/node_modules/dompurify": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz",
|
||||
"integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)"
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
@ -19278,8 +18984,7 @@
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
|
||||
"license": "Unlicense"
|
||||
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.24.0",
|
||||
@ -19549,8 +19254,7 @@
|
||||
"node_modules/rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
|
@ -11,7 +11,7 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2024.12.3-1739814462",
|
||||
"@goauthentik/api": "^2024.12.3-1739449824",
|
||||
"@lit-labs/ssr": "^3.2.2",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
@ -30,12 +30,12 @@
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.38.1",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"dompurify": "^3.2.4",
|
||||
"dompurify": "^3.1.7",
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"lit": "^3.2.0",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.4.1",
|
||||
"mermaid": "^11.2.1",
|
||||
"rapidoc": "^9.3.7",
|
||||
"showdown": "^2.1.0",
|
||||
"style-mod": "^4.1.2",
|
||||
|
@ -8,6 +8,7 @@ import "@goauthentik/admin/policies/password/PasswordPolicyForm";
|
||||
import "@goauthentik/admin/policies/reputation/ReputationPolicyForm";
|
||||
import "@goauthentik/admin/rbac/ObjectPermissionModal";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { PFSize } from "@goauthentik/common/enums";
|
||||
import { PFColor } from "@goauthentik/elements/Label";
|
||||
import "@goauthentik/elements/forms/ConfirmationForm";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
@ -21,7 +22,6 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { PoliciesApi, Policy } from "@goauthentik/api";
|
||||
|
||||
@ -71,7 +71,12 @@ export class PolicyListPage extends TablePage<Policy> {
|
||||
${msg("Warning: Policy is not assigned.")}
|
||||
</ak-label>`}`,
|
||||
html`${item.verboseName}`,
|
||||
html` <ak-forms-modal>
|
||||
html` <ak-forms-modal
|
||||
size=${item.component === "ak-policy-expression-form"
|
||||
? PFSize.XLarge
|
||||
: PFSize.Large}
|
||||
?fullHeight=${item.component === "ak-policy-expression-form"}
|
||||
>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg(str`Update ${item.verboseName}`)} </span>
|
||||
<ak-proxy-form
|
||||
@ -79,7 +84,7 @@ export class PolicyListPage extends TablePage<Policy> {
|
||||
.args=${{
|
||||
instancePk: item.pk,
|
||||
}}
|
||||
type=${ifDefined(item.component)}
|
||||
type=${item.component}
|
||||
>
|
||||
</ak-proxy-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
|
@ -87,7 +87,10 @@ export class PolicyWizard extends AKElement {
|
||||
slot=${`type-${type.component}-${type.modelName}`}
|
||||
.sidebarLabel=${() => msg(str`Create ${type.name}`)}
|
||||
>
|
||||
<ak-proxy-form type=${type.component}></ak-proxy-form>
|
||||
<ak-proxy-form
|
||||
?showPreview=${false}
|
||||
type=${type.component}
|
||||
></ak-proxy-form>
|
||||
</ak-wizard-page-form>
|
||||
`;
|
||||
})}
|
||||
|
@ -1,25 +1,64 @@
|
||||
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { docLink } from "@goauthentik/common/global";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { CSSResult, TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { ExpressionPolicy, PoliciesApi } from "@goauthentik/api";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreUsersListRequest,
|
||||
ExpressionPolicy,
|
||||
PoliciesApi,
|
||||
PolicyTestResult,
|
||||
ResponseError,
|
||||
SessionUser,
|
||||
User,
|
||||
ValidationErrorFromJSON,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-policy-expression-form")
|
||||
export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
||||
loadInstance(pk: string): Promise<ExpressionPolicy> {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionRetrieve({
|
||||
@property({ type: Boolean })
|
||||
showPreview = true;
|
||||
|
||||
@state()
|
||||
preview?: PolicyTestResult;
|
||||
|
||||
@state()
|
||||
previewError?: string[];
|
||||
|
||||
@state()
|
||||
user?: SessionUser;
|
||||
|
||||
@state()
|
||||
previewLoading = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(PFGrid, PFStack, PFTitle);
|
||||
}
|
||||
|
||||
async loadInstance(pk: string): Promise<ExpressionPolicy> {
|
||||
const policy = await new PoliciesApi(DEFAULT_CONFIG).policiesExpressionRetrieve({
|
||||
policyUuid: pk,
|
||||
});
|
||||
this.user = await me();
|
||||
await this.refreshPreview(policy);
|
||||
return policy;
|
||||
}
|
||||
|
||||
async send(data: ExpressionPolicy): Promise<ExpressionPolicy> {
|
||||
@ -35,10 +74,196 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
||||
}
|
||||
}
|
||||
|
||||
_shouldRefresh = false;
|
||||
_timer = 0;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (!this.showPreview) {
|
||||
return;
|
||||
}
|
||||
// Only check if we should update once a second, to prevent spamming API requests
|
||||
// when many fields are edited
|
||||
const minUpdateDelay = 1000;
|
||||
this._timer = setInterval(() => {
|
||||
if (this._shouldRefresh) {
|
||||
this.refreshPreview();
|
||||
this._shouldRefresh = false;
|
||||
}
|
||||
}, minUpdateDelay) as unknown as number;
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (!this.showPreview) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(this._timer);
|
||||
}
|
||||
|
||||
async refreshPreview(policy?: ExpressionPolicy): Promise<void> {
|
||||
if (!policy) {
|
||||
policy = this.serializeForm();
|
||||
if (!policy) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.previewLoading = true;
|
||||
try {
|
||||
interface testpolicy {
|
||||
expression: string;
|
||||
user?: number;
|
||||
context?: { [key: string]: unknown };
|
||||
}
|
||||
const tp = policy as unknown as testpolicy;
|
||||
this.preview = await new PoliciesApi(DEFAULT_CONFIG).policiesExpressionTestCreate({
|
||||
expressionPolicyTestRequest: {
|
||||
expression: tp.expression,
|
||||
user: tp.user || this.user?.user.pk || 0,
|
||||
context: tp.context || {},
|
||||
},
|
||||
policyUuid: this.instancePk || "",
|
||||
});
|
||||
this.previewError = undefined;
|
||||
} catch (exc) {
|
||||
const errorMessage = ValidationErrorFromJSON(
|
||||
await (exc as ResponseError).response.json(),
|
||||
);
|
||||
this.previewError = errorMessage.nonFieldErrors;
|
||||
} finally {
|
||||
this.previewLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<div class="pf-l-grid pf-m-gutter">
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-l-stack">
|
||||
<div class="pf-c-form pf-m-horizontal pf-l-stack__item">
|
||||
${this.renderEditForm()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col">${this.renderPreview()}</div>
|
||||
</div> `;
|
||||
}
|
||||
|
||||
renderPreview(): TemplateResult {
|
||||
return html`
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__title">${msg("Test parameters")}</div>
|
||||
<div class="pf-c-card__body pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("User")} name="user">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||
const args: CoreUsersListRequest = {
|
||||
ordering: "username",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(
|
||||
args,
|
||||
);
|
||||
return users.results;
|
||||
}}
|
||||
.renderElement=${(user: User): string => {
|
||||
return user.username;
|
||||
}}
|
||||
.renderDescription=${(user: User): TemplateResult => {
|
||||
return html`${user.name}`;
|
||||
}}
|
||||
.value=${(user: User | undefined): number | undefined => {
|
||||
return user?.pk;
|
||||
}}
|
||||
.selected=${(user: User): boolean => {
|
||||
return this.user?.user.pk === user.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Context")} name="context">
|
||||
<ak-codemirror mode=${CodeMirrorMode.YAML} value=${YAML.stringify({})}>
|
||||
</ak-codemirror>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
@click=${() => {
|
||||
this.refreshPreview();
|
||||
}}
|
||||
>
|
||||
${msg("Execute")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__title">${msg("Test results")}</div>
|
||||
${this.previewLoading
|
||||
? html`<ak-empty-state loading></ak-empty-state>`
|
||||
: html`<div class="pf-c-card__body pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("Passing")}>
|
||||
<div class="pf-c-form__group-label">
|
||||
<div class="c-form__horizontal-group">
|
||||
<span class="pf-c-form__label-text">
|
||||
<ak-status-label
|
||||
?good=${this.preview?.passing}
|
||||
></ak-status-label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Messages")}>
|
||||
<div class="pf-c-form__group-label">
|
||||
<div class="c-form__horizontal-group">
|
||||
<ul>
|
||||
${(this.preview?.messages || []).length > 0
|
||||
? this.preview?.messages?.map((m) => {
|
||||
return html`<li>
|
||||
<span class="pf-c-form__label-text"
|
||||
>${m}</span
|
||||
>
|
||||
</li>`;
|
||||
})
|
||||
: html`<li>
|
||||
<span class="pf-c-form__label-text">-</span>
|
||||
</li>`}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal label=${msg("Log messages")}>
|
||||
<div class="pf-c-form__group-label">
|
||||
<div class="c-form__horizontal-group">
|
||||
<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<ak-log-viewer
|
||||
.logs=${this.preview?.logMessages}
|
||||
></ak-log-viewer>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
</div>`}
|
||||
</div>
|
||||
${this.previewError
|
||||
? html`
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__body">${msg("Preview errors")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
${this.previewError.map((err) => html`<pre>${err}</pre>`)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderEditForm(): TemplateResult {
|
||||
return html` <span>
|
||||
${msg(
|
||||
"Executes the python snippet to determine whether to allow or deny a request.",
|
||||
"Executes the Python snippet to determine whether to allow or deny a request.",
|
||||
)}
|
||||
</span>
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
@ -80,6 +305,9 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.Python}
|
||||
value="${ifDefined(this.instance?.expression)}"
|
||||
@change=${() => {
|
||||
this._shouldRefresh = true;
|
||||
}}
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/ak-dual-select";
|
||||
import { DataProvision, DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
@ -47,7 +46,7 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<span>
|
||||
return html` <span>
|
||||
${msg(
|
||||
"Ensure the user satisfies requirements of geography or network topology, based on IP address. If any of the configured values match, the policy passes.",
|
||||
)}
|
||||
@ -80,125 +79,13 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Distance settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal name="checkHistoryDistance">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${this.instance?.checkHistoryDistance ?? false}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Check historical distance of logins")}</span
|
||||
>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Distance tolerance")}
|
||||
name="distanceToleranceKm"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value="${first(this.instance?.distanceToleranceKm, 50)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Tolerance in checking for distances in kilometers.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Historical Login Count")}
|
||||
name="historyLoginCount"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value="${first(this.instance?.historyLoginCount, 5)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Amount of previous login events to check against.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Maximum distance")}
|
||||
name="historyMaxDistanceKm"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Maximum distance a login attempt is allowed from in kilometers.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Distance settings (Impossible travel)")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal name="checkImpossibleTravel">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${this.instance?.checkImpossibleTravel ?? true}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Check impossible travel")}</span
|
||||
>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins and if the travel would have been possible in the amount of time since the previous event.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Impossible travel tolerance")}
|
||||
name="impossibleToleranceKm"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value="${first(this.instance?.impossibleToleranceKm, 50)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Tolerance in checking for distances in kilometers.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Static rule settings")}</span>
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Policy-specific settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("ASNs")} name="asns">
|
||||
<input
|
||||
type="text"
|
||||
value="${this.instance?.asns?.join(",") ?? ""}"
|
||||
value="${this.instance?.asns ?? ""}"
|
||||
class="pf-c-form-control pf-m-monospace"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
|
@ -2,7 +2,6 @@ import "@goauthentik/admin/rbac/ObjectPermissionModal";
|
||||
import "@goauthentik/admin/stages/StageWizard";
|
||||
import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_duo/DuoDeviceImportForm";
|
||||
import "@goauthentik/admin/stages/authenticator_email/AuthenticatorEmailStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm";
|
||||
|
@ -1,7 +1,6 @@
|
||||
import "@goauthentik/admin/common/ak-license-notice";
|
||||
import { StageBindingForm } from "@goauthentik/admin/flows/StageBindingForm";
|
||||
import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_email/AuthenticatorEmailStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";
|
||||
|
@ -1,283 +0,0 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
AuthenticatorEmailStage,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
StagesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-authenticator-email-form")
|
||||
export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmailStage> {
|
||||
async loadInstance(pk: string): Promise<AuthenticatorEmailStage> {
|
||||
const stage = await new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEmailRetrieve({
|
||||
stageUuid: pk,
|
||||
});
|
||||
this.showConnectionSettings = !stage.useGlobalSettings;
|
||||
return stage;
|
||||
}
|
||||
|
||||
@property({ type: Boolean })
|
||||
showConnectionSettings = false;
|
||||
|
||||
async send(data: AuthenticatorEmailStage): Promise<AuthenticatorEmailStage> {
|
||||
if (this.instance) {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEmailUpdate({
|
||||
stageUuid: this.instance.pk || "",
|
||||
authenticatorEmailStageRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEmailCreate({
|
||||
authenticatorEmailStageRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderConnectionSettings(): TemplateResult {
|
||||
if (!this.showConnectionSettings) {
|
||||
return html``;
|
||||
}
|
||||
return html`<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Connection settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("SMTP Host")} ?required=${true} name="host">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.host || "")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("SMTP Port")} ?required=${true} name="port">
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.port, 25)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("SMTP Username")} name="username">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.username || "")}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("SMTP Password")}
|
||||
?writeOnly=${this.instance !== undefined}
|
||||
name="password"
|
||||
>
|
||||
<input type="text" value="" class="pf-c-form-control" />
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="useTls">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(this.instance?.useTls, true)}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label">${msg("Use TLS")}</span>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="useSsl">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(this.instance?.useSsl, false)}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label">${msg("Use SSL")}</span>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Timeout")}
|
||||
?required=${true}
|
||||
name="timeout"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.timeout, 30)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("From address")}
|
||||
?required=${true}
|
||||
name="fromAddress"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.fromAddress || "system@authentik.local")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Email address the verification email will be sent from.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html` <span> ${msg("Stage used to configure an email-based authenticator.")} </span>
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.name, "")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authenticator type name")}
|
||||
?required=${false}
|
||||
name="friendlyName"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.friendlyName, "")}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Display name of this authenticator, used by users when they enroll an authenticator.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="useGlobalSettings">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(this.instance?.useGlobalSettings, true)}
|
||||
@change=${(ev: Event) => {
|
||||
const target = ev.target as HTMLInputElement;
|
||||
this.showConnectionSettings = !target.checked;
|
||||
}}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label">${msg("Use global connection settings")}</span>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When enabled, global email connection settings will be used and connection settings below will be ignored.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${this.renderConnectionSettings()}
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Stage-specific settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Subject")}
|
||||
?required=${true}
|
||||
name="subject"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.subject, "authentik Sign-in code")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Subject of the verification email.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Token expiration")}
|
||||
?required=${true}
|
||||
name="tokenExpiry"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.tokenExpiry, "minutes=15")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Configuration flow")}
|
||||
name="configureFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation:
|
||||
FlowsInstancesListDesignationEnum.StageConfiguration,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(
|
||||
args,
|
||||
);
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
}}
|
||||
.value=${(flow: Flow | undefined): string | undefined => {
|
||||
return flow?.pk;
|
||||
}}
|
||||
.selected=${(flow: Flow): boolean => {
|
||||
return this.instance?.configureFlow === flow.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-authenticator-email-form": AuthenticatorEmailStageForm;
|
||||
}
|
||||
}
|
@ -79,7 +79,6 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
|
||||
[DeviceClassesEnum.Webauthn, msg("WebAuthn Authenticators")],
|
||||
[DeviceClassesEnum.Duo, msg("Duo Authenticators")],
|
||||
[DeviceClassesEnum.Sms, msg("SMS-based Authenticators")],
|
||||
[DeviceClassesEnum.Email, msg("Email-based Authenticators")],
|
||||
];
|
||||
|
||||
return html`
|
||||
|
@ -58,8 +58,6 @@ export class UserDeviceTable extends Table<Device> {
|
||||
switch (device.type) {
|
||||
case "authentik_stages_authenticator_duo.DuoDevice":
|
||||
return api.authenticatorsAdminDuoDestroy({ id: parseInt(device.pk, 10) });
|
||||
case "authentik_stages_authenticator_email.EmailDevice":
|
||||
return api.authenticatorsAdminEmailDestroy({ id: parseInt(device.pk, 10) });
|
||||
case "authentik_stages_authenticator_sms.SMSDevice":
|
||||
return api.authenticatorsAdminSmsDestroy({ id: parseInt(device.pk, 10) });
|
||||
case "authentik_stages_authenticator_totp.TOTPDevice":
|
||||
|
@ -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 = "2025.2.0";
|
||||
export const VERSION = "2024.12.3";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -20,8 +20,11 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
|
||||
}
|
||||
|
||||
renderStatusBanner() {
|
||||
if (!this.licenseSummary) {
|
||||
return nothing;
|
||||
}
|
||||
// Check if we're in the correct interface to render a banner
|
||||
switch (this.licenseSummary.status) {
|
||||
switch (this.licenseSummary?.status) {
|
||||
// user warning is both on admin interface and user interface
|
||||
case LicenseSummaryStatusEnum.LimitExceededUser:
|
||||
if (
|
||||
@ -46,7 +49,7 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
|
||||
break;
|
||||
}
|
||||
let message = "";
|
||||
switch (this.licenseSummary.status) {
|
||||
switch (this.licenseSummary?.status) {
|
||||
case LicenseSummaryStatusEnum.LimitExceededAdmin:
|
||||
case LicenseSummaryStatusEnum.LimitExceededUser:
|
||||
message = msg(
|
||||
@ -83,13 +86,16 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
|
||||
}
|
||||
|
||||
renderFlagBanner() {
|
||||
if (!this.licenseSummary) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
${this.licenseSummary.licenseFlags.includes(LicenseFlagsEnum.Trial)
|
||||
${this.licenseSummary?.licenseFlags.includes(LicenseFlagsEnum.Trial)
|
||||
? html`<div class="pf-c-banner pf-m-sticky pf-m-gold">
|
||||
${msg("This authentik instance uses a Trial license.")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this.licenseSummary.licenseFlags.includes(LicenseFlagsEnum.NonProduction)
|
||||
${this.licenseSummary?.licenseFlags.includes(LicenseFlagsEnum.NonProduction)
|
||||
? html`<div class="pf-c-banner pf-m-sticky pf-m-gold">
|
||||
${msg("This authentik instance uses a Non-production license.")}
|
||||
</div>`
|
||||
|
@ -40,6 +40,9 @@ export class ModalButton extends AKElement {
|
||||
@property()
|
||||
size: PFSize = PFSize.Large;
|
||||
|
||||
@property({ type: Boolean })
|
||||
fullHeight = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
open = false;
|
||||
|
||||
@ -69,6 +72,9 @@ export class ModalButton extends AKElement {
|
||||
.pf-c-modal-box.pf-m-xl {
|
||||
--pf-c-modal-box--Width: calc(1.5 * var(--pf-c-modal-box--m-lg--lg--MaxWidth));
|
||||
}
|
||||
:host([fullHeight]) .pf-c-modal-box {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -14,7 +14,20 @@ import { LogEvent, LogLevelEnum } from "@goauthentik/api";
|
||||
@customElement("ak-log-viewer")
|
||||
export class LogViewer extends Table<LogEvent> {
|
||||
@property({ attribute: false })
|
||||
logs?: LogEvent[] = [];
|
||||
set logs(val: LogEvent[]) {
|
||||
this.data = {
|
||||
pagination: {
|
||||
next: 0,
|
||||
previous: 0,
|
||||
count: val.length || 0,
|
||||
current: 1,
|
||||
totalPages: 1,
|
||||
startIndex: 1,
|
||||
endIndex: val.length || 0,
|
||||
},
|
||||
results: val,
|
||||
};
|
||||
}
|
||||
|
||||
expandable = true;
|
||||
paginated = false;
|
||||
@ -24,18 +37,20 @@ export class LogViewer extends Table<LogEvent> {
|
||||
}
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<LogEvent>> {
|
||||
return {
|
||||
pagination: {
|
||||
next: 0,
|
||||
previous: 0,
|
||||
count: this.logs?.length || 0,
|
||||
current: 1,
|
||||
totalPages: 1,
|
||||
startIndex: 1,
|
||||
endIndex: this.logs?.length || 0,
|
||||
},
|
||||
results: this.logs || [],
|
||||
};
|
||||
return (
|
||||
this.data || {
|
||||
pagination: {
|
||||
next: 0,
|
||||
previous: 0,
|
||||
count: this.logs?.length || 0,
|
||||
current: 1,
|
||||
totalPages: 1,
|
||||
startIndex: 1,
|
||||
endIndex: this.logs?.length || 0,
|
||||
},
|
||||
results: this.logs || [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
renderEmpty(): TemplateResult {
|
||||
|
@ -38,6 +38,10 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
|
||||
});
|
||||
}
|
||||
|
||||
get instancePk(): PKT | undefined {
|
||||
return this._instancePk;
|
||||
}
|
||||
|
||||
private _instancePk?: PKT;
|
||||
|
||||
// Keep track if we've loaded the model instance
|
||||
|
@ -392,14 +392,6 @@ export class FlowExecutor extends Interface implements StageHost {
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-authenticator-webauthn>`;
|
||||
case "ak-stage-authenticator-email":
|
||||
await import(
|
||||
"@goauthentik/flow/stages/authenticator_email/AuthenticatorEmailStage"
|
||||
);
|
||||
return html`<ak-stage-authenticator-email
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-authenticator-email>`;
|
||||
case "ak-stage-authenticator-sms":
|
||||
await import("@goauthentik/flow/stages/authenticator_sms/AuthenticatorSMSStage");
|
||||
return html`<ak-stage-authenticator-sms
|
||||
|
@ -1,173 +0,0 @@
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
AuthenticatorEmailChallenge,
|
||||
AuthenticatorEmailChallengeResponseRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-authenticator-email")
|
||||
export class AuthenticatorEmailStage extends BaseStage<
|
||||
AuthenticatorEmailChallenge,
|
||||
AuthenticatorEmailChallengeResponseRequest
|
||||
> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
|
||||
}
|
||||
|
||||
renderEmailInput(): TemplateResult {
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<ak-form-element
|
||||
label="${msg("Configure your email")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["email"]}
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="${msg("Please enter your email address.")}"
|
||||
autofocus=""
|
||||
autocomplete="email"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
${this.renderNonFieldErrors()}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${msg("Continue")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
renderEmailOTPInput(): TemplateResult {
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
A verification token has been sent to your configured email address
|
||||
${ifDefined(this.challenge.email)}
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<ak-form-element
|
||||
label="${msg("Code")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["code"]}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
placeholder="${msg("Please enter the code you received via email")}"
|
||||
autofocus=""
|
||||
autocomplete="one-time-code"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
${this.renderNonFieldErrors()}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${msg("Continue")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
console.debug(
|
||||
"authentik/stages/authenticator_email:",
|
||||
this.challenge ? this.challenge.emailRequired : undefined,
|
||||
);
|
||||
|
||||
if (!this.challenge) {
|
||||
console.debug(
|
||||
"authentik/stages/authenticator_email: AuthenticatorEmailStage.render() called without challenge",
|
||||
);
|
||||
|
||||
return html`<ak-empty-state loading> </ak-empty-state>`;
|
||||
}
|
||||
if (this.challenge.emailRequired) {
|
||||
console.debug(
|
||||
"authentik/stages/authenticator_email: AuthenticatorEmailStage.render() called with challenge",
|
||||
this.challenge,
|
||||
);
|
||||
|
||||
return this.renderEmailInput();
|
||||
}
|
||||
console.debug(
|
||||
"authentik/stages/authenticator_email: AuthenticatorEmailStage.render() called without emailRequired challenge",
|
||||
this.challenge,
|
||||
);
|
||||
|
||||
return this.renderEmailOTPInput();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-authenticator-email": AuthenticatorEmailStage;
|
||||
}
|
||||
}
|
@ -185,12 +185,6 @@ export class AuthenticatorValidateStage
|
||||
<p>${msg("SMS")}</p>
|
||||
<small>${msg("Tokens sent via SMS.")}</small>
|
||||
</div>`;
|
||||
case DeviceClassesEnum.Email:
|
||||
return html`<i class="fas fa-envelope-o"></i>
|
||||
<div class="right">
|
||||
<p>${msg("Email")}</p>
|
||||
<small>${msg("Tokens sent via email.")}</small>
|
||||
</div>`;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -246,7 +240,6 @@ export class AuthenticatorValidateStage
|
||||
switch (this.selectedDeviceChallenge?.deviceClass) {
|
||||
case DeviceClassesEnum.Static:
|
||||
case DeviceClassesEnum.Totp:
|
||||
case DeviceClassesEnum.Email:
|
||||
case DeviceClassesEnum.Sms:
|
||||
return html` <ak-stage-authenticator-validate-code
|
||||
.host=${this}
|
||||
|
@ -33,10 +33,6 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||
|
||||
deviceMessage(): string {
|
||||
switch (this.deviceChallenge?.deviceClass) {
|
||||
case DeviceClassesEnum.Email: {
|
||||
const email = this.deviceChallenge.challenge?.email;
|
||||
return msg(`A code has been sent to you via email${email ? ` ${email}` : ""}`);
|
||||
}
|
||||
case DeviceClassesEnum.Sms:
|
||||
return msg("A code has been sent to you via SMS.");
|
||||
case DeviceClassesEnum.Totp:
|
||||
@ -52,14 +48,12 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||
|
||||
deviceIcon(): string {
|
||||
switch (this.deviceChallenge?.deviceClass) {
|
||||
case DeviceClassesEnum.Email:
|
||||
return "fa-envelope-o";
|
||||
case DeviceClassesEnum.Sms:
|
||||
return "fa-mobile-alt";
|
||||
case DeviceClassesEnum.Totp:
|
||||
return "fa-clock";
|
||||
case DeviceClassesEnum.Static:
|
||||
return "fa-key";
|
||||
case DeviceClassesEnum.Totp:
|
||||
return "fa-mobile-alt";
|
||||
case DeviceClassesEnum.Static:
|
||||
return "fa-sticky-note";
|
||||
}
|
||||
|
||||
return "fa-mobile-alt";
|
||||
|
@ -34,12 +34,6 @@ export class MFADeviceForm extends ModelForm<Device, string> {
|
||||
duoDeviceRequest: device,
|
||||
});
|
||||
break;
|
||||
case "authentik_stages_authenticator_email.EmailDevice":
|
||||
await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsEmailUpdate({
|
||||
id: parseInt(this.instance?.pk, 10),
|
||||
emailDeviceRequest: device,
|
||||
});
|
||||
break;
|
||||
case "authentik_stages_authenticator_sms.SMSDevice":
|
||||
await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsSmsUpdate({
|
||||
id: parseInt(this.instance?.pk, 10),
|
||||
|
@ -95,8 +95,6 @@ export class MFADevicesPage extends Table<Device> {
|
||||
switch (device.type) {
|
||||
case "authentik_stages_authenticator_duo.DuoDevice":
|
||||
return api.authenticatorsDuoDestroy(id);
|
||||
case "authentik_stages_authenticator_email.EmailDevice":
|
||||
return api.authenticatorsEmailDestroy(id);
|
||||
case "authentik_stages_authenticator_sms.SMSDevice":
|
||||
return api.authenticatorsSmsDestroy(id);
|
||||
case "authentik_stages_authenticator_totp.TOTPDevice":
|
||||
|
@ -9344,145 +9344,108 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s82cced87e15e0665">
|
||||
<source>Worker with incorrect version connected.</source>
|
||||
<target>错误版本的 Worker 已连接。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="ha8e91858b300a3b7">
|
||||
<source>(Format: <x id="0" equiv-text="<code>"/>hours=-1;minutes=-2;seconds=-3)<x id="1" equiv-text="</code>"/>.</source>
|
||||
<target>(格式:<x id="0" equiv-text="<code>"/>hours=-1;minutes=-2;seconds=-3)<x id="1" equiv-text="</code>"/>。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="hbe8e6353371ecb22">
|
||||
<source>(Format: <x id="0" equiv-text="<code>"/>hours=1;minutes=2;seconds=3).<x id="1" equiv-text="</code>"/></source>
|
||||
<target>(格式:<x id="0" equiv-text="<code>"/>hours=1;minutes=2;seconds=3)。<x id="1" equiv-text="</code>"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se8dd47dfaf01d6c9">
|
||||
<source>Key used to sign the events.</source>
|
||||
<target>用于签名事件的密钥。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd1b31f8fba05d429">
|
||||
<source>Event Retention</source>
|
||||
<target>事件保留</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd760b123cced4675">
|
||||
<source>Determines how long events are stored for. If an event could not be sent correctly, its expiration is also increased by this duration.</source>
|
||||
<target>设置事件存储多久时间。如果无法成功发送事件,则此时长也会添加到事件的过期时间。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1aa62e1bf258e88a">
|
||||
<source>OIDC Providers</source>
|
||||
<target>OIDC 提供程序</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb9d947872397ca07">
|
||||
<source>SSF Provider is in preview.</source>
|
||||
<target>SSF 提供程序处于预览状态。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78302d097a608b99">
|
||||
<source>Update SSF Provider</source>
|
||||
<target>更新 SSF 提供程序</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa412f34efd88e962">
|
||||
<source>Streams</source>
|
||||
<target>流</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s91ae4b6bf981682b">
|
||||
<source>authentik Logo</source>
|
||||
<target>authentik 图标</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1cdeecda0baf365e">
|
||||
<source>Release</source>
|
||||
<target>发布版</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35429c9dc2319aea">
|
||||
<source>Development</source>
|
||||
<target>开发版</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5937938934a777ab">
|
||||
<source>UI Version</source>
|
||||
<target>界面版本</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3fe3ff490adaaa63">
|
||||
<source>Build</source>
|
||||
<target>构建</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s42aea5c6d3d1fce7">
|
||||
<source>Python version</source>
|
||||
<target>Python 版本</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5c67b6c66029e5b2">
|
||||
<source>Platform</source>
|
||||
<target>平台</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se8574bce64d9c1f8">
|
||||
<source>Kernel</source>
|
||||
<target>内核</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3fbe63b7fcfe3e91">
|
||||
<source>OpenSSL</source>
|
||||
<target>OpenSSL</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1381314790b50b1f">
|
||||
<source>A newer version (<x id="0" equiv-text="${this.version.versionCurrent}"/>) of the UI is available.</source>
|
||||
<target>新版本界面(<x id="0" equiv-text="${this.version.versionCurrent}"/>)可用。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s61ffea061fae0af4">
|
||||
<source>No notifications found.</source>
|
||||
<target>未找到通知。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="scf8d5cdc8b434982">
|
||||
<source>You don't have any notifications currently.</source>
|
||||
<target>您当前没有任何通知。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s358e08de4fbebf51">
|
||||
<source>Version <x id="0" equiv-text="${this.version?.versionCurrent || ""}"/></source>
|
||||
<target>版本 <x id="0" equiv-text="${this.version?.versionCurrent || ""}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfb95ad8c25db95ba">
|
||||
<source>Last password change</source>
|
||||
<target>上次修改密码</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s37e1bad4bf0e9241">
|
||||
<source>Evaluate policies before the Stage is presented to the user.</source>
|
||||
<target>在阶段即将呈现给用户时评估策略。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="h8c1aa2d523f94f36">
|
||||
<source>Can be in the format of <x id="0" equiv-text="<code>"/>unix://<x id="1" equiv-text="</code>"/> when connecting to a local
|
||||
docker daemon, using <x id="2" equiv-text="<code>"/>ssh://<x id="3" equiv-text="</code>"/> to connect via SSH, or
|
||||
<x id="4" equiv-text="<code>"/>https://:2376<x id="5" equiv-text="</code>"/> when connecting to a remote system.</source>
|
||||
<target>可以使用 <x id="0" equiv-text="<code>"/>unix://<x id="1" equiv-text="</code>"/> 格式连接本地 Docker 守护程序,
|
||||
使用 <x id="2" equiv-text="<code>"/>ssh://<x id="3" equiv-text="</code>"/> 通过 SSH 连接,或者
|
||||
使用 <x id="4" equiv-text="<code>"/>https://:2376<x id="5" equiv-text="</code>"/> 连接到远程系统。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="h3ac79dc5cc553329">
|
||||
<source>When using an external logging solution for archiving, this can be
|
||||
set to <x id="0" equiv-text="<code>"/>minutes=5<x id="1" equiv-text="</code>"/>.</source>
|
||||
<target>当使用外部日志解决方案进行存档时,可以
|
||||
设置为 <x id="0" equiv-text="<code>"/>minutes=5<x id="1" equiv-text="</code>"/>。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc92905d14a9421f3">
|
||||
<source>Idle</source>
|
||||
<target>闲置</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4b052cb287d81da7">
|
||||
<source>Connecting</source>
|
||||
<target>正在连接</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s123b7254b44dd148">
|
||||
<source>Waiting</source>
|
||||
<target>正在等待</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s82a4e28ab5b56670">
|
||||
<source>Connected</source>
|
||||
<target>已连接</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd669c8ff49c068ad">
|
||||
<source>Disconnecting</source>
|
||||
<target>正在断开连接</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc2c3bd345c0128a">
|
||||
<source>Disconnected</source>
|
||||
<target>已断开连接</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s47b7ce63a543564c">
|
||||
<source>Fewer details</source>
|
||||
<target>显示更少</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -9344,145 +9344,108 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s82cced87e15e0665">
|
||||
<source>Worker with incorrect version connected.</source>
|
||||
<target>错误版本的 Worker 已连接。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="ha8e91858b300a3b7">
|
||||
<source>(Format: <x id="0" equiv-text="<code>"/>hours=-1;minutes=-2;seconds=-3)<x id="1" equiv-text="</code>"/>.</source>
|
||||
<target>(格式:<x id="0" equiv-text="<code>"/>hours=-1;minutes=-2;seconds=-3)<x id="1" equiv-text="</code>"/>。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="hbe8e6353371ecb22">
|
||||
<source>(Format: <x id="0" equiv-text="<code>"/>hours=1;minutes=2;seconds=3).<x id="1" equiv-text="</code>"/></source>
|
||||
<target>(格式:<x id="0" equiv-text="<code>"/>hours=1;minutes=2;seconds=3)。<x id="1" equiv-text="</code>"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se8dd47dfaf01d6c9">
|
||||
<source>Key used to sign the events.</source>
|
||||
<target>用于签名事件的密钥。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd1b31f8fba05d429">
|
||||
<source>Event Retention</source>
|
||||
<target>事件保留</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd760b123cced4675">
|
||||
<source>Determines how long events are stored for. If an event could not be sent correctly, its expiration is also increased by this duration.</source>
|
||||
<target>设置事件存储多久时间。如果无法成功发送事件,则此时长也会添加到事件的过期时间。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1aa62e1bf258e88a">
|
||||
<source>OIDC Providers</source>
|
||||
<target>OIDC 提供程序</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb9d947872397ca07">
|
||||
<source>SSF Provider is in preview.</source>
|
||||
<target>SSF 提供程序处于预览状态。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78302d097a608b99">
|
||||
<source>Update SSF Provider</source>
|
||||
<target>更新 SSF 提供程序</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa412f34efd88e962">
|
||||
<source>Streams</source>
|
||||
<target>流</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s91ae4b6bf981682b">
|
||||
<source>authentik Logo</source>
|
||||
<target>authentik 图标</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1cdeecda0baf365e">
|
||||
<source>Release</source>
|
||||
<target>发布版</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35429c9dc2319aea">
|
||||
<source>Development</source>
|
||||
<target>开发版</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5937938934a777ab">
|
||||
<source>UI Version</source>
|
||||
<target>界面版本</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3fe3ff490adaaa63">
|
||||
<source>Build</source>
|
||||
<target>构建</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s42aea5c6d3d1fce7">
|
||||
<source>Python version</source>
|
||||
<target>Python 版本</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5c67b6c66029e5b2">
|
||||
<source>Platform</source>
|
||||
<target>平台</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se8574bce64d9c1f8">
|
||||
<source>Kernel</source>
|
||||
<target>内核</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3fbe63b7fcfe3e91">
|
||||
<source>OpenSSL</source>
|
||||
<target>OpenSSL</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1381314790b50b1f">
|
||||
<source>A newer version (<x id="0" equiv-text="${this.version.versionCurrent}"/>) of the UI is available.</source>
|
||||
<target>新版本界面(<x id="0" equiv-text="${this.version.versionCurrent}"/>)可用。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s61ffea061fae0af4">
|
||||
<source>No notifications found.</source>
|
||||
<target>未找到通知。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="scf8d5cdc8b434982">
|
||||
<source>You don't have any notifications currently.</source>
|
||||
<target>您当前没有任何通知。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s358e08de4fbebf51">
|
||||
<source>Version <x id="0" equiv-text="${this.version?.versionCurrent || ""}"/></source>
|
||||
<target>版本 <x id="0" equiv-text="${this.version?.versionCurrent || ""}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfb95ad8c25db95ba">
|
||||
<source>Last password change</source>
|
||||
<target>上次修改密码</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s37e1bad4bf0e9241">
|
||||
<source>Evaluate policies before the Stage is presented to the user.</source>
|
||||
<target>在阶段即将呈现给用户时评估策略。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="h8c1aa2d523f94f36">
|
||||
<source>Can be in the format of <x id="0" equiv-text="<code>"/>unix://<x id="1" equiv-text="</code>"/> when connecting to a local
|
||||
docker daemon, using <x id="2" equiv-text="<code>"/>ssh://<x id="3" equiv-text="</code>"/> to connect via SSH, or
|
||||
<x id="4" equiv-text="<code>"/>https://:2376<x id="5" equiv-text="</code>"/> when connecting to a remote system.</source>
|
||||
<target>可以使用 <x id="0" equiv-text="<code>"/>unix://<x id="1" equiv-text="</code>"/> 格式连接本地 Docker 守护程序,
|
||||
使用 <x id="2" equiv-text="<code>"/>ssh://<x id="3" equiv-text="</code>"/> 通过 SSH 连接,或者
|
||||
使用 <x id="4" equiv-text="<code>"/>https://:2376<x id="5" equiv-text="</code>"/> 连接到远程系统。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="h3ac79dc5cc553329">
|
||||
<source>When using an external logging solution for archiving, this can be
|
||||
set to <x id="0" equiv-text="<code>"/>minutes=5<x id="1" equiv-text="</code>"/>.</source>
|
||||
<target>当使用外部日志解决方案进行存档时,可以
|
||||
设置为 <x id="0" equiv-text="<code>"/>minutes=5<x id="1" equiv-text="</code>"/>。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc92905d14a9421f3">
|
||||
<source>Idle</source>
|
||||
<target>闲置</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4b052cb287d81da7">
|
||||
<source>Connecting</source>
|
||||
<target>正在连接</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s123b7254b44dd148">
|
||||
<source>Waiting</source>
|
||||
<target>正在等待</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s82a4e28ab5b56670">
|
||||
<source>Connected</source>
|
||||
<target>已连接</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd669c8ff49c068ad">
|
||||
<source>Disconnecting</source>
|
||||
<target>正在断开连接</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc2c3bd345c0128a">
|
||||
<source>Disconnected</source>
|
||||
<target>已断开连接</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s47b7ce63a543564c">
|
||||
<source>Fewer details</source>
|
||||
<target>显示更少</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -1,171 +0,0 @@
|
||||
---
|
||||
title: Release 2025.2
|
||||
slug: "/releases/2025.2"
|
||||
---
|
||||
|
||||
:::::note
|
||||
2025.2 has not been released yet! We're publishing these release notes as a preview of what's to come, and for our awesome beta testers trying out release candidates.
|
||||
|
||||
To try out the release candidate, replace your Docker image tag with the latest release candidate number, such as 2025.2.0-rc1. You can find the latest one in [the latest releases on GitHub](https://github.com/goauthentik/authentik/releases). If you don't find any, it means we haven't released one yet.
|
||||
:::::
|
||||
|
||||
## Highlights
|
||||
|
||||
- **SSF Provider <span class="badge badge--primary">Enterprise</span> <span class="badge badge--info">Preview</span>** Add support for Shared Signals Framework
|
||||
TODO: Add preview banner to UI
|
||||
- **RAC moved open source** Remote access is now available to everyone!
|
||||
- **GeoIP distance and impossible travel checks** Add the ability to check for the distance a user has moved compared to a previous login, and if the user could have travelled the distance
|
||||
- **Email OTP Stage** Allow users to use their email accounts as a one-time-password during authentication
|
||||
- **Fine-grained permission for superuser toggle on groups** Setting the **Is superuser** toggle on a group now requires a separate permission.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
- **Deprecated and frozen `:latest` container image tag after 2025.2**
|
||||
|
||||
Using the `:latest` tag with container images is not recommended as it can lead to unintentional updates and potentially broken setups.
|
||||
|
||||
The tag will not be removed, however it will also not be updated past 2025.2.
|
||||
|
||||
We strongly recommended the use of a specific version tag for authentik instances' container images like `:2025.2`.
|
||||
|
||||
## New features
|
||||
|
||||
- SSF Provider <span class="badge badge--primary">Enterprise</span> <span class="badge badge--info">Preview</span>
|
||||
|
||||
[Shared Signals Framework](#todo) allows applications to register a stream with authentik within which they can received events from authentik such as when a session was revoked or a credential was add/changed/deleted and execute actions based on these events.
|
||||
|
||||
This allows admins to integrate authentik with Apple Business/School Manager for federated Apple IDs. See the integration docs [here](#todo)
|
||||
|
||||
- RAC to open source
|
||||
|
||||
Remote access (RDP, VNC and SSH) has moved from enterprise to our free, open source code. We try our best to limit enterprise-specific functionality to features that would be non-essential to homelab users and far more valuable to enterprise use cases. We've had a variety of homelab users reach out with excellent use cases for RAC functionality, so while this will mean giving up some potential revenue, we think that opening up RAC to the community is the right thing to do!
|
||||
|
||||
- GeoIP distance and impossible travel checks
|
||||
|
||||
Add the ability to check for the distance a user has moved compared to a previous login, and add the option to check impossible travel distances based on client IP.
|
||||
|
||||
These options can be used to detect and prevent access from potentially stolen authentik sessions or stolen devices.
|
||||
|
||||
- Email OTP Stage
|
||||
|
||||
Admins now have the ability to configure the option for users to use their email as an authenticator. Users that already have an email address set on their account will be able to use that address to receive one-time-passwords. It is also possible to configure authentik to allow users to add additional email addresses as authenticators.
|
||||
|
||||
See [Email OTP Stage](#todo)
|
||||
|
||||
- Application Wizard is the default way to create applications
|
||||
|
||||
The default way of creating an application now allows admins to configure the provider and any kind of bindings without having to jump through different sections of the UI. The previous way of creating an application is and will stay available alongside the new and streamlined method.
|
||||
|
||||
- Fine-grained permission for superuser toggle on groups
|
||||
|
||||
Setting the **Is superuser** toggle on a group now requires a separate permission, making it much easier to allow for delegated management of groups without risking the ability for users to self-elevate permissions.
|
||||
|
||||
- Improved debugging experience
|
||||
|
||||
For people developing authentik or building very complex, custom integrations, configuring debugging in authentik is now documented [here](#todo)
|
||||
|
||||
## TODO
|
||||
|
||||
temp
|
||||
|
||||
## Upgrading
|
||||
|
||||
This release does not introduce any new requirements. You can follow the upgrade instructions below; for more detailed information about upgrading authentik, refer to our [Upgrade documentation](../../install-config/upgrade.mdx).
|
||||
|
||||
:::warning
|
||||
When you upgrade, be aware that the version of the authentik instance and of any outposts must be the same. We recommended that you always upgrade any outposts at the same time you upgrade your authentik instance.
|
||||
:::
|
||||
|
||||
### Docker Compose
|
||||
|
||||
To upgrade, download the new docker-compose file and update the Docker stack with the new version, using these commands:
|
||||
|
||||
```shell
|
||||
wget -O docker-compose.yml https://goauthentik.io/version/2025.2/docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The `-O` flag retains the downloaded file's name, overwriting any existing local file with the same name.
|
||||
|
||||
### Kubernetes
|
||||
|
||||
Upgrade the Helm Chart to the new version, using the following commands:
|
||||
|
||||
```shell
|
||||
helm repo update
|
||||
helm upgrade authentik authentik/authentik -f values.yaml --version ^2025.2
|
||||
```
|
||||
|
||||
## Minor changes/fixes
|
||||
|
||||
- admin: monitor worker version (#12463)
|
||||
- api: cleanup owner permissions (#12598)
|
||||
- blueprints: add REPL for blueprint YAML tags (#9223)
|
||||
- blueprints: fix schema for meta models (#12421)
|
||||
- core: add indexes on ExpiringModel (#12658)
|
||||
- core: fix application entitlements not creatable with blueprints (#12673)
|
||||
- core: fix error when creating new user with default path (#12609)
|
||||
- core: fix generic sources not being fetchable by pk (#12896)
|
||||
- core: fix permissions for admin device listing (#12787)
|
||||
- core: search users' attributes (#12740)
|
||||
- core: show last password change date (#12958)
|
||||
- enterprise/providers: SSF (#12327)
|
||||
- enterprise/providers/SSF: fix a couple of bugs after real world testing (#12987)
|
||||
- enterprise/rac: Improve client connection status & bugfixes (#12684)
|
||||
- events: make sure password set event has the correct IP (#12585)
|
||||
- events: notification_cleanup: avoid unnecessary loop (#12417)
|
||||
- flows: clear flow state before redirecting to final URL (#12788)
|
||||
- flows: fix history containing other plans (#12655)
|
||||
- flows: fix inspector permission check (#12907)
|
||||
- flows: more tests (#11587)
|
||||
- flows: show policy messages in reevaluate marker (#12855)
|
||||
- flows/inspector: add button to open flow inspector (#12656)
|
||||
- internal: fix missing trailing slash in outpost websocket (#12470)
|
||||
- internal: fix URL generation for websocket connection (#12439)
|
||||
- lifecycle: update python to 3.12.8 (#12783)
|
||||
- lifecycle/migrate: don't migrate tenants if not enabled (#12850)
|
||||
- outposts: fix version label (#12486)
|
||||
- providers/oauth2: include scope in token response (#12921)
|
||||
- providers/oauth2: support token revocation for public clients (#12704)
|
||||
- providers/saml: fix handle Accept: application/xml for SAML Metadata endpoint (#12483) (#12518)
|
||||
- providers/saml: fix invalid SAML Response when assertion and response are signed (#12611)
|
||||
- providers/saml: provide generic metadata url when possible (#12413)
|
||||
- rbac: exclude permissions for internal models (#12803)
|
||||
- rbac: permissions endpoint: allow authenticated users (#12608)
|
||||
- root: backport version bump (#12426)
|
||||
- root: docker: ensure apt packages are up-to-date (#12683)
|
||||
- root: expose CONN_MAX_AGE, CONN_HEALTH_CHECKS and DISABLE_SERVER_SIDE_CURSORS for PostgreSQL config (#10159)
|
||||
- root: fix dev build version being invalid semver (#12472)
|
||||
- root: redis, make sure tlscacert isn't an empty string (#12407)
|
||||
- sources: allow uuid or slug to be used for retrieving a source (#12780)
|
||||
- sources: allow uuid or slug to be used for retrieving a source (2024.12 fix) (#12772)
|
||||
- sources/kerberos: authenticate with the user's username instead of the first username in authentik (#12497)
|
||||
- sources/kerberos: handle principal expire time (#12748)
|
||||
- sources/oauth: fix authentication only being sent in form body (#12713)
|
||||
- sources/scim: fix user creation (duplicate userName) (#12547)
|
||||
- stages/authenticator: add user field to devices (#12636)
|
||||
- stages/prompt: always show policy messages (#12765)
|
||||
- stages/redirect: fix query parameter when redirecting to flow (#12750)
|
||||
- web, core: fix grammatical issue in stage bindings (#10799)
|
||||
- web: fix build dev build (#12473)
|
||||
- web: fix error handling bug in ApplicationWizard.RACProviderForm (#12640)
|
||||
- web: Fix issue where Codemirror partially applies OneDark theme. (#12811)
|
||||
- web: fix mobile scrolling bug (#12601)
|
||||
- web: fix source selection and outpost integration health (#12530)
|
||||
- web: fix source selection and outpost integration health (#12530)
|
||||
- web: fixes broken docLinks - url missing s (#12789)
|
||||
- web: housekeeping, optimizations and small fixes (#12450)
|
||||
- web: improve notification and API drawers (#12659)
|
||||
- web: misc fixes for admin and flow inspector (#12461)
|
||||
- web: only load version context when authenticated (#12482)
|
||||
- web: update gen-client-ts to OpenAPI 7.11.0 (#12756)
|
||||
- web/admin: fix role changelog missing primary key filter (#12671)
|
||||
- web/admin: improve user display view (#12988)
|
||||
- web/admin: more cleanup and consistency (#12657)
|
||||
- web/admin: Refine navigation (#12441)
|
||||
- web/components: ak-number-input: add support for min (#12703)
|
||||
- web/flows: fix `login` / `log in` inconsistency (#12526)
|
||||
|
||||
## API Changes
|
||||
|
||||
<!-- _Insert output of `make gen-diff` here_ -->
|
@ -22,7 +22,7 @@ The following placeholders are used in this guide:
|
||||
|
||||

|
||||
|
||||
3. Select **OAuth2** from the left menu
|
||||
3. Select **OAuth2** from the left Menu
|
||||
|
||||
4. Copy the **Client ID** and _save it for later_
|
||||
|
||||
@ -38,8 +38,8 @@ Here is an example of a completed OAuth2 screen for Discord.
|
||||
|
||||
8. Under _Directory -> Federation & Social login_ Click **Create Discord OAuth Source**
|
||||
|
||||
9. **Name:** Choose a name (For the example I used `Discord`)
|
||||
10. **Slug:** discord (You can choose a different slug, if you do you will need to update the Discord redirect URL and point it to the correct slug.)
|
||||
9. **Name:** Choose a name (For the example I used Discord)
|
||||
10. **Slug:** discord (You can choose a different slug, if you do you will need to update the Discord redirect URLand point it to the correct slug.)
|
||||
11. **Consumer Key:** Client ID from step 4
|
||||
12. **Consumer Secret:** Client Secret from step 5
|
||||
|
||||
|
@ -1,100 +0,0 @@
|
||||
---
|
||||
title: Integrate with AdventureLog
|
||||
sidebar_label: AdventureLog
|
||||
---
|
||||
|
||||
# Integrate with AdventureLog
|
||||
|
||||
<span class="badge badge--secondary">Support level: Community</span>
|
||||
|
||||
## What is AdventureLog
|
||||
|
||||
> AdventureLog is a self-hosted travel tracker and trip planner. AdventureLog is the ultimate travel companion for the modern-day explorer.
|
||||
>
|
||||
> -- https://adventurelog.app/
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `https://adventurelog.company` is the FQDN of the AdventureLog server installation.
|
||||
- `https://authentik.company` is the FQDN of the authentik installation.
|
||||
|
||||
:::note
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
1. Create a new OAuth2/OpenID Provider under **Applications** > **Providers** using the following settings:
|
||||
- **Name**: AdventureLog
|
||||
- **Authentication flow**: default-authentication-flow
|
||||
- **Authorization flow**: default-provider-authorization-explicit-consent
|
||||
- **Client type**: Confidential
|
||||
- **Client ID**: Either create your own Client ID or use the auto-populated ID
|
||||
- **Client Secret**: Either create your own Client Secret or use the auto-populated secret
|
||||
:::note
|
||||
Take note of the `Client ID` and `Client Secret` as they are required when configuring AdventureLog.
|
||||
:::
|
||||
- **Redirect URIs/Origins (RegEx)**:
|
||||
:::note
|
||||
Make sure type is set to `RegEx` and the following RegEx is used.
|
||||
:::
|
||||
- `^https://adventurelog.company/accounts/oidc/.*$`
|
||||
- **Signing Key**: authentik Self-signed Certificate
|
||||
- Leave everything else as default
|
||||
2. Open the new provider you've just created.
|
||||
3. Make a note of the **OpenID Configuration Issuer**.
|
||||
4. Navigate to **Applications -> Applications** and create a new application that uses the provider you just created.
|
||||
|
||||
## AdventureLog configuration
|
||||
|
||||
AdventureLog documentation can be found here: https://adventurelog.app/docs/configuration/social_auth/authentik.html
|
||||
|
||||
This configuration is done in the Admin Panel. Launch the panel by clicking your user avatar in the navbar, selecting **Settings**, and then clicking **Launch Admin Panel**. Make sure you are logged in as an administrator for this to work.
|
||||
|
||||
Alternatively, navigate to `/admin` on your AdventureLog server.
|
||||
|
||||
1. In the admin panel, scroll down to the **Social Accounts** section and click **Add** next to **Social applications**. Fill in the following fields:
|
||||
|
||||
- Provider: OpenID Connect
|
||||
- Provider ID: authentik Client ID
|
||||
- Name: authentik
|
||||
- Client ID: authentik Client ID
|
||||
- Secret Key: authentik Client Secret
|
||||
- Key: _should be left blank_
|
||||
- Settings: (make sure http/https is set correctly)
|
||||
|
||||
```json
|
||||
{
|
||||
"server_url": "https://authentik.company/application/o/[YOUR_SLUG]/"
|
||||
}
|
||||
```
|
||||
|
||||
- Sites: move over the sites you want to enable authentik on, usually `example.com` and `www.example.com` unless you renamed your sites.
|
||||
|
||||
:::warning
|
||||
`localhost` is most likely not a valid `server_url` for authentik in this instance because `localhost` is the server running AdventureLog, not authentik. You should use the IP address of the server running authentik or the domain name if you have one.
|
||||
:::
|
||||
|
||||
2. Save the configuration.
|
||||
|
||||
Ensure that the authentik server is running and accessible by AdventureLog. Users should now be able to log in to AdventureLog using their authentik account.
|
||||
|
||||
## Configuration validation
|
||||
|
||||
To validate the configuration, either link to an existing account as described below or naviage to the AdventureLog login page and click the **authentik** button to log in. You should be redirected to the authentik login page. After logging in, you should be redirected back to AdventureLog.
|
||||
|
||||
### Linking to Existing Account
|
||||
|
||||
If a user has an existing AdventureLog account and wants to link it to their authentik account, they can do so by logging in to their AdventureLog account and navigating to the **Settings** page. There is a button that says **Launch Account Connections**, click that and then choose the provider to link to the existing account.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 404 error when logging in.
|
||||
|
||||
Ensure the `https://adventurelog.company/accounts` path is routed to the backend, as it shouldn't hit the frontend when it's properly configured. For information on how to configure this, refer to the AdventureLog documentation on reverse proxy configuration [here](https://adventurelog.app/docs/install/getting_started.html).
|
||||
|
||||
### authentik - No Permission
|
||||
|
||||
Launch your authentik dashboard as an admin and find the AdventureLog app. Click **More details** then **Edit**. In the admin interface, click **Test** under **Check Access**. If you get a 403 error, you need to grant the user the correct permissions. This can be done by going to the user's profile and adding the correct permissions.
|
95
website/integrations/services/engomo/index.md
Normal file
95
website/integrations/services/engomo/index.md
Normal file
@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Integrate with engomo
|
||||
sidebar_label: engomo
|
||||
---
|
||||
|
||||
# Integrate with engomo
|
||||
|
||||
<span class="badge badge--secondary">Support level: Community</span>
|
||||
|
||||
## What is engomo
|
||||
|
||||
> engomo is an low-code app development platform to create enterprise apps for smartphones and tablets based on Android, iOS, or iPadOS.
|
||||
> -- https://engomo.com/
|
||||
>
|
||||
> This guide explains how to set up engomo to use authentik as the OAuth provider for the application login on the smartphone/tablet and login to the admin WebGUI (composer).
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `engomo.company` is the FQDN of the engomo installation.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `engomo.mapping` is the name of the Scope Mapping.
|
||||
- `ak.cert` is the self-signed certificate that will be used for the service provider.
|
||||
|
||||
:::note
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
In authentik, create a new scope mapping. To do so, log in and navigate to the Admin interface, then go to **Customization --> Property Mapping** and click **Create**.
|
||||
|
||||
- `engomo.mapping` is the value of the Mapping's name.
|
||||
- `profile` is the value for the Scope name.
|
||||
- `return {"preferred_username": request.user.email}` is the value for the Expression.
|
||||
|
||||
Create an application and an OAuth2/OpenID provider in authentik. Use the following parameters for the OAuth2/OpenID provider:
|
||||
|
||||
**Provider:**
|
||||
|
||||
- Name: `SP-engomo`
|
||||
- Client type: `Public`
|
||||
- Redirect URIs/Origins (RegEx): `https://engomo.company/auth` and `com.engomo.engomo://callback/`
|
||||
- Signing Key: `ak.cert`
|
||||
- Scopes: `authentik default OAuth Mapping: OpenID 'email', 'offline_access', OpenID 'openid'` and `engomo.mapping`
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Redirect URIs => write the values line by line.
|
||||
|
||||
Leave the rest as default values. The durations can be changed as needed.
|
||||
|
||||
**Application:**
|
||||
|
||||
- Name: `engomo`
|
||||
- Slug: `engomo`
|
||||
- Launch URL: `https://engomo.company/`
|
||||
|
||||
## engomo configuration
|
||||
|
||||
Navigate to `https://engomo.company/composer` and log in with your admin credentials.
|
||||
|
||||
- Select `Server`.
|
||||
- Select `Authentication`.
|
||||
- Add a new authentication method by clicking on the plus icon on the right.
|
||||
- Name: `authentik`
|
||||
- Type: `OpenID Connect`
|
||||
- Click **Create**.
|
||||
- Set the `Issuer` to the authentik FQDN `https://authentik.company/application/o/engomo`.
|
||||
- Set the `Client ID` to the Client ID from the SP-engomo provider that you created in authentik.
|
||||
- Set the `Client Secret` to the Client Secret from the SP-engomo provider that you created in authentik.
|
||||
|
||||
Leave the rest as default.
|
||||
|
||||
## engomo user creation
|
||||
|
||||
engomo doesn't create users automatically when signing in. So you have to do it manually right now.
|
||||
Navigate to `https://engomo.company/composer` and log in with your admin credentials.
|
||||
|
||||
- Select `Users & Devices`.
|
||||
- Click the plus button next in the Users section.
|
||||
- Select `authentik` as the Authenticator in the dropdown.
|
||||
- Create your user by typing in the email as the Username used in authentik.
|
||||
|
||||
At this point you are done.
|
||||
|
||||
## Test the login
|
||||
|
||||
- Open a browser of your choice and open the URL `https://engomo.company`.
|
||||
- Enter the created user's email address and click the small arrow icon to log in.
|
||||
- You should be redirected to authentik (with the login flows you created) and then authentik should redirect you back to `https://engomo.company/composer` URL.
|
||||
- If you are redirected back to the `https://engomo.company/composer` URL you did everything correct.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The created user will only have access to the app or composer page if you granted the permission to the user of course.
|
@ -1,87 +0,0 @@
|
||||
---
|
||||
title: Integrate with engomo
|
||||
sidebar_label: engomo
|
||||
---
|
||||
|
||||
# Integrate with engomo
|
||||
|
||||
<span class="badge badge--secondary">Support level: Community</span>
|
||||
|
||||
## What is engomo
|
||||
|
||||
> engomo is an low-code app development platform to create enterprise apps for smartphones and tablets based on Android, iOS, or iPadOS.
|
||||
>
|
||||
> -- https://engomo.com/
|
||||
>
|
||||
> This guide explains how to set up engomo to use authentik as the OAuth provider for the application login on the smartphone/tablet and login to the admin WebGUI (composer).
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `engomo.company` is the FQDN of the engomo installation.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `engomo.mapping` is the name of the Scope Mapping.
|
||||
|
||||
:::note
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
In authentik, create a new scope mapping. To do so, log in and navigate to the Admin interface, then go to **Customization --> Property Mapping** and click **Create**.
|
||||
|
||||
- `engomo.mapping` is the value of the Mapping's name.
|
||||
- `profile` is the value for the Scope name.
|
||||
- `return {"preferred_username": request.user.email}` is the value for the Expression.
|
||||
|
||||
[Create](https://docs.goauthentik.io/docs/add-secure-apps/applications/manage_apps#add-new-applications) an OAuth2/OpenID provider and an application in authentik. Use the following parameters for the OAuth2/OpenID provider:
|
||||
|
||||
1. In the authentik Admin interface, navigate to **Applications** -> **Applications**.
|
||||
2. Use the wizard to create a new application and provider. During this process:
|
||||
- Note the **Client ID**, **Client Secret**, and **slug** values for later use.
|
||||
- Select implicit or explicit authorization flow as desired.
|
||||
- Set Client type to `Public`.
|
||||
- Set the redirect URI to <kbd>https://<em>engomo.company</em>/auth</kbd> and <kbd>com.engomo.engomo://callback/</kbd>.
|
||||
- Select any available signing key.
|
||||
- Add the `engomo.mapping` scope in addition to the default values.
|
||||
|
||||
:::note
|
||||
Redirect URIs => write the values line by line.
|
||||
:::
|
||||
|
||||
## engomo configuration
|
||||
|
||||
Navigate to <kbd>https://<em>engomo.company</em>/composer</kbd> and log in with your admin credentials.
|
||||
|
||||
1. Select **Server**.
|
||||
2. Select **Authentication**.
|
||||
3. Add a new authentication method by clicking on the plus icon on the right.
|
||||
4. Name: `authentik`
|
||||
5. Type: **OpenID Connect**
|
||||
6. Click **Create**.
|
||||
7. Configure the following values using information from the authentik provider:
|
||||
- Set **Issuer** to <kbd>https://<em>authentik.company</em>/application/o/<em>engomo</em></kbd>.
|
||||
- Set **Client ID** to the Client ID copied from authentik.
|
||||
- Set **Client secret** to the Client Secret copied from authentik.
|
||||
|
||||
## engomo user creation
|
||||
|
||||
engomo doesn't create users automatically when signing in. So you have to do it manually right now.
|
||||
Navigate to <kbd>https://<em>engomo.company</em>/composer</kbd> and log in with your admin credentials.
|
||||
|
||||
- Select **Users & Devices**.
|
||||
- Click the plus button in the Users section.
|
||||
- Choose `authentik` from the Authenticator dropdown.
|
||||
- Create your user by entering the email address as the username. This email must match the one used for the user in authentik.
|
||||
|
||||
## Test the login
|
||||
|
||||
- Open a browser of your choice and open the URL <kbd>https://<em>engomo.company</em></kbd>.
|
||||
- Enter the created user's email address and click the small arrow icon to log in.
|
||||
- You should be redirected to authentik (with the login flows you created) and then authentik should redirect you back to <kbd>https://<em>engomo.company</em>/composer</kbd> URL.
|
||||
- If you are redirected back to the <kbd>https://<em>engomo.company</em>/composer</kbd> URL you did everything correct.
|
||||
|
||||
:::note
|
||||
The created user will only have access to the app or composer page if they have been granted the necessary permissions.
|
||||
:::
|
@ -14,8 +14,6 @@ sidebar_label: RustDesk Server Pro
|
||||
> Ideal for businesses, it provides full control over data while ensuring scalable and reliable remote access.
|
||||
>
|
||||
> -- https://rustdesk.com/
|
||||
>
|
||||
> This guide explains how to configure Rustdesk Server Pro to use authentik as the OAuth provider for logging in to the Web GUI.
|
||||
|
||||
## Preparation
|
||||
|
||||
@ -30,30 +28,31 @@ This documentation lists only the settings that you need to change from their de
|
||||
|
||||
## authentik configuration
|
||||
|
||||
[Create](https://docs.goauthentik.io/docs/add-secure-apps/applications/manage_apps#add-new-applications) an OAuth2/OpenID provider and an application in authentik. Use the following parameters for the OAuth2/OpenID provider:
|
||||
|
||||
1. In the authentik Admin interface, navigate to **Applications** -> **Applications**.
|
||||
2. Use the wizard to create a new application and provider. During this process:
|
||||
- Note the **Client ID**, **Client Secret**, and **slug** values for later use.
|
||||
- Select implicit or explicit authorization flow as desired.
|
||||
- Set the redirect URI to <kbd>https://<em>rustdesk.company</em>/api/oidc/callback</kbd>.
|
||||
- Set the redirect URI to https://_rustdesk.company_/api/oidc/callback.
|
||||
- Select any available signing key.
|
||||
|
||||
## RustDesk Server Pro configuration
|
||||
|
||||
1. Sign in to RustDesk Server Pro using a browser.
|
||||
|
||||
2. In the left menu, select **Settings** and then **OIDC**.
|
||||
|
||||
3. Click **+ New Auth Provider**.
|
||||
|
||||
4. In the popup window, select **custom** as the **Auth Type** and click **OK**.
|
||||
|
||||
5. Configure the following values using information from the authentik provider:
|
||||
- Set **Name** to `authentik`
|
||||
- Set **Client ID** to the Client ID copied from authentik.
|
||||
- Set **Client secret** to the Client Secret copied from authentik.
|
||||
- Set **Issuer** to <kbd>https://<em>authentik.company</em>/application/o/<em>slug</em>/</kbd>
|
||||
- Set **Authorization Endpoint** to <kbd>https://<em>authentik.company</em>/application/o/authorize/</kbd>
|
||||
- Set **Token Endpoint** to <kbd>https://<em>authentik.company</em>/application/o/token/</kbd>
|
||||
- Set **Userinfo Endpoint** to <kbd>https://<em>authentik.company</em>/application/o/userinfo/</kbd>
|
||||
- Set **JWKS Endpoint** to <kbd>https://<em>authentik.company</em>/application/o/<em>slug</em>/jwks/</kbd>
|
||||
- **Name**: _SSO-Login_
|
||||
- **Client ID**: _client-id_
|
||||
- **Client Secret**: _client-secret_
|
||||
- **Issuer**: https://_authentik.company_/application/o/_slug_/
|
||||
- **Authorization Endpoint**: https://_authentik.company_/application/o/authorize/
|
||||
- **Token Endpoint**: https://_authentik.company_/application/o/token/
|
||||
- **Userinfo Endpoint**: https://_authentik.company_/application/o/userinfo/
|
||||
- **JWKS Endpoint**: https://_authentik.company_/application/o/_slug_/jwks/
|
||||
|
||||
:::info
|
||||
Users are created automatically on login. Permissions must be assigned by an administrator after user creation.
|
||||
@ -61,7 +60,7 @@ Users are created automatically on login. Permissions must be assigned by an adm
|
||||
|
||||
## Test the Login
|
||||
|
||||
- Open a browser and navigate to <kbd>https://<em>rustdesk.company</em></kbd>.
|
||||
- Click **Continue with authentik**.
|
||||
- You should be redirected to authentik (with the login flows you configured). After logging in, authentik will redirect you back to <kbd>https://<em>rustdesk.company</em></kbd>.
|
||||
- If you are redirected back to <kbd>https://<em>rustdesk.company</em></kbd> and can read the username in the top right corner, the setup was successful.
|
||||
- Open a browser and navigate to https://_rustdesk.company_.
|
||||
- Click **Continue with SSO-Login**.
|
||||
- You should be redirected to authentik (with the login flows you configured). After logging in, authentik will redirect you back to https://_rustdesk.company_.
|
||||
- If you are redirected back to https://_rustdesk.company_ and can read the username in the top right corner, the setup was successful.
|
||||
|
@ -28,14 +28,27 @@ This documentation lists only the settings that you need to change from their de
|
||||
|
||||
## authentik configuration
|
||||
|
||||
[Create](https://docs.goauthentik.io/docs/add-secure-apps/applications/manage_apps#add-new-applications) an OAuth2/OpenID provider and an application in authentik. Use the following parameters for the OAuth2/OpenID provider:
|
||||
Start the wizard for adding a new application.
|
||||
|
||||
1. In the authentik Admin interface, navigate to **Applications** -> **Applications**.
|
||||
2. Use the wizard to create a new application and provider. During this process:
|
||||
- Note the **Client ID**, **Client Secret**, and **slug** values for later use.
|
||||
- Select implicit or explicit authorization flow as desired.
|
||||
- Set the redirect URI to <kbd>https://<em>semaphore.company</em>/api/auth/oidc/authentik/redirect/</kbd>.
|
||||
- Select any available signing key.
|
||||
**1. Application:**
|
||||
|
||||
- Name: `Semaphore UI`
|
||||
- Slug: `semaphore`
|
||||
|
||||
**2. Choose a Provider**
|
||||
|
||||
Select `OAuth2/OpenID Provider`
|
||||
|
||||
**3. Configure Provider**
|
||||
|
||||
Select implicit or explicit authorization flow as desired.
|
||||
|
||||
Take note of the Client ID and Client Secret, you'll need to give them to Semaphore UI later.
|
||||
|
||||
- Redirect URIs/Origins (RegEx): `https://semaphore.company/api/auth/oidc/authentik/redirect/`
|
||||
- Signing Key: `authentik Self-signed Certificate`
|
||||
|
||||
Leave the rest as default values.
|
||||
|
||||
## Semaphore UI configuration
|
||||
|
||||
@ -47,7 +60,7 @@ Add the `oidc_providers` configuration:
|
||||
{
|
||||
"oidc_providers": {
|
||||
"authentik": {
|
||||
"display_name": "Sign in with authentik",
|
||||
"display_name": "Sign in with Authentik",
|
||||
"provider_url": "https://authentik.company/application/o/<slug>/",
|
||||
"client_id": "<client-id>",
|
||||
"client_secret": "<client-secret>",
|
||||
@ -76,12 +89,14 @@ SEMAPHORE_WEB_ROOT: /
|
||||
|
||||
More information on this can be found in the Semaphore documentation https://docs.semaphoreui.com/administration-guide/openid/authentik/.
|
||||
|
||||
Leave the rest as default.
|
||||
|
||||
## Test the login
|
||||
|
||||
- Open a browser of your choice and open the URL <kbd>https://<em>semaphore.company</em></kbd>.
|
||||
- Open a browser of your choice and open the URL `https://semaphore.company`.
|
||||
- Click on the SSO-Login button.
|
||||
- You should be redirected to authentik (with the login flows you created) and then authentik should redirect you back to <kbd>https://<em>semaphore.company</em></kbd> URL.
|
||||
- If you are redirected back to the <kbd>https://<em>semaphore.company</em></kbd> URL you did everything correct.
|
||||
- You should be redirected to authentik (with the login flows you created) and then authentik should redirect you back to `https://semaphore.company` URL.
|
||||
- If you are redirected back to the `https://semaphore.company` URL you did everything correct.
|
||||
|
||||
:::info
|
||||
Users are created upon logging in with authentik. They will not have the rights to create anything initially. These permissions must be assigned later by the local admin created during the first login to the Semaphore UI.
|
||||
|
1192
website/package-lock.json
generated
1192
website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -123,7 +123,6 @@ module.exports = {
|
||||
label: "Miscellaneous",
|
||||
items: [
|
||||
"services/actual-budget/index",
|
||||
"services/adventurelog/index",
|
||||
"services/engomo/index",
|
||||
"services/frappe/index",
|
||||
"services/freshrss/index",
|
||||
|
Reference in New Issue
Block a user