Compare commits
12 Commits
main
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
2739376a2a | |||
152121175b | |||
1d57a258f3 | |||
f15cac39c8 | |||
ce77d82b24 | |||
c3fe57197d | |||
267938d435 | |||
6a7c2e0662 | |||
5336afb1b4 | |||
9bb44055a3 | |||
143663d293 | |||
bd54d034e1 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2023.5.3
|
current_version = 2023.5.4
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from os import environ
|
from os import environ
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
__version__ = "2023.5.3"
|
__version__ = "2023.5.4"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""authentik administration overview"""
|
"""authentik administration overview"""
|
||||||
import os
|
|
||||||
import platform
|
import platform
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sys import version as python_version
|
from sys import version as python_version
|
||||||
@ -34,7 +33,6 @@ class RuntimeDict(TypedDict):
|
|||||||
class SystemSerializer(PassiveSerializer):
|
class SystemSerializer(PassiveSerializer):
|
||||||
"""Get system information."""
|
"""Get system information."""
|
||||||
|
|
||||||
env = SerializerMethodField()
|
|
||||||
http_headers = SerializerMethodField()
|
http_headers = SerializerMethodField()
|
||||||
http_host = SerializerMethodField()
|
http_host = SerializerMethodField()
|
||||||
http_is_secure = SerializerMethodField()
|
http_is_secure = SerializerMethodField()
|
||||||
@ -43,10 +41,6 @@ class SystemSerializer(PassiveSerializer):
|
|||||||
server_time = SerializerMethodField()
|
server_time = SerializerMethodField()
|
||||||
embedded_outpost_host = SerializerMethodField()
|
embedded_outpost_host = SerializerMethodField()
|
||||||
|
|
||||||
def get_env(self, request: Request) -> dict[str, str]:
|
|
||||||
"""Get Environment"""
|
|
||||||
return os.environ.copy()
|
|
||||||
|
|
||||||
def get_http_headers(self, request: Request) -> dict[str, str]:
|
def get_http_headers(self, request: Request) -> dict[str, str]:
|
||||||
"""Get HTTP Request headers"""
|
"""Get HTTP Request headers"""
|
||||||
headers = {}
|
headers = {}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""API Authentication"""
|
"""API Authentication"""
|
||||||
|
from hmac import compare_digest
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -78,7 +79,7 @@ def token_secret_key(value: str) -> Optional[User]:
|
|||||||
and return the service account for the managed outpost"""
|
and return the service account for the managed outpost"""
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||||
|
|
||||||
if value != settings.SECRET_KEY:
|
if not compare_digest(value, settings.SECRET_KEY):
|
||||||
return None
|
return None
|
||||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||||
if not outposts:
|
if not outposts:
|
||||||
|
@ -82,7 +82,10 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
|||||||
def retrieve_file(self) -> str:
|
def retrieve_file(self) -> str:
|
||||||
"""Get blueprint from path"""
|
"""Get blueprint from path"""
|
||||||
try:
|
try:
|
||||||
full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path))
|
base = Path(CONFIG.y("blueprints_dir"))
|
||||||
|
full_path = base.joinpath(Path(self.path)).resolve()
|
||||||
|
if not str(full_path).startswith(str(base.resolve())):
|
||||||
|
raise BlueprintRetrievalFailed("Invalid blueprint path")
|
||||||
with full_path.open("r", encoding="utf-8") as _file:
|
with full_path.open("r", encoding="utf-8") as _file:
|
||||||
return _file.read()
|
return _file.read()
|
||||||
except (IOError, OSError) as exc:
|
except (IOError, OSError) as exc:
|
||||||
|
@ -1,34 +1,15 @@
|
|||||||
"""authentik managed models tests"""
|
"""authentik managed models tests"""
|
||||||
from typing import Callable, Type
|
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from authentik.blueprints.v1.importer import is_model_allowed
|
from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed
|
||||||
from authentik.lib.models import SerializerModel
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
class TestModels(TestCase):
|
class TestModels(TestCase):
|
||||||
"""Test Models"""
|
"""Test Models"""
|
||||||
|
|
||||||
|
def test_retrieve_file(self):
|
||||||
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
|
"""Test retrieve_file"""
|
||||||
"""Test serializer"""
|
instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts")
|
||||||
|
with self.assertRaises(BlueprintRetrievalFailed):
|
||||||
def tester(self: TestModels):
|
instance.retrieve()
|
||||||
if test_model._meta.abstract: # pragma: no cover
|
|
||||||
return
|
|
||||||
model_class = test_model()
|
|
||||||
self.assertTrue(isinstance(model_class, SerializerModel))
|
|
||||||
self.assertIsNotNone(model_class.serializer)
|
|
||||||
|
|
||||||
return tester
|
|
||||||
|
|
||||||
|
|
||||||
for app in apps.get_app_configs():
|
|
||||||
if not app.label.startswith("authentik"):
|
|
||||||
continue
|
|
||||||
for model in app.get_models():
|
|
||||||
if not is_model_allowed(model):
|
|
||||||
continue
|
|
||||||
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
|
|
||||||
|
34
authentik/blueprints/tests/test_serializer_models.py
Normal file
34
authentik/blueprints/tests/test_serializer_models.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""authentik managed models tests"""
|
||||||
|
from typing import Callable, Type
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik.blueprints.v1.importer import is_model_allowed
|
||||||
|
from authentik.lib.models import SerializerModel
|
||||||
|
|
||||||
|
|
||||||
|
class TestModels(TestCase):
|
||||||
|
"""Test Models"""
|
||||||
|
|
||||||
|
|
||||||
|
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
|
||||||
|
"""Test serializer"""
|
||||||
|
|
||||||
|
def tester(self: TestModels):
|
||||||
|
if test_model._meta.abstract: # pragma: no cover
|
||||||
|
return
|
||||||
|
model_class = test_model()
|
||||||
|
self.assertTrue(isinstance(model_class, SerializerModel))
|
||||||
|
self.assertIsNotNone(model_class.serializer)
|
||||||
|
|
||||||
|
return tester
|
||||||
|
|
||||||
|
|
||||||
|
for app in apps.get_app_configs():
|
||||||
|
if not app.label.startswith("authentik"):
|
||||||
|
continue
|
||||||
|
for model in app.get_models():
|
||||||
|
if not is_model_allowed(model):
|
||||||
|
continue
|
||||||
|
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
|
@ -67,11 +67,12 @@ from authentik.core.models import (
|
|||||||
TokenIntents,
|
TokenIntents,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import FlowToken
|
from authentik.flows.models import FlowToken
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.stages.email.tasks import send_mails
|
from authentik.stages.email.tasks import send_mails
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
@ -543,6 +544,58 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
send_mails(email_stage, message)
|
send_mails(email_stage, message)
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
|
@permission_required("authentik_core.impersonate")
|
||||||
|
@extend_schema(
|
||||||
|
request=OpenApiTypes.NONE,
|
||||||
|
responses={
|
||||||
|
"204": OpenApiResponse(description="Successfully started impersonation"),
|
||||||
|
"401": OpenApiResponse(description="Access denied"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=["POST"])
|
||||||
|
def impersonate(self, request: Request, pk: int) -> Response:
|
||||||
|
"""Impersonate a user"""
|
||||||
|
if not CONFIG.y_bool("impersonation"):
|
||||||
|
LOGGER.debug("User attempted to impersonate", user=request.user)
|
||||||
|
return Response(status=401)
|
||||||
|
if not request.user.has_perm("impersonate"):
|
||||||
|
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
|
||||||
|
return Response(status=401)
|
||||||
|
|
||||||
|
user_to_be = self.get_object()
|
||||||
|
|
||||||
|
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||||
|
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
||||||
|
|
||||||
|
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||||
|
|
||||||
|
return Response(status=201)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=OpenApiTypes.NONE,
|
||||||
|
responses={
|
||||||
|
"204": OpenApiResponse(description="Successfully started impersonation"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=False, methods=["GET"])
|
||||||
|
def impersonate_end(self, request: Request) -> Response:
|
||||||
|
"""End Impersonation a user"""
|
||||||
|
if (
|
||||||
|
SESSION_KEY_IMPERSONATE_USER not in request.session
|
||||||
|
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||||
|
):
|
||||||
|
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
|
||||||
|
del request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||||
|
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
|
||||||
|
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
||||||
|
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
||||||
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
||||||
for backend in list(self.filter_backends):
|
for backend in list(self.filter_backends):
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
"""impersonation tests"""
|
"""impersonation tests"""
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from django.test.testcases import TestCase
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
|
||||||
|
|
||||||
class TestImpersonation(TestCase):
|
class TestImpersonation(APITestCase):
|
||||||
"""impersonation tests"""
|
"""impersonation tests"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
@ -23,10 +23,10 @@ class TestImpersonation(TestCase):
|
|||||||
self.other_user.save()
|
self.other_user.save()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
self.client.get(
|
self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_core:impersonate-init",
|
"authentik_api:user-impersonate",
|
||||||
kwargs={"user_id": self.other_user.pk},
|
kwargs={"pk": self.other_user.pk},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ class TestImpersonation(TestCase):
|
|||||||
self.assertEqual(response_body["user"]["username"], self.other_user.username)
|
self.assertEqual(response_body["user"]["username"], self.other_user.username)
|
||||||
self.assertEqual(response_body["original"]["username"], self.user.username)
|
self.assertEqual(response_body["original"]["username"], self.user.username)
|
||||||
|
|
||||||
self.client.get(reverse("authentik_core:impersonate-end"))
|
self.client.get(reverse("authentik_api:user-impersonate-end"))
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
response_body = loads(response.content.decode())
|
response_body = loads(response.content.decode())
|
||||||
@ -46,9 +46,7 @@ class TestImpersonation(TestCase):
|
|||||||
"""test impersonation without permissions"""
|
"""test impersonation without permissions"""
|
||||||
self.client.force_login(self.other_user)
|
self.client.force_login(self.other_user)
|
||||||
|
|
||||||
self.client.get(
|
self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}))
|
||||||
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
response_body = loads(response.content.decode())
|
response_body = loads(response.content.decode())
|
||||||
@ -58,5 +56,5 @@ class TestImpersonation(TestCase):
|
|||||||
"""test un-impersonation without impersonating first"""
|
"""test un-impersonation without impersonating first"""
|
||||||
self.client.force_login(self.other_user)
|
self.client.force_login(self.other_user)
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_core:impersonate-end"))
|
response = self.client.get(reverse("authentik_api:user-impersonate-end"))
|
||||||
self.assertRedirects(response, reverse("authentik_core:if-user"))
|
self.assertEqual(response.status_code, 204)
|
||||||
|
@ -16,7 +16,7 @@ from authentik.core.api.providers import ProviderViewSet
|
|||||||
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
||||||
from authentik.core.api.tokens import TokenViewSet
|
from authentik.core.api.tokens import TokenViewSet
|
||||||
from authentik.core.api.users import UserViewSet
|
from authentik.core.api.users import UserViewSet
|
||||||
from authentik.core.views import apps, impersonate
|
from authentik.core.views import apps
|
||||||
from authentik.core.views.debug import AccessDeniedView
|
from authentik.core.views.debug import AccessDeniedView
|
||||||
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
|
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
|
||||||
from authentik.core.views.session import EndSessionView
|
from authentik.core.views.session import EndSessionView
|
||||||
@ -38,17 +38,6 @@ urlpatterns = [
|
|||||||
apps.RedirectToAppLaunch.as_view(),
|
apps.RedirectToAppLaunch.as_view(),
|
||||||
name="application-launch",
|
name="application-launch",
|
||||||
),
|
),
|
||||||
# Impersonation
|
|
||||||
path(
|
|
||||||
"-/impersonation/<int:user_id>/",
|
|
||||||
impersonate.ImpersonateInitView.as_view(),
|
|
||||||
name="impersonate-init",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"-/impersonation/end/",
|
|
||||||
impersonate.ImpersonateEndView.as_view(),
|
|
||||||
name="impersonate-end",
|
|
||||||
),
|
|
||||||
# Interfaces
|
# Interfaces
|
||||||
path(
|
path(
|
||||||
"if/admin/",
|
"if/admin/",
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
"""authentik impersonation views"""
|
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
|
||||||
from django.views import View
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.core.middleware import (
|
|
||||||
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
|
|
||||||
SESSION_KEY_IMPERSONATE_USER,
|
|
||||||
)
|
|
||||||
from authentik.core.models import User
|
|
||||||
from authentik.events.models import Event, EventAction
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateInitView(View):
|
|
||||||
"""Initiate Impersonation"""
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
|
|
||||||
"""Impersonation handler, checks permissions"""
|
|
||||||
if not CONFIG.y_bool("impersonation"):
|
|
||||||
LOGGER.debug("User attempted to impersonate", user=request.user)
|
|
||||||
return HttpResponse("Unauthorized", status=401)
|
|
||||||
if not request.user.has_perm("impersonate"):
|
|
||||||
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
|
|
||||||
return HttpResponse("Unauthorized", status=401)
|
|
||||||
|
|
||||||
user_to_be = get_object_or_404(User, pk=user_id)
|
|
||||||
|
|
||||||
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
|
||||||
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
|
||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
|
||||||
|
|
||||||
return redirect("authentik_core:if-user")
|
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateEndView(View):
|
|
||||||
"""End User impersonation"""
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
|
||||||
"""End Impersonation handler"""
|
|
||||||
if (
|
|
||||||
SESSION_KEY_IMPERSONATE_USER not in request.session
|
|
||||||
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
|
||||||
):
|
|
||||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
|
||||||
return redirect("authentik_core:if-user")
|
|
||||||
|
|
||||||
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
|
||||||
|
|
||||||
del request.session[SESSION_KEY_IMPERSONATE_USER]
|
|
||||||
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
|
||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
|
||||||
|
|
||||||
return redirect("authentik_core:root-redirect")
|
|
@ -23,7 +23,8 @@ class DiagramElement:
|
|||||||
style: list[str] = field(default_factory=lambda: ["[", "]"])
|
style: list[str] = field(default_factory=lambda: ["[", "]"])
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
element = f'{self.identifier}{self.style[0]}"{self.description}"{self.style[1]}'
|
description = self.description.replace('"', "#quot;")
|
||||||
|
element = f'{self.identifier}{self.style[0]}"{description}"{self.style[1]}'
|
||||||
if self.action is not None:
|
if self.action is not None:
|
||||||
if self.action != "":
|
if self.action != "":
|
||||||
element = f"--{self.action}--> {element}"
|
element = f"--{self.action}--> {element}"
|
||||||
|
@ -204,12 +204,12 @@ class ChallengeStageView(StageView):
|
|||||||
for field, errors in response.errors.items():
|
for field, errors in response.errors.items():
|
||||||
for error in errors:
|
for error in errors:
|
||||||
full_errors.setdefault(field, [])
|
full_errors.setdefault(field, [])
|
||||||
full_errors[field].append(
|
field_error = {
|
||||||
{
|
"string": str(error),
|
||||||
"string": str(error),
|
}
|
||||||
"code": error.code,
|
if hasattr(error, "code"):
|
||||||
}
|
field_error["code"] = error.code
|
||||||
)
|
full_errors[field].append(field_error)
|
||||||
challenge_response.initial_data["response_errors"] = full_errors
|
challenge_response.initial_data["response_errors"] = full_errors
|
||||||
if not challenge_response.is_valid():
|
if not challenge_response.is_valid():
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
|
@ -132,9 +132,9 @@ class TestPolicyProcess(TestCase):
|
|||||||
)
|
)
|
||||||
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
|
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
|
||||||
|
|
||||||
http_request = self.factory.get(reverse("authentik_core:impersonate-end"))
|
http_request = self.factory.get(reverse("authentik_api:user-impersonate-end"))
|
||||||
http_request.user = self.user
|
http_request.user = self.user
|
||||||
http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end"))
|
http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end"))
|
||||||
|
|
||||||
request = PolicyRequest(self.user)
|
request = PolicyRequest(self.user)
|
||||||
request.set_http_request(http_request)
|
request.set_http_request(http_request)
|
||||||
|
@ -66,8 +66,8 @@ def ldap_sync_password(sender, user: User, password: str, **_):
|
|||||||
if not sources.exists():
|
if not sources.exists():
|
||||||
return
|
return
|
||||||
source = sources.first()
|
source = sources.first()
|
||||||
changer = LDAPPasswordChanger(source)
|
|
||||||
try:
|
try:
|
||||||
|
changer = LDAPPasswordChanger(source)
|
||||||
changer.change_password(user, password)
|
changer.change_password(user, password)
|
||||||
except LDAPOperationResult as exc:
|
except LDAPOperationResult as exc:
|
||||||
LOGGER.warning("failed to set LDAP password", exc=exc)
|
LOGGER.warning("failed to set LDAP password", exc=exc)
|
||||||
|
@ -133,6 +133,12 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
|
|||||||
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
|
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
|
||||||
if not device:
|
if not device:
|
||||||
raise ValidationError("Invalid device")
|
raise ValidationError("Invalid device")
|
||||||
|
# We can only check the device's user if the user we're given isn't anonymous
|
||||||
|
# as this validation is also used for password-less login where webauthn is the very first
|
||||||
|
# step done by a user. Only if this validation happens at a later stage we can check
|
||||||
|
# that the device belongs to the user
|
||||||
|
if not user.is_anonymous and device.user != user:
|
||||||
|
raise ValidationError("Invalid device")
|
||||||
|
|
||||||
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
|
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
|
||||||
|
|
||||||
|
@ -36,9 +36,9 @@ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_ME
|
|||||||
|
|
||||||
COOKIE_NAME_MFA = "authentik_mfa"
|
COOKIE_NAME_MFA = "authentik_mfa"
|
||||||
|
|
||||||
SESSION_KEY_STAGES = "authentik/stages/authenticator_validate/stages"
|
PLAN_CONTEXT_STAGES = "goauthentik.io/stages/authenticator_validate/stages"
|
||||||
SESSION_KEY_SELECTED_STAGE = "authentik/stages/authenticator_validate/selected_stage"
|
PLAN_CONTEXT_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage"
|
||||||
SESSION_KEY_DEVICE_CHALLENGES = "authentik/stages/authenticator_validate/device_challenges"
|
PLAN_CONTEXT_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges"
|
||||||
|
|
||||||
|
|
||||||
class SelectableStageSerializer(PassiveSerializer):
|
class SelectableStageSerializer(PassiveSerializer):
|
||||||
@ -72,8 +72,8 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
|||||||
component = CharField(default="ak-stage-authenticator-validate")
|
component = CharField(default="ak-stage-authenticator-validate")
|
||||||
|
|
||||||
def _challenge_allowed(self, classes: list):
|
def _challenge_allowed(self, classes: list):
|
||||||
device_challenges: list[dict] = self.stage.request.session.get(
|
device_challenges: list[dict] = self.stage.executor.plan.context.get(
|
||||||
SESSION_KEY_DEVICE_CHALLENGES, []
|
PLAN_CONTEXT_DEVICE_CHALLENGES, []
|
||||||
)
|
)
|
||||||
if not any(x["device_class"] in classes for x in device_challenges):
|
if not any(x["device_class"] in classes for x in device_challenges):
|
||||||
raise ValidationError("No compatible device class allowed")
|
raise ValidationError("No compatible device class allowed")
|
||||||
@ -103,7 +103,9 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
|||||||
"""Check which challenge the user has selected. Actual logic only used for SMS stage."""
|
"""Check which challenge the user has selected. Actual logic only used for SMS stage."""
|
||||||
# First check if the challenge is valid
|
# First check if the challenge is valid
|
||||||
allowed = False
|
allowed = False
|
||||||
for device_challenge in self.stage.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []):
|
for device_challenge in self.stage.executor.plan.context.get(
|
||||||
|
PLAN_CONTEXT_DEVICE_CHALLENGES, []
|
||||||
|
):
|
||||||
if device_challenge.get("device_class", "") == challenge.get(
|
if device_challenge.get("device_class", "") == challenge.get(
|
||||||
"device_class", ""
|
"device_class", ""
|
||||||
) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""):
|
) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""):
|
||||||
@ -121,11 +123,11 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
|||||||
|
|
||||||
def validate_selected_stage(self, stage_pk: str) -> str:
|
def validate_selected_stage(self, stage_pk: str) -> str:
|
||||||
"""Check that the selected stage is valid"""
|
"""Check that the selected stage is valid"""
|
||||||
stages = self.stage.request.session.get(SESSION_KEY_STAGES, [])
|
stages = self.stage.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
|
||||||
if not any(str(stage.pk) == stage_pk for stage in stages):
|
if not any(str(stage.pk) == stage_pk for stage in stages):
|
||||||
raise ValidationError("Selected stage is invalid")
|
raise ValidationError("Selected stage is invalid")
|
||||||
self.stage.logger.debug("Setting selected stage to ", stage=stage_pk)
|
self.stage.logger.debug("Setting selected stage to ", stage=stage_pk)
|
||||||
self.stage.request.session[SESSION_KEY_SELECTED_STAGE] = stage_pk
|
self.stage.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = stage_pk
|
||||||
return stage_pk
|
return stage_pk
|
||||||
|
|
||||||
def validate(self, attrs: dict):
|
def validate(self, attrs: dict):
|
||||||
@ -230,7 +232,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
else:
|
else:
|
||||||
self.logger.debug("No pending user, continuing")
|
self.logger.debug("No pending user, continuing")
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
self.request.session[SESSION_KEY_DEVICE_CHALLENGES] = challenges
|
self.executor.plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = challenges
|
||||||
|
|
||||||
# No allowed devices
|
# No allowed devices
|
||||||
if len(challenges) < 1:
|
if len(challenges) < 1:
|
||||||
@ -263,23 +265,23 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
if stage.configuration_stages.count() == 1:
|
if stage.configuration_stages.count() == 1:
|
||||||
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
|
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
|
||||||
self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
|
self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
|
||||||
self.request.session[SESSION_KEY_SELECTED_STAGE] = next_stage
|
self.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = next_stage
|
||||||
# Because that normal execution only happens on post, we directly inject it here and
|
# Because that normal execution only happens on post, we directly inject it here and
|
||||||
# return it
|
# return it
|
||||||
self.executor.plan.insert_stage(next_stage)
|
self.executor.plan.insert_stage(next_stage)
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
|
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
|
||||||
self.request.session[SESSION_KEY_STAGES] = stages
|
self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages
|
||||||
return super().get(self.request, *args, **kwargs)
|
return super().get(self.request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
res = super().post(request, *args, **kwargs)
|
res = super().post(request, *args, **kwargs)
|
||||||
if (
|
if (
|
||||||
SESSION_KEY_SELECTED_STAGE in self.request.session
|
PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context
|
||||||
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
|
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
|
||||||
):
|
):
|
||||||
self.logger.debug("Got selected stage in session, running that")
|
self.logger.debug("Got selected stage in context, running that")
|
||||||
stage_pk = self.request.session.get(SESSION_KEY_SELECTED_STAGE)
|
stage_pk = self.executor.plan.context(PLAN_CONTEXT_SELECTED_STAGE)
|
||||||
# Because the foreign key to stage.configuration_stage points to
|
# Because the foreign key to stage.configuration_stage points to
|
||||||
# a base stage class, we need to do another lookup
|
# a base stage class, we need to do another lookup
|
||||||
stage = Stage.objects.get_subclass(pk=stage_pk)
|
stage = Stage.objects.get_subclass(pk=stage_pk)
|
||||||
@ -290,8 +292,8 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
def get_challenge(self) -> AuthenticatorValidationChallenge:
|
def get_challenge(self) -> AuthenticatorValidationChallenge:
|
||||||
challenges = self.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, [])
|
challenges = self.executor.plan.context.get(PLAN_CONTEXT_DEVICE_CHALLENGES, [])
|
||||||
stages = self.request.session.get(SESSION_KEY_STAGES, [])
|
stages = self.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
|
||||||
stage_challenges = []
|
stage_challenges = []
|
||||||
for stage in stages:
|
for stage in stages:
|
||||||
serializer = SelectableStageSerializer(
|
serializer = SelectableStageSerializer(
|
||||||
@ -306,6 +308,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
stage_challenges.append(serializer.data)
|
stage_challenges.append(serializer.data)
|
||||||
return AuthenticatorValidationChallenge(
|
return AuthenticatorValidationChallenge(
|
||||||
data={
|
data={
|
||||||
|
"component": "ak-stage-authenticator-validate",
|
||||||
"type": ChallengeTypes.NATIVE.value,
|
"type": ChallengeTypes.NATIVE.value,
|
||||||
"device_challenges": challenges,
|
"device_challenges": challenges,
|
||||||
"configuration_stages": stage_challenges,
|
"configuration_stages": stage_challenges,
|
||||||
@ -385,8 +388,3 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
"device": webauthn_device,
|
"device": webauthn_device,
|
||||||
}
|
}
|
||||||
return self.set_valid_mfa_cookie(response.device)
|
return self.set_valid_mfa_cookie(response.device)
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
self.request.session.pop(SESSION_KEY_STAGES, None)
|
|
||||||
self.request.session.pop(SESSION_KEY_SELECTED_STAGE, None)
|
|
||||||
self.request.session.pop(SESSION_KEY_DEVICE_CHALLENGES, None)
|
|
||||||
|
@ -1,26 +1,19 @@
|
|||||||
"""Test validator stage"""
|
"""Test validator stage"""
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
|
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
|
||||||
from authentik.flows.planner import FlowPlan
|
from authentik.flows.planner import FlowPlan
|
||||||
from authentik.flows.stage import StageView
|
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.lib.tests.utils import dummy_get_response
|
|
||||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||||
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
|
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
from authentik.stages.authenticator_validate.stage import (
|
from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
|
||||||
SESSION_KEY_DEVICE_CHALLENGES,
|
|
||||||
AuthenticatorValidationChallengeResponse,
|
|
||||||
)
|
|
||||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||||
|
|
||||||
|
|
||||||
@ -86,12 +79,17 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
|||||||
|
|
||||||
def test_validate_selected_challenge(self):
|
def test_validate_selected_challenge(self):
|
||||||
"""Test validate_selected_challenge"""
|
"""Test validate_selected_challenge"""
|
||||||
# Prepare request with session
|
flow = create_test_flow()
|
||||||
request = self.request_factory.get("/")
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
|
device_classes=[DeviceClasses.STATIC, DeviceClasses.TOTP],
|
||||||
|
)
|
||||||
|
|
||||||
middleware = SessionMiddleware(dummy_get_response)
|
session = self.client.session
|
||||||
middleware.process_request(request)
|
plan = FlowPlan(flow_pk=flow.pk.hex)
|
||||||
request.session[SESSION_KEY_DEVICE_CHALLENGES] = [
|
plan.append_stage(stage)
|
||||||
|
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
|
||||||
{
|
{
|
||||||
"device_class": "static",
|
"device_class": "static",
|
||||||
"device_uid": "1",
|
"device_uid": "1",
|
||||||
@ -101,23 +99,43 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
|||||||
"device_uid": "2",
|
"device_uid": "2",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
request.session.save()
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
|
||||||
res = AuthenticatorValidationChallengeResponse()
|
response = self.client.post(
|
||||||
res.stage = StageView(FlowExecutorView())
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
res.stage.request = request
|
data={
|
||||||
with self.assertRaises(ValidationError):
|
"selected_challenge": {
|
||||||
res.validate_selected_challenge(
|
|
||||||
{
|
|
||||||
"device_class": "baz",
|
"device_class": "baz",
|
||||||
"device_uid": "quox",
|
"device_uid": "quox",
|
||||||
|
"challenge": {},
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
res.validate_selected_challenge(
|
)
|
||||||
{
|
self.assertStageResponse(
|
||||||
"device_class": "static",
|
response,
|
||||||
"device_uid": "1",
|
flow,
|
||||||
}
|
response_errors={
|
||||||
|
"selected_challenge": [{"string": "invalid challenge selected", "code": "invalid"}]
|
||||||
|
},
|
||||||
|
component="ak-stage-authenticator-validate",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
|
data={
|
||||||
|
"selected_challenge": {
|
||||||
|
"device_class": "static",
|
||||||
|
"device_uid": "1",
|
||||||
|
"challenge": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertStageResponse(
|
||||||
|
response,
|
||||||
|
flow,
|
||||||
|
response_errors={"non_field_errors": [{"string": "Empty response", "code": "invalid"}]},
|
||||||
|
component="ak-stage-authenticator-validate",
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
|
@ -22,7 +22,7 @@ from authentik.stages.authenticator_validate.challenge import (
|
|||||||
)
|
)
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
from authentik.stages.authenticator_validate.stage import (
|
from authentik.stages.authenticator_validate.stage import (
|
||||||
SESSION_KEY_DEVICE_CHALLENGES,
|
PLAN_CONTEXT_DEVICE_CHALLENGES,
|
||||||
AuthenticatorValidateStageView,
|
AuthenticatorValidateStageView,
|
||||||
)
|
)
|
||||||
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
||||||
@ -211,14 +211,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
plan.append_stage(UserLoginStage(name=generate_id()))
|
plan.append_stage(UserLoginStage(name=generate_id()))
|
||||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||||
session[SESSION_KEY_PLAN] = plan
|
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
|
||||||
session[SESSION_KEY_DEVICE_CHALLENGES] = [
|
|
||||||
{
|
{
|
||||||
"device_class": device.__class__.__name__.lower().replace("device", ""),
|
"device_class": device.__class__.__name__.lower().replace("device", ""),
|
||||||
"device_uid": device.pk,
|
"device_uid": device.pk,
|
||||||
"challenge": {},
|
"challenge": {},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||||
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
||||||
)
|
)
|
||||||
@ -283,14 +283,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
plan = FlowPlan(flow_pk=flow.pk.hex)
|
plan = FlowPlan(flow_pk=flow.pk.hex)
|
||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
plan.append_stage(UserLoginStage(name=generate_id()))
|
plan.append_stage(UserLoginStage(name=generate_id()))
|
||||||
session[SESSION_KEY_PLAN] = plan
|
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
|
||||||
session[SESSION_KEY_DEVICE_CHALLENGES] = [
|
|
||||||
{
|
{
|
||||||
"device_class": device.__class__.__name__.lower().replace("device", ""),
|
"device_class": device.__class__.__name__.lower().replace("device", ""),
|
||||||
"device_uid": device.pk,
|
"device_uid": device.pk,
|
||||||
"challenge": {},
|
"challenge": {},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||||
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
||||||
)
|
)
|
||||||
|
@ -32,7 +32,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.4}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -50,7 +50,7 @@ services:
|
|||||||
- "${COMPOSE_PORT_HTTP:-9000}:9000"
|
- "${COMPOSE_PORT_HTTP:-9000}:9000"
|
||||||
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
|
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.4}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2023.5.3"
|
const VERSION = "2023.5.4"
|
||||||
|
@ -113,7 +113,7 @@ filterwarnings = [
|
|||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "authentik"
|
name = "authentik"
|
||||||
version = "2023.5.3"
|
version = "2023.5.4"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||||
|
|
||||||
|
64
schema.yml
64
schema.yml
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2023.5.3
|
version: 2023.5.4
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@goauthentik.io
|
email: hello@goauthentik.io
|
||||||
@ -4783,6 +4783,38 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
description: ''
|
description: ''
|
||||||
|
/core/users/{id}/impersonate/:
|
||||||
|
post:
|
||||||
|
operationId: core_users_impersonate_create
|
||||||
|
description: Impersonate a user
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
description: A unique integer value identifying this User.
|
||||||
|
required: true
|
||||||
|
tags:
|
||||||
|
- core
|
||||||
|
security:
|
||||||
|
- authentik: []
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Successfully started impersonation
|
||||||
|
'401':
|
||||||
|
description: Access denied
|
||||||
|
'400':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
description: ''
|
||||||
|
'403':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GenericError'
|
||||||
|
description: ''
|
||||||
/core/users/{id}/metrics/:
|
/core/users/{id}/metrics/:
|
||||||
get:
|
get:
|
||||||
operationId: core_users_metrics_retrieve
|
operationId: core_users_metrics_retrieve
|
||||||
@ -4962,6 +4994,29 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
description: ''
|
description: ''
|
||||||
|
/core/users/impersonate_end/:
|
||||||
|
get:
|
||||||
|
operationId: core_users_impersonate_end_retrieve
|
||||||
|
description: End Impersonation a user
|
||||||
|
tags:
|
||||||
|
- core
|
||||||
|
security:
|
||||||
|
- authentik: []
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Successfully started impersonation
|
||||||
|
'400':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
description: ''
|
||||||
|
'403':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GenericError'
|
||||||
|
description: ''
|
||||||
/core/users/me/:
|
/core/users/me/:
|
||||||
get:
|
get:
|
||||||
operationId: core_users_me_retrieve
|
operationId: core_users_me_retrieve
|
||||||
@ -40493,12 +40548,6 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
description: Get system information.
|
description: Get system information.
|
||||||
properties:
|
properties:
|
||||||
env:
|
|
||||||
type: object
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
description: Get Environment
|
|
||||||
readOnly: true
|
|
||||||
http_headers:
|
http_headers:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@ -40552,7 +40601,6 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- embedded_outpost_host
|
- embedded_outpost_host
|
||||||
- env
|
|
||||||
- http_headers
|
- http_headers
|
||||||
- http_host
|
- http_host
|
||||||
- http_is_secure
|
- http_is_secure
|
||||||
|
8
web/package-lock.json
generated
8
web/package-lock.json
generated
@ -17,7 +17,7 @@
|
|||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@formatjs/intl-listformat": "^7.2.2",
|
"@formatjs/intl-listformat": "^7.2.2",
|
||||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||||
"@goauthentik/api": "^2023.5.0-1684333401",
|
"@goauthentik/api": "^2023.5.3-1687462221",
|
||||||
"@lingui/cli": "^4.1.2",
|
"@lingui/cli": "^4.1.2",
|
||||||
"@lingui/core": "^4.1.2",
|
"@lingui/core": "^4.1.2",
|
||||||
"@lingui/detect-locale": "^4.1.2",
|
"@lingui/detect-locale": "^4.1.2",
|
||||||
@ -2127,9 +2127,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/api": {
|
"node_modules/@goauthentik/api": {
|
||||||
"version": "2023.5.0-1684333401",
|
"version": "2023.5.3-1687462221",
|
||||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.5.0-1684333401.tgz",
|
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.5.3-1687462221.tgz",
|
||||||
"integrity": "sha512-lbLXgqhvI65w3uodsJMdGIvFvJUzpleC0M4QneiXH3rTNbufVZWP9WbLCRLynKzEateOf3XSgjQ322BeZBA2bA=="
|
"integrity": "sha512-34LJCBVPOfdlIHhDPQEA7NS7mJvrKJKHSfa2HPQClyVM3o5us8Bp4yJKs2nm4hGime3rbZAwYYq7n75tccr7rQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@hcaptcha/types": {
|
"node_modules/@hcaptcha/types": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@formatjs/intl-listformat": "^7.2.2",
|
"@formatjs/intl-listformat": "^7.2.2",
|
||||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||||
"@goauthentik/api": "^2023.5.0-1684333401",
|
"@goauthentik/api": "^2023.5.3-1687462221",
|
||||||
"@lingui/cli": "^4.1.2",
|
"@lingui/cli": "^4.1.2",
|
||||||
"@lingui/core": "^4.1.2",
|
"@lingui/core": "^4.1.2",
|
||||||
"@lingui/detect-locale": "^4.1.2",
|
"@lingui/detect-locale": "^4.1.2",
|
||||||
|
@ -31,7 +31,7 @@ import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
|||||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import { AdminApi, SessionUser, Version } from "@goauthentik/api";
|
import { AdminApi, CoreApi, SessionUser, Version } from "@goauthentik/api";
|
||||||
|
|
||||||
autoDetectLanguage();
|
autoDetectLanguage();
|
||||||
|
|
||||||
@ -175,10 +175,11 @@ export class AdminInterface extends Interface {
|
|||||||
${this.user?.original
|
${this.user?.original
|
||||||
? html`<ak-sidebar-item
|
? html`<ak-sidebar-item
|
||||||
?highlight=${true}
|
?highlight=${true}
|
||||||
?isAbsoluteLink=${true}
|
@click=${() => {
|
||||||
path=${`/-/impersonation/end/?back=${encodeURIComponent(
|
new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
|
||||||
`${window.location.pathname}#${window.location.hash}`,
|
window.location.reload();
|
||||||
)}`}
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span slot="label"
|
<span slot="label"
|
||||||
>${t`You're currently impersonating ${this.user.user.username}. Click to stop.`}</span
|
>${t`You're currently impersonating ${this.user.user.username}. Click to stop.`}</span
|
||||||
|
@ -115,9 +115,8 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser">
|
||||||
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser">
|
|
||||||
<ak-search-select
|
<ak-search-select
|
||||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||||
const args: CoreUsersListRequest = {
|
const args: CoreUsersListRequest = {
|
||||||
@ -144,7 +143,6 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
|
|||||||
>
|
>
|
||||||
</ak-search-select>
|
</ak-search-select>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
${this.result ? this.renderResult() : html``}
|
${this.result ? this.renderResult() : html``}`;
|
||||||
</form>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,9 +21,12 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<ak-form-element-horizontal
|
||||||
<ak-form-element-horizontal label=${t`Common Name`} name="commonName" ?required=${true}>
|
label=${t`Common Name`}
|
||||||
|
name="commonName"
|
||||||
|
?required=${true}
|
||||||
|
>
|
||||||
<input type="text" class="pf-c-form-control" required />
|
<input type="text" class="pf-c-form-control" required />
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName">
|
<ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName">
|
||||||
@ -38,7 +41,6 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
|
|||||||
?required=${true}
|
?required=${true}
|
||||||
>
|
>
|
||||||
<input class="pf-c-form-control" type="number" value="365" />
|
<input class="pf-c-form-control" type="number" value="365" />
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>`;
|
||||||
</form>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,15 +87,13 @@ export class FlowImportForm extends Form<Flow> {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<ak-form-element-horizontal label=${t`Flow`} name="flow">
|
||||||
<ak-form-element-horizontal label=${t`Flow`} name="flow">
|
|
||||||
<input type="file" value="" class="pf-c-form-control" />
|
<input type="file" value="" class="pf-c-form-control" />
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`}
|
${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
${this.result ? this.renderResult() : html``}
|
${this.result ? this.renderResult() : html``}`;
|
||||||
</form>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,41 +46,39 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<ak-form-element-horizontal label=${t`Groups to add`} name="groups">
|
||||||
<ak-form-element-horizontal label=${t`Groups to add`} name="groups">
|
<div class="pf-c-input-group">
|
||||||
<div class="pf-c-input-group">
|
<ak-user-group-select-table
|
||||||
<ak-user-group-select-table
|
.confirm=${(items: Group[]) => {
|
||||||
.confirm=${(items: Group[]) => {
|
this.groupsToAdd = items;
|
||||||
this.groupsToAdd = items;
|
this.requestUpdate();
|
||||||
this.requestUpdate();
|
return Promise.resolve();
|
||||||
return Promise.resolve();
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
|
||||||
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
|
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
</button>
|
||||||
</button>
|
</ak-user-group-select-table>
|
||||||
</ak-user-group-select-table>
|
<div class="pf-c-form-control">
|
||||||
<div class="pf-c-form-control">
|
<ak-chip-group>
|
||||||
<ak-chip-group>
|
${this.groupsToAdd.map((group) => {
|
||||||
${this.groupsToAdd.map((group) => {
|
return html`<ak-chip
|
||||||
return html`<ak-chip
|
.removable=${true}
|
||||||
.removable=${true}
|
value=${ifDefined(group.pk)}
|
||||||
value=${ifDefined(group.pk)}
|
@remove=${() => {
|
||||||
@remove=${() => {
|
const idx = this.groupsToAdd.indexOf(group);
|
||||||
const idx = this.groupsToAdd.indexOf(group);
|
this.groupsToAdd.splice(idx, 1);
|
||||||
this.groupsToAdd.splice(idx, 1);
|
this.requestUpdate();
|
||||||
this.requestUpdate();
|
}}
|
||||||
}}
|
>
|
||||||
>
|
${group.name}
|
||||||
${group.name}
|
</ak-chip>`;
|
||||||
</ak-chip>`;
|
})}
|
||||||
})}
|
</ak-chip-group>
|
||||||
</ak-chip-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ak-form-element-horizontal>
|
</div>
|
||||||
</form> `;
|
</ak-form-element-horizontal>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,9 +116,8 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
|
||||||
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
|
|
||||||
<ak-search-select
|
<ak-search-select
|
||||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||||
const args: CoreUsersListRequest = {
|
const args: CoreUsersListRequest = {
|
||||||
@ -155,7 +154,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
|
|||||||
${t`Set custom attributes using YAML or JSON.`}
|
${t`Set custom attributes using YAML or JSON.`}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
${this.result ? this.renderResult() : html``}
|
${this.result ? this.renderResult() : html``}`;
|
||||||
</form>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,9 +119,8 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
|
||||||
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
|
|
||||||
<ak-search-select
|
<ak-search-select
|
||||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||||
const args: CoreUsersListRequest = {
|
const args: CoreUsersListRequest = {
|
||||||
@ -156,7 +155,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
|
|||||||
</ak-codemirror>
|
</ak-codemirror>
|
||||||
<p class="pf-c-form__helper-text">${this.renderExampleButtons()}</p>
|
<p class="pf-c-form__helper-text">${this.renderExampleButtons()}</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
${this.result ? this.renderResult() : html``}
|
${this.result ? this.renderResult() : html``}`;
|
||||||
</form>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,9 +37,8 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
|
||||||
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
|
|
||||||
<input type="text" class="pf-c-form-control" required />
|
<input type="text" class="pf-c-form-control" required />
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
@ -77,7 +76,6 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
|||||||
|
|
||||||
<ak-form-element-horizontal label=${t`Metadata`} name="metadata">
|
<ak-form-element-horizontal label=${t`Metadata`} name="metadata">
|
||||||
<input type="file" value="" class="pf-c-form-control" />
|
<input type="file" value="" class="pf-c-form-control" />
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>`;
|
||||||
</form>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,9 +59,8 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`${this.group?.isSuperuser ? html`` : html``}
|
||||||
${this.group?.isSuperuser ? html`` : html``}
|
|
||||||
<ak-form-element-horizontal label=${t`Users to add`} name="users">
|
<ak-form-element-horizontal label=${t`Users to add`} name="users">
|
||||||
<div class="pf-c-input-group">
|
<div class="pf-c-input-group">
|
||||||
<ak-group-member-select-table
|
<ak-group-member-select-table
|
||||||
@ -93,8 +92,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
|
|||||||
</ak-chip-group>
|
</ak-chip-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>`;
|
||||||
</form> `;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,12 +191,20 @@ export class RelatedUserList extends Table<User> {
|
|||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
|
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
|
||||||
? html`
|
? html`
|
||||||
<a
|
<ak-action-button
|
||||||
class="pf-c-button pf-m-tertiary"
|
class="pf-m-tertiary"
|
||||||
href="${`/-/impersonation/${item.pk}/`}"
|
.apiRequest=${() => {
|
||||||
|
return new CoreApi(DEFAULT_CONFIG)
|
||||||
|
.coreUsersImpersonateCreate({
|
||||||
|
id: item.pk,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
window.location.href = "/";
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
${t`Impersonate`}
|
${t`Impersonate`}
|
||||||
</a>
|
</ak-action-button>
|
||||||
`
|
`
|
||||||
: html``}`,
|
: html``}`,
|
||||||
];
|
];
|
||||||
|
@ -35,9 +35,8 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
|
|||||||
this.result = undefined;
|
this.result = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRequestForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name">
|
||||||
<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name">
|
|
||||||
<input type="text" value="" class="pf-c-form-control" required />
|
<input type="text" value="" class="pf-c-form-control" required />
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${t`User's primary identifier. 150 characters or fewer.`}
|
${t`User's primary identifier. 150 characters or fewer.`}
|
||||||
@ -78,8 +77,7 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
|
|||||||
value="${dateTimeLocal(new Date(Date.now() + 1000 * 60 ** 2 * 24 * 360))}"
|
value="${dateTimeLocal(new Date(Date.now() + 1000 * 60 ** 2 * 24 * 360))}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>`;
|
||||||
</form>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderResponseForm(): TemplateResult {
|
renderResponseForm(): TemplateResult {
|
||||||
@ -113,6 +111,6 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
|
|||||||
if (this.result) {
|
if (this.result) {
|
||||||
return this.renderResponseForm();
|
return this.renderResponseForm();
|
||||||
}
|
}
|
||||||
return this.renderRequestForm();
|
return super.renderForm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,12 +196,20 @@ export class UserListPage extends TablePage<User> {
|
|||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
|
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
|
||||||
? html`
|
? html`
|
||||||
<a
|
<ak-action-button
|
||||||
class="pf-c-button pf-m-tertiary"
|
class="pf-m-tertiary"
|
||||||
href="${`/-/impersonation/${item.pk}/`}"
|
.apiRequest=${() => {
|
||||||
|
return new CoreApi(DEFAULT_CONFIG)
|
||||||
|
.coreUsersImpersonateCreate({
|
||||||
|
id: item.pk,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
window.location.href = "/";
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
${t`Impersonate`}
|
${t`Impersonate`}
|
||||||
</a>
|
</ak-action-button>
|
||||||
`
|
`
|
||||||
: html``}`,
|
: html``}`,
|
||||||
];
|
];
|
||||||
|
@ -26,11 +26,13 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<ak-form-element-horizontal
|
||||||
<ak-form-element-horizontal label=${t`Password`} ?required=${true} name="password">
|
label=${t`Password`}
|
||||||
<input type="password" value="" class="pf-c-form-control" required />
|
?required=${true}
|
||||||
</ak-form-element-horizontal>
|
name="password"
|
||||||
</form>`;
|
>
|
||||||
|
<input type="password" value="" class="pf-c-form-control" required />
|
||||||
|
</ak-form-element-horizontal>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,32 +32,34 @@ export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailRetrieveReque
|
|||||||
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailRetrieve(data);
|
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailRetrieve(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderInlineForm(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<ak-form-element-horizontal
|
||||||
<ak-form-element-horizontal label=${t`Email stage`} ?required=${true} name="emailStage">
|
label=${t`Email stage`}
|
||||||
<ak-search-select
|
?required=${true}
|
||||||
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
|
name="emailStage"
|
||||||
const args: StagesAllListRequest = {
|
>
|
||||||
ordering: "name",
|
<ak-search-select
|
||||||
};
|
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
|
||||||
if (query !== undefined) {
|
const args: StagesAllListRequest = {
|
||||||
args.search = query;
|
ordering: "name",
|
||||||
}
|
};
|
||||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
|
if (query !== undefined) {
|
||||||
return stages.results;
|
args.search = query;
|
||||||
}}
|
}
|
||||||
.groupBy=${(items: Stage[]) => {
|
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
|
||||||
return groupBy(items, (stage) => stage.verboseNamePlural);
|
return stages.results;
|
||||||
}}
|
}}
|
||||||
.renderElement=${(stage: Stage): string => {
|
.groupBy=${(items: Stage[]) => {
|
||||||
return stage.name;
|
return groupBy(items, (stage) => stage.verboseNamePlural);
|
||||||
}}
|
}}
|
||||||
.value=${(stage: Stage | undefined): string | undefined => {
|
.renderElement=${(stage: Stage): string => {
|
||||||
return stage?.pk;
|
return stage.name;
|
||||||
}}
|
}}
|
||||||
>
|
.value=${(stage: Stage | undefined): string | undefined => {
|
||||||
</ak-search-select>
|
return stage?.pk;
|
||||||
</ak-form-element-horizontal>
|
}}
|
||||||
</form>`;
|
>
|
||||||
|
</ak-search-select>
|
||||||
|
</ak-form-element-horizontal>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,12 +201,20 @@ export class UserViewPage extends AKElement {
|
|||||||
)
|
)
|
||||||
? html`
|
? html`
|
||||||
<div class="pf-c-card__footer">
|
<div class="pf-c-card__footer">
|
||||||
<a
|
<ak-action-button
|
||||||
class="pf-c-button pf-m-tertiary"
|
class="pf-m-tertiary"
|
||||||
href="${`/-/impersonation/${this.user?.pk}/`}"
|
.apiRequest=${() => {
|
||||||
|
return new CoreApi(DEFAULT_CONFIG)
|
||||||
|
.coreUsersImpersonateCreate({
|
||||||
|
id: this.user?.pk || 0,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
window.location.href = "/";
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
${t`Impersonate`}
|
${t`Impersonate`}
|
||||||
</a>
|
</ak-action-button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: html``}
|
: html``}
|
||||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||||||
export const ERROR_CLASS = "pf-m-danger";
|
export const ERROR_CLASS = "pf-m-danger";
|
||||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||||
export const CURRENT_CLASS = "pf-m-current";
|
export const CURRENT_CLASS = "pf-m-current";
|
||||||
export const VERSION = "2023.5.3";
|
export const VERSION = "2023.5.4";
|
||||||
export const TITLE_DEFAULT = "authentik";
|
export const TITLE_DEFAULT = "authentik";
|
||||||
export const ROUTE_SEPARATOR = ";";
|
export const ROUTE_SEPARATOR = ";";
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ export class Diagram extends AKElement {
|
|||||||
flowchart: {
|
flowchart: {
|
||||||
curve: "linear",
|
curve: "linear",
|
||||||
},
|
},
|
||||||
|
htmlLabels: false,
|
||||||
};
|
};
|
||||||
mermaid.initialize(this.config);
|
mermaid.initialize(this.config);
|
||||||
}
|
}
|
||||||
|
@ -283,9 +283,23 @@ export abstract class Form<T> extends AKElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderForm(): TemplateResult {
|
||||||
|
const inline = this.renderInlineForm();
|
||||||
|
if (inline) {
|
||||||
|
return html`<form class="pf-c-form pf-m-horizontal" @submit=${this.submit}>
|
||||||
|
${inline}
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
return html`<slot></slot>`;
|
return html`<slot></slot>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline form render callback when inheriting this class, should be overwritten
|
||||||
|
* instead of `this.renderForm`
|
||||||
|
*/
|
||||||
|
renderInlineForm(): TemplateResult | undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
renderNonFieldErrors(): TemplateResult {
|
renderNonFieldErrors(): TemplateResult {
|
||||||
if (!this.nonFieldErrors) {
|
if (!this.nonFieldErrors) {
|
||||||
return html``;
|
return html``;
|
||||||
|
@ -11,6 +11,7 @@ import { me } from "@goauthentik/common/users";
|
|||||||
import { first } from "@goauthentik/common/utils";
|
import { first } from "@goauthentik/common/utils";
|
||||||
import { WebsocketClient } from "@goauthentik/common/ws";
|
import { WebsocketClient } from "@goauthentik/common/ws";
|
||||||
import { Interface } from "@goauthentik/elements/Base";
|
import { Interface } from "@goauthentik/elements/Base";
|
||||||
|
import "@goauthentik/elements/buttons/ActionButton";
|
||||||
import "@goauthentik/elements/messages/MessageContainer";
|
import "@goauthentik/elements/messages/MessageContainer";
|
||||||
import "@goauthentik/elements/notifications/APIDrawer";
|
import "@goauthentik/elements/notifications/APIDrawer";
|
||||||
import "@goauthentik/elements/notifications/NotificationDrawer";
|
import "@goauthentik/elements/notifications/NotificationDrawer";
|
||||||
@ -36,7 +37,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
|||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
|
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
|
||||||
|
|
||||||
import { EventsApi, SessionUser } from "@goauthentik/api";
|
import { CoreApi, EventsApi, SessionUser } from "@goauthentik/api";
|
||||||
|
|
||||||
autoDetectLanguage();
|
autoDetectLanguage();
|
||||||
|
|
||||||
@ -234,18 +235,23 @@ export class UserInterface extends Interface {
|
|||||||
: html``}
|
: html``}
|
||||||
</div>
|
</div>
|
||||||
${this.me.original
|
${this.me.original
|
||||||
? html`<div class="pf-c-page__header-tools">
|
? html`
|
||||||
<div class="pf-c-page__header-tools-group">
|
<div class="pf-c-page__header-tools">
|
||||||
<a
|
<div class="pf-c-page__header-tools-group">
|
||||||
class="pf-c-button pf-m-warning pf-m-small"
|
<ak-action-button
|
||||||
href=${`/-/impersonation/end/?back=${encodeURIComponent(
|
class="pf-m-warning pf-m-small"
|
||||||
`${window.location.pathname}#${window.location.hash}`,
|
.apiRequest=${() => {
|
||||||
)}`}
|
return new CoreApi(DEFAULT_CONFIG)
|
||||||
>
|
.coreUsersImpersonateEndRetrieve()
|
||||||
${t`Stop impersonation`}
|
.then(() => {
|
||||||
</a>
|
window.location.reload();
|
||||||
</div>
|
});
|
||||||
</div>`
|
}}
|
||||||
|
>
|
||||||
|
${t`Stop impersonation`}
|
||||||
|
</ak-action-button>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
: html``}
|
: html``}
|
||||||
<div class="pf-c-page__header-tools-group">
|
<div class="pf-c-page__header-tools-group">
|
||||||
<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md">
|
<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md">
|
||||||
|
Reference in New Issue
Block a user