commit
f2d5d62c9c
|
@ -1,5 +0,0 @@
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,5 +1,3 @@
|
||||||
{% extends container_template|default:"administration/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load authentik_utils %}
|
{% load authentik_utils %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
"""admin tests"""
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.test.client import RequestFactory
|
|
||||||
|
|
||||||
from authentik.admin.views.policies_bindings import PolicyBindingCreateView
|
|
||||||
from authentik.core.models import Application
|
|
||||||
from authentik.policies.forms import PolicyBindingForm
|
|
||||||
|
|
||||||
|
|
||||||
class TestPolicyBindingView(TestCase):
|
|
||||||
"""Generic admin tests"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
|
|
||||||
def test_without_get_param(self):
|
|
||||||
"""Test PolicyBindingCreateView without get params"""
|
|
||||||
request = self.factory.get("/")
|
|
||||||
view = PolicyBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {})
|
|
||||||
|
|
||||||
def test_with_params_invalid(self):
|
|
||||||
"""Test PolicyBindingCreateView with invalid get params"""
|
|
||||||
request = self.factory.get("/", {"target": uuid4()})
|
|
||||||
view = PolicyBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {})
|
|
||||||
|
|
||||||
def test_with_params(self):
|
|
||||||
"""Test PolicyBindingCreateView with get params"""
|
|
||||||
target = Application.objects.create(name="test")
|
|
||||||
request = self.factory.get("/", {"target": target.pk.hex})
|
|
||||||
view = PolicyBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
|
|
||||||
|
|
||||||
self.assertTrue(
|
|
||||||
isinstance(
|
|
||||||
PolicyBindingForm(initial={"target": "foo"}).fields["target"].widget,
|
|
||||||
forms.HiddenInput,
|
|
||||||
)
|
|
||||||
)
|
|
|
@ -1,43 +0,0 @@
|
||||||
"""admin tests"""
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.test.client import RequestFactory
|
|
||||||
|
|
||||||
from authentik.admin.views.stages_bindings import StageBindingCreateView
|
|
||||||
from authentik.flows.forms import FlowStageBindingForm
|
|
||||||
from authentik.flows.models import Flow
|
|
||||||
|
|
||||||
|
|
||||||
class TestStageBindingView(TestCase):
|
|
||||||
"""Generic admin tests"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
|
|
||||||
def test_without_get_param(self):
|
|
||||||
"""Test StageBindingCreateView without get params"""
|
|
||||||
request = self.factory.get("/")
|
|
||||||
view = StageBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {})
|
|
||||||
|
|
||||||
def test_with_params_invalid(self):
|
|
||||||
"""Test StageBindingCreateView with invalid get params"""
|
|
||||||
request = self.factory.get("/", {"target": uuid4()})
|
|
||||||
view = StageBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {})
|
|
||||||
|
|
||||||
def test_with_params(self):
|
|
||||||
"""Test StageBindingCreateView with get params"""
|
|
||||||
target = Flow.objects.create(name="test", slug="test")
|
|
||||||
request = self.factory.get("/", {"target": target.pk.hex})
|
|
||||||
view = StageBindingCreateView(request=request)
|
|
||||||
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
|
|
||||||
|
|
||||||
self.assertTrue(
|
|
||||||
isinstance(
|
|
||||||
FlowStageBindingForm(initial={"target": "foo"}).fields["target"].widget,
|
|
||||||
forms.HiddenInput,
|
|
||||||
)
|
|
||||||
)
|
|
|
@ -4,14 +4,10 @@ from django.urls import path
|
||||||
from authentik.admin.views import (
|
from authentik.admin.views import (
|
||||||
outposts_service_connections,
|
outposts_service_connections,
|
||||||
policies,
|
policies,
|
||||||
policies_bindings,
|
|
||||||
property_mappings,
|
property_mappings,
|
||||||
providers,
|
providers,
|
||||||
sources,
|
sources,
|
||||||
stages,
|
stages,
|
||||||
stages_bindings,
|
|
||||||
stages_invitations,
|
|
||||||
stages_prompts,
|
|
||||||
)
|
)
|
||||||
from authentik.providers.saml.views.metadata import MetadataImportView
|
from authentik.providers.saml.views.metadata import MetadataImportView
|
||||||
|
|
||||||
|
@ -30,17 +26,6 @@ urlpatterns = [
|
||||||
policies.PolicyUpdateView.as_view(),
|
policies.PolicyUpdateView.as_view(),
|
||||||
name="policy-update",
|
name="policy-update",
|
||||||
),
|
),
|
||||||
# Policy bindings
|
|
||||||
path(
|
|
||||||
"policies/bindings/create/",
|
|
||||||
policies_bindings.PolicyBindingCreateView.as_view(),
|
|
||||||
name="policy-binding-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"policies/bindings/<uuid:pk>/update/",
|
|
||||||
policies_bindings.PolicyBindingUpdateView.as_view(),
|
|
||||||
name="policy-binding-update",
|
|
||||||
),
|
|
||||||
# Providers
|
# Providers
|
||||||
path(
|
path(
|
||||||
"providers/create/",
|
"providers/create/",
|
||||||
|
@ -64,34 +49,6 @@ urlpatterns = [
|
||||||
stages.StageUpdateView.as_view(),
|
stages.StageUpdateView.as_view(),
|
||||||
name="stage-update",
|
name="stage-update",
|
||||||
),
|
),
|
||||||
# Stage bindings
|
|
||||||
path(
|
|
||||||
"stages/bindings/create/",
|
|
||||||
stages_bindings.StageBindingCreateView.as_view(),
|
|
||||||
name="stage-binding-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"stages/bindings/<uuid:pk>/update/",
|
|
||||||
stages_bindings.StageBindingUpdateView.as_view(),
|
|
||||||
name="stage-binding-update",
|
|
||||||
),
|
|
||||||
# Stage Prompts
|
|
||||||
path(
|
|
||||||
"stages_prompts/create/",
|
|
||||||
stages_prompts.PromptCreateView.as_view(),
|
|
||||||
name="stage-prompt-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"stages_prompts/<uuid:pk>/update/",
|
|
||||||
stages_prompts.PromptUpdateView.as_view(),
|
|
||||||
name="stage-prompt-update",
|
|
||||||
),
|
|
||||||
# Stage Invitations
|
|
||||||
path(
|
|
||||||
"stages/invitations/create/",
|
|
||||||
stages_invitations.InvitationCreateView.as_view(),
|
|
||||||
name="stage-invitation-create",
|
|
||||||
),
|
|
||||||
# Property Mappings
|
# Property Mappings
|
||||||
path(
|
path(
|
||||||
"property-mappings/create/",
|
"property-mappings/create/",
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
"""authentik PolicyBinding administration"""
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.db.models import Max
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
from authentik.policies.forms import PolicyBindingForm
|
|
||||||
from authentik.policies.models import PolicyBinding, PolicyBindingModel
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new PolicyBinding"""
|
|
||||||
|
|
||||||
model = PolicyBinding
|
|
||||||
permission_required = "authentik_policies.add_policybinding"
|
|
||||||
form_class = PolicyBindingForm
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully created PolicyBinding")
|
|
||||||
|
|
||||||
def get_initial(self) -> dict[str, Any]:
|
|
||||||
if "target" in self.request.GET:
|
|
||||||
initial_target_pk = self.request.GET["target"]
|
|
||||||
targets = PolicyBindingModel.objects.filter(
|
|
||||||
pk=initial_target_pk
|
|
||||||
).select_subclasses()
|
|
||||||
if not targets.exists():
|
|
||||||
return {}
|
|
||||||
max_order = PolicyBinding.objects.filter(target=targets.first()).aggregate(
|
|
||||||
Max("order")
|
|
||||||
)["order__max"]
|
|
||||||
if not isinstance(max_order, int):
|
|
||||||
max_order = -1
|
|
||||||
return {"target": targets.first(), "order": max_order + 1}
|
|
||||||
return super().get_initial()
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update policybinding"""
|
|
||||||
|
|
||||||
model = PolicyBinding
|
|
||||||
permission_required = "authentik_policies.change_policybinding"
|
|
||||||
form_class = PolicyBindingForm
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully updated PolicyBinding")
|
|
|
@ -1,65 +0,0 @@
|
||||||
"""authentik StageBinding administration"""
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.db.models import Max
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.flows.forms import FlowStageBindingForm
|
|
||||||
from authentik.flows.models import Flow, FlowStageBinding
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class StageBindingCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new StageBinding"""
|
|
||||||
|
|
||||||
model = FlowStageBinding
|
|
||||||
permission_required = "authentik_flows.add_flowstagebinding"
|
|
||||||
form_class = FlowStageBindingForm
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully created StageBinding")
|
|
||||||
|
|
||||||
def get_initial(self) -> dict[str, Any]:
|
|
||||||
if "target" in self.request.GET:
|
|
||||||
initial_target_pk = self.request.GET["target"]
|
|
||||||
targets = Flow.objects.filter(pk=initial_target_pk).select_subclasses()
|
|
||||||
if not targets.exists():
|
|
||||||
return {}
|
|
||||||
max_order = FlowStageBinding.objects.filter(
|
|
||||||
target=targets.first()
|
|
||||||
).aggregate(Max("order"))["order__max"]
|
|
||||||
if not isinstance(max_order, int):
|
|
||||||
max_order = -1
|
|
||||||
return {"target": targets.first(), "order": max_order + 1}
|
|
||||||
return super().get_initial()
|
|
||||||
|
|
||||||
|
|
||||||
class StageBindingUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update FlowStageBinding"""
|
|
||||||
|
|
||||||
model = FlowStageBinding
|
|
||||||
permission_required = "authentik_flows.change_flowstagebinding"
|
|
||||||
form_class = FlowStageBindingForm
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully updated StageBinding")
|
|
|
@ -1,36 +0,0 @@
|
||||||
"""authentik Invitation administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
from authentik.stages.invitation.forms import InvitationForm
|
|
||||||
from authentik.stages.invitation.models import Invitation
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Invitation"""
|
|
||||||
|
|
||||||
model = Invitation
|
|
||||||
form_class = InvitationForm
|
|
||||||
permission_required = "authentik_stages_invitation.add_invitation"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully created Invitation")
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
obj = form.save(commit=False)
|
|
||||||
obj.created_by = self.request.user
|
|
||||||
obj.save()
|
|
||||||
return HttpResponseRedirect(self.success_url)
|
|
|
@ -1,48 +0,0 @@
|
||||||
"""authentik Prompt administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
from authentik.stages.prompt.forms import PromptAdminForm
|
|
||||||
from authentik.stages.prompt.models import Prompt
|
|
||||||
|
|
||||||
|
|
||||||
class PromptCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Prompt"""
|
|
||||||
|
|
||||||
model = Prompt
|
|
||||||
form_class = PromptAdminForm
|
|
||||||
permission_required = "authentik_stages_prompt.add_prompt"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully created Prompt")
|
|
||||||
|
|
||||||
|
|
||||||
class PromptUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update prompt"""
|
|
||||||
|
|
||||||
model = Prompt
|
|
||||||
form_class = PromptAdminForm
|
|
||||||
permission_required = "authentik_stages_prompt.change_prompt"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully updated Prompt")
|
|
|
@ -18,7 +18,7 @@ from authentik.events.models import Event, EventAction
|
||||||
class TokenSerializer(ModelSerializer):
|
class TokenSerializer(ModelSerializer):
|
||||||
"""Token Serializer"""
|
"""Token Serializer"""
|
||||||
|
|
||||||
user = UserSerializer()
|
user = UserSerializer(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
|
@ -61,6 +61,9 @@ class TokenViewSet(ModelViewSet):
|
||||||
]
|
]
|
||||||
ordering = ["expires"]
|
ordering = ["expires"]
|
||||||
|
|
||||||
|
def perform_create(self, serializer: TokenSerializer):
|
||||||
|
serializer.save(user=self.request.user)
|
||||||
|
|
||||||
@permission_required("authentik_core.view_token_key")
|
@permission_required("authentik_core.view_token_key")
|
||||||
@swagger_auto_schema(responses={200: TokenViewSerializer(many=False)})
|
@swagger_auto_schema(responses={200: TokenViewSerializer(many=False)})
|
||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
"""Core user token form"""
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from authentik.core.models import Token
|
|
||||||
|
|
||||||
|
|
||||||
class UserTokenForm(forms.ModelForm):
|
|
||||||
"""Token form, for tokens created by endusers"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = Token
|
|
||||||
fields = [
|
|
||||||
"identifier",
|
|
||||||
"expires",
|
|
||||||
"expiring",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"identifier": forms.TextInput(),
|
|
||||||
"description": forms.TextInput(),
|
|
||||||
}
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""Test token API"""
|
||||||
|
from django.urls.base import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from authentik.core.models import Token, User
|
||||||
|
|
||||||
|
|
||||||
|
class TestTokenAPI(APITestCase):
|
||||||
|
"""Test token API"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
self.user = User.objects.get(username="akadmin")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_token_create(self):
|
||||||
|
"""Test token creation endpoint"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:token-list"), {"identifier": "test-token"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(Token.objects.get(identifier="test-token").user, self.user)
|
|
@ -5,7 +5,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
from authentik.core.views import impersonate, user
|
from authentik.core.views import impersonate
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
|
@ -13,17 +13,6 @@ urlpatterns = [
|
||||||
login_required(RedirectView.as_view(pattern_name="authentik_core:if-admin")),
|
login_required(RedirectView.as_view(pattern_name="authentik_core:if-admin")),
|
||||||
name="root-redirect",
|
name="root-redirect",
|
||||||
),
|
),
|
||||||
# User views
|
|
||||||
path(
|
|
||||||
"-/user/tokens/create/",
|
|
||||||
user.TokenCreateView.as_view(),
|
|
||||||
name="user-tokens-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"-/user/tokens/<slug:identifier>/update/",
|
|
||||||
user.TokenUpdateView.as_view(),
|
|
||||||
name="user-tokens-update",
|
|
||||||
),
|
|
||||||
# Impersonation
|
# Impersonation
|
||||||
path(
|
path(
|
||||||
"-/impersonation/<int:user_id>/",
|
"-/impersonation/<int:user_id>/",
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
"""authentik core user views"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http.response import HttpResponse
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
from guardian.shortcuts import get_objects_for_user
|
|
||||||
|
|
||||||
from authentik.core.forms.token import UserTokenForm
|
|
||||||
from authentik.core.models import Token, TokenIntents
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class TokenCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Token"""
|
|
||||||
|
|
||||||
model = Token
|
|
||||||
form_class = UserTokenForm
|
|
||||||
permission_required = "authentik_core.add_token"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = "/"
|
|
||||||
success_message = _("Successfully created Token")
|
|
||||||
|
|
||||||
def form_valid(self, form: UserTokenForm) -> HttpResponse:
|
|
||||||
form.instance.user = self.request.user
|
|
||||||
form.instance.intent = TokenIntents.INTENT_API
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class TokenUpdateView(
|
|
||||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
|
||||||
):
|
|
||||||
"""Update token"""
|
|
||||||
|
|
||||||
model = Token
|
|
||||||
form_class = UserTokenForm
|
|
||||||
permission_required = "authentik_core.change_token"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = "/"
|
|
||||||
success_message = _("Successfully updated Token")
|
|
||||||
|
|
||||||
def get_object(self) -> Token:
|
|
||||||
identifier = self.kwargs.get("identifier")
|
|
||||||
return (
|
|
||||||
get_objects_for_user(
|
|
||||||
self.request.user, self.permission_required, self.model
|
|
||||||
)
|
|
||||||
.filter(intent=TokenIntents.INTENT_API, identifier=identifier)
|
|
||||||
.first()
|
|
||||||
)
|
|
|
@ -1,34 +0,0 @@
|
||||||
"""Flow and Stage forms"""
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from authentik.flows.models import FlowStageBinding, Stage
|
|
||||||
from authentik.lib.widgets import GroupedModelChoiceField
|
|
||||||
|
|
||||||
|
|
||||||
class FlowStageBindingForm(forms.ModelForm):
|
|
||||||
"""FlowStageBinding Form"""
|
|
||||||
|
|
||||||
stage = GroupedModelChoiceField(
|
|
||||||
queryset=Stage.objects.all().order_by("name").select_subclasses(),
|
|
||||||
to_field_name="stage_uuid",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
if "target" in self.initial:
|
|
||||||
self.fields["target"].widget = forms.HiddenInput()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = FlowStageBinding
|
|
||||||
fields = [
|
|
||||||
"target",
|
|
||||||
"stage",
|
|
||||||
"evaluate_on_plan",
|
|
||||||
"re_evaluate_policies",
|
|
||||||
"order",
|
|
||||||
"policy_engine_mode",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
}
|
|
|
@ -29,6 +29,9 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
||||||
for to_remove_name in to_remove:
|
for to_remove_name in to_remove:
|
||||||
if to_remove_name in data:
|
if to_remove_name in data:
|
||||||
data.pop(to_remove_name)
|
data.pop(to_remove_name)
|
||||||
|
for key in list(data.keys()):
|
||||||
|
if key.endswith("_obj"):
|
||||||
|
data.pop(key)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
"""policy binding API Views"""
|
"""policy binding API Views"""
|
||||||
|
from typing import OrderedDict
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField
|
from rest_framework.serializers import (
|
||||||
|
ModelSerializer,
|
||||||
|
PrimaryKeyRelatedField,
|
||||||
|
ValidationError,
|
||||||
|
)
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.api.groups import GroupSerializer
|
from authentik.core.api.groups import GroupSerializer
|
||||||
|
from authentik.core.api.users import UserSerializer
|
||||||
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.models import PolicyBinding, PolicyBindingModel
|
from authentik.policies.models import PolicyBinding, PolicyBindingModel
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
@ -26,8 +34,8 @@ class PolicyBindingModelForeignKey(PrimaryKeyRelatedField):
|
||||||
# won't return anything. This is because the direct lookup
|
# won't return anything. This is because the direct lookup
|
||||||
# checks the PK of PolicyBindingModel (for example),
|
# checks the PK of PolicyBindingModel (for example),
|
||||||
# but we get given the Primary Key of the inheriting class
|
# but we get given the Primary Key of the inheriting class
|
||||||
for model in self.get_queryset().select_subclasses().all().select_related():
|
for model in self.get_queryset().select_subclasses().all():
|
||||||
if model.pk == data:
|
if str(model.pk) == str(data):
|
||||||
return model
|
return model
|
||||||
# as a fallback we still try a direct lookup
|
# as a fallback we still try a direct lookup
|
||||||
return self.get_queryset().get_subclass(pk=data)
|
return self.get_queryset().get_subclass(pk=data)
|
||||||
|
@ -51,7 +59,9 @@ class PolicyBindingSerializer(ModelSerializer):
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
group = GroupSerializer(required=False)
|
policy_obj = PolicySerializer(required=False, read_only=True, source="policy")
|
||||||
|
group_obj = GroupSerializer(required=False, read_only=True, source="group")
|
||||||
|
user_obj = UserSerializer(required=False, read_only=True, source="user")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
|
@ -61,12 +71,31 @@ class PolicyBindingSerializer(ModelSerializer):
|
||||||
"policy",
|
"policy",
|
||||||
"group",
|
"group",
|
||||||
"user",
|
"user",
|
||||||
|
"policy_obj",
|
||||||
|
"group_obj",
|
||||||
|
"user_obj",
|
||||||
"target",
|
"target",
|
||||||
"enabled",
|
"enabled",
|
||||||
"order",
|
"order",
|
||||||
"timeout",
|
"timeout",
|
||||||
]
|
]
|
||||||
depth = 2
|
|
||||||
|
def validate(self, data: OrderedDict) -> OrderedDict:
|
||||||
|
"""Check that either policy, group or user is set."""
|
||||||
|
count = sum(
|
||||||
|
[
|
||||||
|
bool(data.get("policy", None)),
|
||||||
|
bool(data.get("group", None)),
|
||||||
|
bool(data.get("user", None)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
invalid = count > 1
|
||||||
|
empty = count < 1
|
||||||
|
if invalid:
|
||||||
|
raise ValidationError("Only one of 'policy', 'group' or 'user' can be set.")
|
||||||
|
if empty:
|
||||||
|
raise ValidationError("One of 'policy', 'group' or 'user' must be set.")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingViewSet(ModelViewSet):
|
class PolicyBindingViewSet(ModelViewSet):
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
"""Test bindings API"""
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from authentik.core.models import Group, User
|
||||||
|
from authentik.policies.models import PolicyBindingModel
|
||||||
|
|
||||||
|
|
||||||
|
class TestBindingsAPI(APITestCase):
|
||||||
|
"""Test bindings API"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
self.pbm = PolicyBindingModel.objects.create()
|
||||||
|
self.group = Group.objects.first()
|
||||||
|
self.user = User.objects.get(username="akadmin")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_valid_binding(self):
|
||||||
|
"""Test valid binding"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:policybinding-list"),
|
||||||
|
data={"target": self.pbm.pk, "user": self.user.pk, "order": 0},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
|
def test_invalid_too_much(self):
|
||||||
|
"""Test invalid binding (too much)"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:policybinding-list"),
|
||||||
|
data={
|
||||||
|
"target": self.pbm.pk,
|
||||||
|
"user": self.user.pk,
|
||||||
|
"group": self.group.pk,
|
||||||
|
"order": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
response.content.decode(),
|
||||||
|
{
|
||||||
|
"non_field_errors": [
|
||||||
|
"Only one of 'policy', 'group' or 'user' can be set."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_invalid_too_little(self):
|
||||||
|
"""Test invvalid binding (too little)"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:policybinding-list"),
|
||||||
|
data={"target": self.pbm.pk, "order": 0},
|
||||||
|
)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
response.content.decode(),
|
||||||
|
{"non_field_errors": ["One of 'policy', 'group' or 'user' must be set."]},
|
||||||
|
)
|
|
@ -49,5 +49,4 @@ class InvitationViewSet(ModelViewSet):
|
||||||
filterset_fields = ["created_by__username", "expires"]
|
filterset_fields = ["created_by__username", "expires"]
|
||||||
|
|
||||||
def perform_create(self, serializer: InvitationSerializer):
|
def perform_create(self, serializer: InvitationSerializer):
|
||||||
serializer.instance.created_by = self.request.user
|
serializer.save(created_by=self.request.user)
|
||||||
return super().perform_create(serializer)
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
"""authentik flows invitation forms"""
|
"""authentik flows invitation forms"""
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from authentik.admin.fields import CodeMirrorWidget, YAMLField
|
from authentik.stages.invitation.models import InvitationStage
|
||||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationStageForm(forms.ModelForm):
|
class InvitationStageForm(forms.ModelForm):
|
||||||
|
@ -15,14 +14,3 @@ class InvitationStageForm(forms.ModelForm):
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"name": forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class InvitationForm(forms.ModelForm):
|
|
||||||
"""InvitationForm"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = Invitation
|
|
||||||
fields = ["expires", "fixed_data"]
|
|
||||||
widgets = {"fixed_data": CodeMirrorWidget()}
|
|
||||||
field_classes = {"fixed_data": YAMLField}
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.challenge import ChallengeTypes
|
from authentik.flows.challenge import ChallengeTypes
|
||||||
|
@ -134,3 +135,20 @@ class TestUserLoginStage(TestCase):
|
||||||
force_str(response.content),
|
force_str(response.content),
|
||||||
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
|
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInvitationsAPI(APITestCase):
|
||||||
|
"""Test Invitations API"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
self.user = User.objects.get(username="akadmin")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_invite_create(self):
|
||||||
|
"""Test Invitations creation endpoint"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:invitation-list"), {"identifier": "test-token"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(Invitation.objects.first().created_by, self.user)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Prompt forms"""
|
"""Prompt forms"""
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from authentik.stages.prompt.models import Prompt, PromptStage
|
from authentik.stages.prompt.models import PromptStage
|
||||||
|
|
||||||
|
|
||||||
class PromptStageForm(forms.ModelForm):
|
class PromptStageForm(forms.ModelForm):
|
||||||
|
@ -14,23 +14,3 @@ class PromptStageForm(forms.ModelForm):
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"name": forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PromptAdminForm(forms.ModelForm):
|
|
||||||
"""Form to edit Prompt instances for admins"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = Prompt
|
|
||||||
fields = [
|
|
||||||
"field_key",
|
|
||||||
"label",
|
|
||||||
"type",
|
|
||||||
"required",
|
|
||||||
"placeholder",
|
|
||||||
"order",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"label": forms.TextInput(),
|
|
||||||
"placeholder": forms.TextInput(),
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ from rest_framework.fields import (
|
||||||
DateField,
|
DateField,
|
||||||
DateTimeField,
|
DateTimeField,
|
||||||
EmailField,
|
EmailField,
|
||||||
|
HiddenField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
)
|
)
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
|
@ -89,10 +90,10 @@ class Prompt(SerializerModel):
|
||||||
field_class = EmailField
|
field_class = EmailField
|
||||||
if self.type == FieldTypes.NUMBER:
|
if self.type == FieldTypes.NUMBER:
|
||||||
field_class = IntegerField
|
field_class = IntegerField
|
||||||
# TODO: Hidden?
|
|
||||||
if self.type == FieldTypes.HIDDEN:
|
if self.type == FieldTypes.HIDDEN:
|
||||||
|
field_class = HiddenField
|
||||||
kwargs["required"] = False
|
kwargs["required"] = False
|
||||||
kwargs["initial"] = self.placeholder
|
kwargs["default"] = self.placeholder
|
||||||
if self.type == FieldTypes.CHECKBOX:
|
if self.type == FieldTypes.CHECKBOX:
|
||||||
field_class = BooleanField
|
field_class = BooleanField
|
||||||
kwargs["required"] = False
|
kwargs["required"] = False
|
||||||
|
|
246
swagger.yaml
246
swagger.yaml
|
@ -14827,7 +14827,6 @@ definitions:
|
||||||
Token:
|
Token:
|
||||||
required:
|
required:
|
||||||
- identifier
|
- identifier
|
||||||
- user
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
|
@ -16007,246 +16006,25 @@ definitions:
|
||||||
format: uuid
|
format: uuid
|
||||||
readOnly: true
|
readOnly: true
|
||||||
policy:
|
policy:
|
||||||
type: object
|
title: Policy
|
||||||
properties:
|
|
||||||
policy_uuid:
|
|
||||||
title: Policy uuid
|
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
readOnly: true
|
|
||||||
created:
|
|
||||||
title: Created
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
last_updated:
|
|
||||||
title: Last updated
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
name:
|
|
||||||
title: Name
|
|
||||||
type: string
|
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
execution_logging:
|
|
||||||
title: Execution logging
|
|
||||||
description: When this option is enabled, all executions of this policy
|
|
||||||
will be logged. By default, only execution errors are logged.
|
|
||||||
type: boolean
|
|
||||||
readOnly: true
|
|
||||||
group:
|
group:
|
||||||
$ref: '#/definitions/Group'
|
title: Group
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
x-nullable: true
|
||||||
user:
|
user:
|
||||||
required:
|
title: User
|
||||||
- password
|
|
||||||
- username
|
|
||||||
- name
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
title: ID
|
|
||||||
type: integer
|
type: integer
|
||||||
readOnly: true
|
|
||||||
password:
|
|
||||||
title: Password
|
|
||||||
type: string
|
|
||||||
maxLength: 128
|
|
||||||
minLength: 1
|
|
||||||
last_login:
|
|
||||||
title: Last login
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
username:
|
policy_obj:
|
||||||
title: Username
|
$ref: '#/definitions/Policy'
|
||||||
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
|
group_obj:
|
||||||
only.
|
$ref: '#/definitions/Group'
|
||||||
type: string
|
user_obj:
|
||||||
pattern: ^[\w.@+-]+$
|
$ref: '#/definitions/User'
|
||||||
maxLength: 150
|
|
||||||
minLength: 1
|
|
||||||
first_name:
|
|
||||||
title: First name
|
|
||||||
type: string
|
|
||||||
maxLength: 150
|
|
||||||
last_name:
|
|
||||||
title: Last name
|
|
||||||
type: string
|
|
||||||
maxLength: 150
|
|
||||||
email:
|
|
||||||
title: Email address
|
|
||||||
type: string
|
|
||||||
format: email
|
|
||||||
maxLength: 254
|
|
||||||
is_active:
|
|
||||||
title: Active
|
|
||||||
description: Designates whether this user should be treated as active.
|
|
||||||
Unselect this instead of deleting accounts.
|
|
||||||
type: boolean
|
|
||||||
date_joined:
|
|
||||||
title: Date joined
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
uuid:
|
|
||||||
title: Uuid
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
readOnly: true
|
|
||||||
name:
|
|
||||||
title: Name
|
|
||||||
description: User's display name.
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
password_change_date:
|
|
||||||
title: Password change date
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
attributes:
|
|
||||||
title: Attributes
|
|
||||||
type: object
|
|
||||||
groups:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
title: ID
|
|
||||||
type: integer
|
|
||||||
readOnly: true
|
|
||||||
name:
|
|
||||||
title: Name
|
|
||||||
type: string
|
|
||||||
maxLength: 150
|
|
||||||
minLength: 1
|
|
||||||
permissions:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: integer
|
|
||||||
uniqueItems: true
|
|
||||||
readOnly: true
|
|
||||||
user_permissions:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
- codename
|
|
||||||
- content_type
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
title: ID
|
|
||||||
type: integer
|
|
||||||
readOnly: true
|
|
||||||
name:
|
|
||||||
title: Name
|
|
||||||
type: string
|
|
||||||
maxLength: 255
|
|
||||||
minLength: 1
|
|
||||||
codename:
|
|
||||||
title: Codename
|
|
||||||
type: string
|
|
||||||
maxLength: 100
|
|
||||||
minLength: 1
|
|
||||||
content_type:
|
|
||||||
title: Content type
|
|
||||||
type: integer
|
|
||||||
readOnly: true
|
|
||||||
sources:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
- slug
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
pbm_uuid:
|
|
||||||
title: Pbm uuid
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
readOnly: true
|
|
||||||
policy_engine_mode:
|
|
||||||
title: Policy engine mode
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- all
|
|
||||||
- any
|
|
||||||
name:
|
|
||||||
title: Name
|
|
||||||
description: Source's display Name.
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
slug:
|
|
||||||
title: Slug
|
|
||||||
description: Internal source name, used in URLs.
|
|
||||||
type: string
|
|
||||||
format: slug
|
|
||||||
pattern: ^[-a-zA-Z0-9_]+$
|
|
||||||
maxLength: 50
|
|
||||||
minLength: 1
|
|
||||||
enabled:
|
|
||||||
title: Enabled
|
|
||||||
type: boolean
|
|
||||||
authentication_flow:
|
|
||||||
title: Authentication flow
|
|
||||||
description: Flow to use when authenticating existing users.
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
x-nullable: true
|
|
||||||
enrollment_flow:
|
|
||||||
title: Enrollment flow
|
|
||||||
description: Flow to use when enrolling new users.
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
x-nullable: true
|
|
||||||
policies:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
readOnly: true
|
|
||||||
uniqueItems: true
|
|
||||||
property_mappings:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
uniqueItems: true
|
|
||||||
readOnly: true
|
|
||||||
ak_groups:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
- parent
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
group_uuid:
|
|
||||||
title: Group uuid
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
readOnly: true
|
|
||||||
name:
|
|
||||||
title: Name
|
|
||||||
type: string
|
|
||||||
maxLength: 80
|
|
||||||
minLength: 1
|
|
||||||
is_superuser:
|
|
||||||
title: Is superuser
|
|
||||||
description: Users added to this group will be superusers.
|
|
||||||
type: boolean
|
|
||||||
attributes:
|
|
||||||
title: Attributes
|
|
||||||
type: object
|
|
||||||
parent:
|
|
||||||
title: Parent
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
x-nullable: true
|
|
||||||
readOnly: true
|
|
||||||
readOnly: true
|
|
||||||
target:
|
target:
|
||||||
title: Target
|
title: Target
|
||||||
type: string
|
type: string
|
||||||
|
|
|
@ -4,10 +4,6 @@ export class AdminURLManager {
|
||||||
return `/administration/policies/${rest}`;
|
return `/administration/policies/${rest}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static policyBindings(rest: string): string {
|
|
||||||
return `/administration/policies/bindings/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static providers(rest: string): string {
|
static providers(rest: string): string {
|
||||||
return `/administration/providers/${rest}`;
|
return `/administration/providers/${rest}`;
|
||||||
}
|
}
|
||||||
|
@ -24,38 +20,10 @@ export class AdminURLManager {
|
||||||
return `/administration/stages/${rest}`;
|
return `/administration/stages/${rest}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static stagePrompts(rest: string): string {
|
|
||||||
return `/administration/stages_prompts/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static stageInvitations(rest: string): string {
|
|
||||||
return `/administration/stages/invitations/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static stageBindings(rest: string): string {
|
|
||||||
return `/administration/stages/bindings/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static sources(rest: string): string {
|
static sources(rest: string): string {
|
||||||
return `/administration/sources/${rest}`;
|
return `/administration/sources/${rest}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static tokens(rest: string): string {
|
|
||||||
return `/administration/tokens/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserURLManager {
|
|
||||||
|
|
||||||
static tokens(rest: string): string {
|
|
||||||
return `/-/user/tokens/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static authenticatorWebauthn(rest: string): string {
|
|
||||||
return `/-/user/authenticator/webauthn/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AppURLManager {
|
export class AppURLManager {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
import AKGlobal from "../../authentik.css";
|
import AKGlobal from "../../authentik.css";
|
||||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||||
|
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||||
import { MessageLevel } from "../messages/Message";
|
import { MessageLevel } from "../messages/Message";
|
||||||
import { IronFormElement } from "@polymer/iron-form/iron-form";
|
import { IronFormElement } from "@polymer/iron-form/iron-form";
|
||||||
import { camelToSnake } from "../../utils";
|
import { camelToSnake } from "../../utils";
|
||||||
|
@ -31,8 +32,11 @@ export class Form<T> extends LitElement {
|
||||||
@property()
|
@property()
|
||||||
send!: (data: T) => Promise<unknown>;
|
send!: (data: T) => Promise<unknown>;
|
||||||
|
|
||||||
|
@property({attribute: false})
|
||||||
|
nonFieldErrors?: string[];
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFCard, PFButton, PFForm, PFFormControl, AKGlobal, css`
|
return [PFBase, PFCard, PFButton, PFForm, PFAlert, PFFormControl, AKGlobal, css`
|
||||||
select[multiple] {
|
select[multiple] {
|
||||||
height: 15em;
|
height: 15em;
|
||||||
}
|
}
|
||||||
|
@ -84,6 +88,8 @@ export class Form<T> extends LitElement {
|
||||||
const values = form._serializeElementValues(element);
|
const values = form._serializeElementValues(element);
|
||||||
if (element.tagName.toLowerCase() === "select" && "multiple" in element.attributes) {
|
if (element.tagName.toLowerCase() === "select" && "multiple" in element.attributes) {
|
||||||
json[element.name] = values;
|
json[element.name] = values;
|
||||||
|
} else if (element.tagName.toLowerCase() === "input" && element.type === "date") {
|
||||||
|
json[element.name] = element.valueAsDate;
|
||||||
} else {
|
} else {
|
||||||
for (let v = 0; v < values.length; v++) {
|
for (let v = 0; v < values.length; v++) {
|
||||||
form._addSerializedElement(json, element.name, values[v]);
|
form._addSerializedElement(json, element.name, values[v]);
|
||||||
|
@ -114,6 +120,7 @@ export class Form<T> extends LitElement {
|
||||||
if (errorMessage instanceof Error) {
|
if (errorMessage instanceof Error) {
|
||||||
throw errorMessage;
|
throw errorMessage;
|
||||||
}
|
}
|
||||||
|
// assign all input-related errors to their elements
|
||||||
const elements: PaperInputElement[] = ironForm._getSubmittableElements();
|
const elements: PaperInputElement[] = ironForm._getSubmittableElements();
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
const elementName = element.name;
|
const elementName = element.name;
|
||||||
|
@ -123,6 +130,9 @@ export class Form<T> extends LitElement {
|
||||||
element.invalid = true;
|
element.invalid = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if ("non_field_errors" in errorMessage) {
|
||||||
|
this.nonFieldErrors = errorMessage["non_field_errors"];
|
||||||
|
}
|
||||||
throw new APIError(errorMessage);
|
throw new APIError(errorMessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -134,6 +144,24 @@ export class Form<T> extends LitElement {
|
||||||
return html`<slot></slot>`;
|
return html`<slot></slot>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderNonFieldErrors(): TemplateResult {
|
||||||
|
if (!this.nonFieldErrors) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`<div class="pf-c-form__alert">
|
||||||
|
${this.nonFieldErrors.map(err => {
|
||||||
|
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
|
||||||
|
<div class="pf-c-alert__icon">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
</div>
|
||||||
|
<h4 class="pf-c-alert__title">
|
||||||
|
${err}
|
||||||
|
</h4>
|
||||||
|
</div>`;
|
||||||
|
})}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
const rect = this.getBoundingClientRect();
|
const rect = this.getBoundingClientRect();
|
||||||
if (rect.x + rect.y + rect.width + rect.height === 0) {
|
if (rect.x + rect.y + rect.width + rect.height === 0) {
|
||||||
|
@ -141,6 +169,7 @@ export class Form<T> extends LitElement {
|
||||||
}
|
}
|
||||||
return html`<iron-form
|
return html`<iron-form
|
||||||
@iron-form-presubmit=${(ev: Event) => { this.submit(ev); }}>
|
@iron-form-presubmit=${(ev: Event) => { this.submit(ev); }}>
|
||||||
|
${this.renderNonFieldErrors()}
|
||||||
${this.renderForm()}
|
${this.renderForm()}
|
||||||
</iron-form>`;
|
</iron-form>`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,9 @@ export class Sidebar extends LitElement {
|
||||||
PFNav,
|
PFNav,
|
||||||
AKGlobal,
|
AKGlobal,
|
||||||
css`
|
css`
|
||||||
|
:host {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
.pf-c-nav__link.pf-m-current::after,
|
.pf-c-nav__link.pf-m-current::after,
|
||||||
.pf-c-nav__link.pf-m-current:hover::after,
|
.pf-c-nav__link.pf-m-current:hover::after,
|
||||||
.pf-c-nav__item.pf-m-current:not(.pf-m-expanded) .pf-c-nav__link::after {
|
.pf-c-nav__item.pf-m-current:not(.pf-m-expanded) .pf-c-nav__link::after {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import "../../elements/Tabs";
|
||||||
import "../../elements/charts/ApplicationAuthorizeChart";
|
import "../../elements/charts/ApplicationAuthorizeChart";
|
||||||
import "../../elements/buttons/ModalButton";
|
import "../../elements/buttons/ModalButton";
|
||||||
import "../../elements/buttons/SpinnerButton";
|
import "../../elements/buttons/SpinnerButton";
|
||||||
import "../../elements/policies/BoundPoliciesList";
|
import "../policies/BoundPoliciesList";
|
||||||
import "../../elements/EmptyState";
|
import "../../elements/EmptyState";
|
||||||
import "../../elements/events/ObjectChangelog";
|
import "../../elements/events/ObjectChangelog";
|
||||||
import { Application, CoreApi } from "authentik-api";
|
import { Application, CoreApi } from "authentik-api";
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { customElement, html, property, TemplateResult } from "lit-element";
|
||||||
import { AKResponse } from "../../api/Client";
|
import { AKResponse } from "../../api/Client";
|
||||||
import { TablePage } from "../../elements/table/TablePage";
|
import { TablePage } from "../../elements/table/TablePage";
|
||||||
|
|
||||||
import "../../elements/policies/BoundPoliciesList";
|
import "../policies/BoundPoliciesList";
|
||||||
import "../../elements/buttons/SpinnerButton";
|
import "../../elements/buttons/SpinnerButton";
|
||||||
import "../../elements/forms/ModalForm";
|
import "../../elements/forms/ModalForm";
|
||||||
import { TableColumn } from "../../elements/table/Table";
|
import { TableColumn } from "../../elements/table/Table";
|
||||||
|
|
|
@ -4,16 +4,19 @@ import { AKResponse } from "../../api/Client";
|
||||||
import { Table, TableColumn } from "../../elements/table/Table";
|
import { Table, TableColumn } from "../../elements/table/Table";
|
||||||
|
|
||||||
import "../../elements/forms/DeleteForm";
|
import "../../elements/forms/DeleteForm";
|
||||||
|
import "../../elements/forms/ModalForm";
|
||||||
|
import "./StageBindingForm";
|
||||||
import "../../elements/Tabs";
|
import "../../elements/Tabs";
|
||||||
import "../../elements/buttons/ModalButton";
|
import "../../elements/buttons/ModalButton";
|
||||||
import "../../elements/buttons/SpinnerButton";
|
import "../../elements/buttons/SpinnerButton";
|
||||||
import "../../elements/buttons/Dropdown";
|
import "../../elements/buttons/Dropdown";
|
||||||
import "../../elements/policies/BoundPoliciesList";
|
import "../policies/BoundPoliciesList";
|
||||||
import { until } from "lit-html/directives/until";
|
import { until } from "lit-html/directives/until";
|
||||||
import { PAGE_SIZE } from "../../constants";
|
import { PAGE_SIZE } from "../../constants";
|
||||||
import { FlowsApi, FlowStageBinding, StagesApi } from "authentik-api";
|
import { FlowsApi, FlowStageBinding, StagesApi } from "authentik-api";
|
||||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
import { AdminURLManager } from "../../api/legacy";
|
import { AdminURLManager } from "../../api/legacy";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ak-bound-stages-list")
|
@customElement("ak-bound-stages-list")
|
||||||
export class BoundStagesList extends Table<FlowStageBinding> {
|
export class BoundStagesList extends Table<FlowStageBinding> {
|
||||||
|
@ -52,12 +55,19 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||||
</ak-spinner-button>
|
</ak-spinner-button>
|
||||||
<div slot="modal"></div>
|
<div slot="modal"></div>
|
||||||
</ak-modal-button>
|
</ak-modal-button>
|
||||||
<ak-modal-button href="${AdminURLManager.stageBindings(`${item.pk}/update/`)}">
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
<span slot="submit">
|
||||||
|
${gettext("Update")}
|
||||||
|
</span>
|
||||||
|
<span slot="header">
|
||||||
|
${gettext("Update Stage binding")}
|
||||||
|
</span>
|
||||||
|
<ak-stage-binding-form slot="form" .fsb=${item}>
|
||||||
|
</ak-stage-binding-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||||
${gettext("Edit Binding")}
|
${gettext("Edit Binding")}
|
||||||
</ak-spinner-button>
|
</button>
|
||||||
<div slot="modal"></div>
|
</ak-forms-modal>
|
||||||
</ak-modal-button>
|
|
||||||
<ak-forms-delete
|
<ak-forms-delete
|
||||||
.obj=${item}
|
.obj=${item}
|
||||||
objectLabel=${gettext("Stage binding")}
|
objectLabel=${gettext("Stage binding")}
|
||||||
|
@ -95,12 +105,19 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||||
${gettext("No stages are currently bound to this flow.")}
|
${gettext("No stages are currently bound to this flow.")}
|
||||||
</div>
|
</div>
|
||||||
<div slot="primary">
|
<div slot="primary">
|
||||||
<ak-modal-button href="${AdminURLManager.stageBindings(`create/?target=${this.target}`)}">
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
<span slot="submit">
|
||||||
${gettext("Bind Stage")}
|
${gettext("Create")}
|
||||||
</ak-spinner-button>
|
</span>
|
||||||
<div slot="modal"></div>
|
<span slot="header">
|
||||||
</ak-modal-button>
|
${gettext("Create Stage binding")}
|
||||||
|
</span>
|
||||||
|
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
|
||||||
|
</ak-stage-binding-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||||
|
${gettext("Bind stage")}
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
</div>
|
</div>
|
||||||
</ak-empty-state>`);
|
</ak-empty-state>`);
|
||||||
}
|
}
|
||||||
|
@ -127,12 +144,19 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||||
}), html`<ak-spinner></ak-spinner>`)}
|
}), html`<ak-spinner></ak-spinner>`)}
|
||||||
</ul>
|
</ul>
|
||||||
</ak-dropdown>
|
</ak-dropdown>
|
||||||
<ak-modal-button href="${AdminURLManager.stageBindings(`create/?target=${this.target}`)}">
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
<span slot="submit">
|
||||||
${gettext("Bind Stage")}
|
${gettext("Create")}
|
||||||
</ak-spinner-button>
|
</span>
|
||||||
<div slot="modal"></div>
|
<span slot="header">
|
||||||
</ak-modal-button>
|
${gettext("Create Stage binding")}
|
||||||
|
</span>
|
||||||
|
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
|
||||||
|
</ak-stage-binding-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||||
|
${gettext("Bind stage")}
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
${super.renderToolbar()}
|
${super.renderToolbar()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,13 @@ import "../../elements/Tabs";
|
||||||
import "../../elements/events/ObjectChangelog";
|
import "../../elements/events/ObjectChangelog";
|
||||||
import "../../elements/buttons/ModalButton";
|
import "../../elements/buttons/ModalButton";
|
||||||
import "../../elements/buttons/SpinnerButton";
|
import "../../elements/buttons/SpinnerButton";
|
||||||
import "../../elements/policies/BoundPoliciesList";
|
import "../policies/BoundPoliciesList";
|
||||||
import "./BoundStagesList";
|
import "./BoundStagesList";
|
||||||
import "./FlowDiagram";
|
import "./FlowDiagram";
|
||||||
import { Flow, FlowsApi } from "authentik-api";
|
import { Flow, FlowsApi } from "authentik-api";
|
||||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
|
|
||||||
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||||
|
@ -33,7 +34,7 @@ export class FlowViewPage extends LitElement {
|
||||||
flow!: Flow;
|
flow!: Flow;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFPage, PFCard, PFContent, PFGallery, AKGlobal].concat(
|
return [PFBase, PFPage, PFButton, PFCard, PFContent, PFGallery, AKGlobal].concat(
|
||||||
css`
|
css`
|
||||||
img.pf-icon {
|
img.pf-icon {
|
||||||
max-height: 24px;
|
max-height: 24px;
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { FlowsApi, FlowStageBinding, FlowStageBindingPolicyEngineModeEnum, Stage, StagesApi } from "authentik-api";
|
||||||
|
import { gettext } from "django";
|
||||||
|
import { customElement, property } from "lit-element";
|
||||||
|
import { html, TemplateResult } from "lit-html";
|
||||||
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
|
import { Form } from "../../elements/forms/Form";
|
||||||
|
import { until } from "lit-html/directives/until";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
import "../../elements/forms/HorizontalFormElement";
|
||||||
|
import { groupBy } from "../../utils";
|
||||||
|
|
||||||
|
@customElement("ak-stage-binding-form")
|
||||||
|
export class StageBindingForm extends Form<FlowStageBinding> {
|
||||||
|
|
||||||
|
@property({attribute: false})
|
||||||
|
fsb?: FlowStageBinding;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
targetPk?: string;
|
||||||
|
|
||||||
|
getSuccessMessage(): string {
|
||||||
|
if (this.fsb) {
|
||||||
|
return gettext("Successfully updated binding.");
|
||||||
|
} else {
|
||||||
|
return gettext("Successfully created binding.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send = (data: FlowStageBinding): Promise<FlowStageBinding> => {
|
||||||
|
if (this.fsb) {
|
||||||
|
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsUpdate({
|
||||||
|
fsbUuid: this.fsb.pk || "",
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsCreate({
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
groupStages(stages: Stage[]): TemplateResult {
|
||||||
|
return html`
|
||||||
|
${groupBy<Stage>(stages, (s => s.verboseName || "")).map(([group, stages]) => {
|
||||||
|
return html`<optgroup label=${group}>
|
||||||
|
${stages.map(stage => {
|
||||||
|
const selected = (this.fsb?.stage === stage.pk);
|
||||||
|
return html`<option ?selected=${selected} value=${ifDefined(stage.pk)}>${stage.name}</option>`;
|
||||||
|
})}
|
||||||
|
</optgroup>`;
|
||||||
|
})}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder(): Promise<number> {
|
||||||
|
if (this.fsb) {
|
||||||
|
return Promise.resolve(this.fsb.order);
|
||||||
|
}
|
||||||
|
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsList({
|
||||||
|
target: this.targetPk || "",
|
||||||
|
}).then(bindings => {
|
||||||
|
const orders = bindings.results.map(binding => binding.order);
|
||||||
|
return Math.max(...orders) + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTarget(): TemplateResult {
|
||||||
|
if (this.fsb?.target || this.targetPk) {
|
||||||
|
return html`
|
||||||
|
<input required name="target" type="hidden" value=${ifDefined(this.fsb?.target || this.targetPk)}>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html`<ak-form-element-horizontal
|
||||||
|
label=${gettext("Target")}
|
||||||
|
?required=${true}
|
||||||
|
name="target">
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||||
|
ordering: "pk"
|
||||||
|
}).then(flows => {
|
||||||
|
return flows.results.map(flow => {
|
||||||
|
// No ?selected check here, as this input isnt shown on update forms
|
||||||
|
return html`<option value=${ifDefined(flow.pk)}>${flow.name}</option>`;
|
||||||
|
});
|
||||||
|
}), html``)}
|
||||||
|
</select>
|
||||||
|
</ak-form-element-horizontal>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderForm(): TemplateResult {
|
||||||
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
${this.renderTarget()}
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Stage")}
|
||||||
|
?required=${true}
|
||||||
|
name="stage">
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
${until(new StagesApi(DEFAULT_CONFIG).stagesAllList({
|
||||||
|
ordering: "pk"
|
||||||
|
}).then(stages => {
|
||||||
|
return this.groupStages(stages.results);
|
||||||
|
}), html``)}
|
||||||
|
</select>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal name="evaluateOnPlan">
|
||||||
|
<div class="pf-c-check">
|
||||||
|
<input type="checkbox" class="pf-c-check__input" ?checked=${this.fsb?.evaluateOnPlan || true}>
|
||||||
|
<label class="pf-c-check__label">
|
||||||
|
${gettext("Evaluate on plan")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="pf-c-form__helper-text">${gettext("Evaluate policies during the Flow planning process. Disable this for input-based policies.")}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal name="reEvaluatePolicies">
|
||||||
|
<div class="pf-c-check">
|
||||||
|
<input type="checkbox" class="pf-c-check__input" ?checked=${this.fsb?.reEvaluatePolicies || false}>
|
||||||
|
<label class="pf-c-check__label">
|
||||||
|
${gettext("Re-evaluate policies")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="pf-c-form__helper-text">${gettext("Evaluate policies when the Stage is present to the user.")}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Order")}
|
||||||
|
?required=${true}
|
||||||
|
name="order">
|
||||||
|
<input type="number" value="${until(this.getOrder(), this.fsb?.order)}" class="pf-c-form-control" required>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Policy engine mode")}
|
||||||
|
?required=${true}
|
||||||
|
name="policyEngineMode">
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
<option value=${FlowStageBindingPolicyEngineModeEnum.Any} ?selected=${this.fsb?.policyEngineMode === FlowStageBindingPolicyEngineModeEnum.Any}>
|
||||||
|
${gettext("ANY, any policy must match to grant access.")}
|
||||||
|
</option>
|
||||||
|
<option value=${FlowStageBindingPolicyEngineModeEnum.All} ?selected=${this.fsb?.policyEngineMode === FlowStageBindingPolicyEngineModeEnum.All}>
|
||||||
|
${gettext("ALL, all policies must match to grant access.")}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,7 +15,10 @@ import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
import { AdminURLManager } from "../../api/legacy";
|
import { AdminURLManager } from "../../api/legacy";
|
||||||
|
|
||||||
import "../../elements/forms/ModalForm";
|
import "../../elements/forms/ModalForm";
|
||||||
import "../../pages/groups/GroupForm";
|
import "../groups/GroupForm";
|
||||||
|
import "../users/UserForm";
|
||||||
|
import "./PolicyBindingForm";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ak-bound-policies-list")
|
@customElement("ak-bound-policies-list")
|
||||||
export class BoundPoliciesList extends Table<PolicyBinding> {
|
export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||||
|
@ -43,11 +46,11 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||||
|
|
||||||
getPolicyUserGroupRow(item: PolicyBinding): string {
|
getPolicyUserGroupRow(item: PolicyBinding): string {
|
||||||
if (item.policy) {
|
if (item.policy) {
|
||||||
return gettext(`Policy ${item.policy.name}`);
|
return gettext(`Policy ${item.policyObj?.name}`);
|
||||||
} else if (item.group) {
|
} else if (item.group) {
|
||||||
return gettext(`Group ${item.group.name}`);
|
return gettext(`Group ${item.groupObj?.name}`);
|
||||||
} else if (item.user) {
|
} else if (item.user) {
|
||||||
return gettext(`User ${item.user.name}`);
|
return gettext(`User ${item.userObj?.name}`);
|
||||||
} else {
|
} else {
|
||||||
return gettext("");
|
return gettext("");
|
||||||
}
|
}
|
||||||
|
@ -55,7 +58,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||||
|
|
||||||
getObjectEditButton(item: PolicyBinding): TemplateResult {
|
getObjectEditButton(item: PolicyBinding): TemplateResult {
|
||||||
if (item.policy) {
|
if (item.policy) {
|
||||||
return html`<ak-modal-button href="${AdminURLManager.policies(`${item.policy?.policyUuid}/update/`)}">
|
return html`<ak-modal-button href="${AdminURLManager.policies(`${item.policy}/update/`)}">
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||||
${gettext("Edit Policy")}
|
${gettext("Edit Policy")}
|
||||||
</ak-spinner-button>
|
</ak-spinner-button>
|
||||||
|
@ -69,19 +72,26 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
${gettext("Update Group")}
|
${gettext("Update Group")}
|
||||||
</span>
|
</span>
|
||||||
<ak-group-form slot="form" .group=${item.group}>
|
<ak-group-form slot="form" .group=${item.groupObj}>
|
||||||
</ak-group-form>
|
</ak-group-form>
|
||||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||||
${gettext("Edit Group")}
|
${gettext("Edit Group")}
|
||||||
</button>
|
</button>
|
||||||
</ak-forms-modal>`;
|
</ak-forms-modal>`;
|
||||||
} else if (item.user) {
|
} else if (item.user) {
|
||||||
return html`<ak-modal-button href="${AdminURLManager.policies(`${item.user?.id}/update/`)}">
|
return html`<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
<span slot="submit">
|
||||||
${gettext("Edit User")}
|
${gettext("Update")}
|
||||||
</ak-spinner-button>
|
</span>
|
||||||
<div slot="modal"></div>
|
<span slot="header">
|
||||||
</ak-modal-button>`;
|
${gettext("Update User")}
|
||||||
|
</span>
|
||||||
|
<ak-user-form slot="form" .user=${item.userObj}>
|
||||||
|
</ak-user-form>
|
||||||
|
<button slot="trigger" class="pf-m-secondary pf-c-button">
|
||||||
|
${gettext("Edit")}
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>`;
|
||||||
} else {
|
} else {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
@ -95,12 +105,19 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||||
html`${item.timeout}`,
|
html`${item.timeout}`,
|
||||||
html`
|
html`
|
||||||
${this.getObjectEditButton(item)}
|
${this.getObjectEditButton(item)}
|
||||||
<ak-modal-button href="${AdminURLManager.policyBindings(`${item.pk}/update/`)}">
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
<span slot="submit">
|
||||||
|
${gettext("Update")}
|
||||||
|
</span>
|
||||||
|
<span slot="header">
|
||||||
|
${gettext("Update Binding")}
|
||||||
|
</span>
|
||||||
|
<ak-policy-binding-form slot="form" .binding=${item} targetPk=${ifDefined(this.target)}>
|
||||||
|
</ak-policy-binding-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||||
${gettext("Edit Binding")}
|
${gettext("Edit Binding")}
|
||||||
</ak-spinner-button>
|
</button>
|
||||||
<div slot="modal"></div>
|
</ak-forms-modal>
|
||||||
</ak-modal-button>
|
|
||||||
<ak-forms-delete
|
<ak-forms-delete
|
||||||
.obj=${item}
|
.obj=${item}
|
||||||
objectLabel=${gettext("Policy binding")}
|
objectLabel=${gettext("Policy binding")}
|
||||||
|
@ -122,12 +139,19 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||||
${gettext("No policies are currently bound to this object.")}
|
${gettext("No policies are currently bound to this object.")}
|
||||||
</div>
|
</div>
|
||||||
<div slot="primary">
|
<div slot="primary">
|
||||||
<ak-modal-button href=${AdminURLManager.policyBindings(`create/?target=${this.target}`)}>
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
<span slot="submit">
|
||||||
${gettext("Bind Policy")}
|
${gettext("Create")}
|
||||||
</ak-spinner-button>
|
</span>
|
||||||
<div slot="modal"></div>
|
<span slot="header">
|
||||||
</ak-modal-button>
|
${gettext("Create Binding")}
|
||||||
|
</span>
|
||||||
|
<ak-policy-binding-form slot="form" targetPk=${ifDefined(this.target)}>
|
||||||
|
</ak-policy-binding-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||||
|
${gettext("Create Binding")}
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
</div>
|
</div>
|
||||||
</ak-empty-state>`);
|
</ak-empty-state>`);
|
||||||
}
|
}
|
||||||
|
@ -154,12 +178,19 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||||
}), html`<ak-spinner></ak-spinner>`)}
|
}), html`<ak-spinner></ak-spinner>`)}
|
||||||
</ul>
|
</ul>
|
||||||
</ak-dropdown>
|
</ak-dropdown>
|
||||||
<ak-modal-button href=${AdminURLManager.policyBindings(`create/?target=${this.target}`)}>
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
<span slot="submit">
|
||||||
${gettext("Bind Policy")}
|
${gettext("Create")}
|
||||||
</ak-spinner-button>
|
</span>
|
||||||
<div slot="modal"></div>
|
<span slot="header">
|
||||||
</ak-modal-button>
|
${gettext("Create Binding")}
|
||||||
|
</span>
|
||||||
|
<ak-policy-binding-form slot="form" targetPk=${ifDefined(this.target)}>
|
||||||
|
</ak-policy-binding-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||||
|
${gettext("Create Binding")}
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
${super.renderToolbar()}
|
${super.renderToolbar()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { CoreApi, PoliciesApi, Policy, PolicyBinding } from "authentik-api";
|
||||||
|
import { gettext } from "django";
|
||||||
|
import { customElement, property } from "lit-element";
|
||||||
|
import { html, TemplateResult } from "lit-html";
|
||||||
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
|
import { Form } from "../../elements/forms/Form";
|
||||||
|
import { until } from "lit-html/directives/until";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
import { groupBy } from "../../utils";
|
||||||
|
import "../../elements/forms/HorizontalFormElement";
|
||||||
|
|
||||||
|
@customElement("ak-policy-binding-form")
|
||||||
|
export class PolicyBindingForm extends Form<PolicyBinding> {
|
||||||
|
|
||||||
|
@property({attribute: false})
|
||||||
|
binding?: PolicyBinding;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
targetPk?: string;
|
||||||
|
|
||||||
|
getSuccessMessage(): string {
|
||||||
|
if (this.binding) {
|
||||||
|
return gettext("Successfully updated binding.");
|
||||||
|
} else {
|
||||||
|
return gettext("Successfully created binding.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async customValidate(form: PolicyBinding): Promise<PolicyBinding> {
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
send = (data: PolicyBinding): Promise<PolicyBinding> => {
|
||||||
|
if (this.binding) {
|
||||||
|
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsUpdate({
|
||||||
|
policyBindingUuid: this.binding.pk || "",
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsCreate({
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
groupPolicies(policies: Policy[]): TemplateResult {
|
||||||
|
return html`
|
||||||
|
${groupBy<Policy>(policies, (p => p.verboseName || "")).map(([group, policies]) => {
|
||||||
|
return html`<optgroup label=${group}>
|
||||||
|
${policies.map(p => {
|
||||||
|
const selected = (this.binding?.policy === p.pk);
|
||||||
|
return html`<option ?selected=${selected} value=${ifDefined(p.pk)}>${p.name}</option>`;
|
||||||
|
})}
|
||||||
|
</optgroup>`;
|
||||||
|
})}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder(): Promise<number> {
|
||||||
|
if (this.binding) {
|
||||||
|
return Promise.resolve(this.binding.order);
|
||||||
|
}
|
||||||
|
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({
|
||||||
|
target: this.targetPk || "",
|
||||||
|
}).then(bindings => {
|
||||||
|
const orders = bindings.results.map(binding => binding.order);
|
||||||
|
return Math.max(...orders) + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderForm(): TemplateResult {
|
||||||
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Policy")}
|
||||||
|
name="policy">
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
<option value="" ?selected=${this.binding?.policy === undefined}>---------</option>
|
||||||
|
${until(new PoliciesApi(DEFAULT_CONFIG).policiesAllList({
|
||||||
|
ordering: "pk"
|
||||||
|
}).then(policies => {
|
||||||
|
return this.groupPolicies(policies.results);
|
||||||
|
}), html``)}
|
||||||
|
</select>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Group")}
|
||||||
|
name="group">
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
<option value="" ?selected=${this.binding?.group === undefined}>---------</option>
|
||||||
|
${until(new CoreApi(DEFAULT_CONFIG).coreGroupsList({
|
||||||
|
ordering: "pk"
|
||||||
|
}).then(groups => {
|
||||||
|
return groups.results.map(group => {
|
||||||
|
return html`<option value=${ifDefined(group.pk)} ?selected=${group.pk === this.binding?.group}>${group.name}</option>`;
|
||||||
|
});
|
||||||
|
}), html``)}
|
||||||
|
</select>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("User")}
|
||||||
|
name="user">
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
<option value="" ?selected=${this.binding?.user === undefined}>---------</option>
|
||||||
|
${until(new CoreApi(DEFAULT_CONFIG).coreUsersList({
|
||||||
|
ordering: "pk"
|
||||||
|
}).then(users => {
|
||||||
|
return users.results.map(user => {
|
||||||
|
return html`<option value=${ifDefined(user.pk)} ?selected=${user.pk === this.binding?.user}>${user.name}</option>`;
|
||||||
|
});
|
||||||
|
}), html``)}
|
||||||
|
</select>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<input required name="target" type="hidden" value=${ifDefined(this.binding?.target || this.targetPk)}>
|
||||||
|
<ak-form-element-horizontal name="enabled">
|
||||||
|
<div class="pf-c-check">
|
||||||
|
<input type="checkbox" class="pf-c-check__input" ?checked=${this.binding?.enabled || true}>
|
||||||
|
<label class="pf-c-check__label">
|
||||||
|
${gettext("Enabled")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Order")}
|
||||||
|
?required=${true}
|
||||||
|
name="order">
|
||||||
|
<input type="number" value="${until(this.getOrder(), this.binding?.order)}" class="pf-c-form-control" required>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Timeout")}
|
||||||
|
?required=${true}
|
||||||
|
name="timeout">
|
||||||
|
<input type="number" value="${this.binding?.timeout || 30}" class="pf-c-form-control" required>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -43,7 +43,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
|
||||||
page: page,
|
page: page,
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
search: this.search || "",
|
search: this.search || "",
|
||||||
managedIsnull: this.hideManaged.toString(),
|
managedIsnull: this.hideManaged ? "true" : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,6 +126,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
|
||||||
<div class="pf-c-check">
|
<div class="pf-c-check">
|
||||||
<input class="pf-c-check__input" type="checkbox" id="hide-managed" name="hide-managed" ?checked=${this.hideManaged} @change=${() => {
|
<input class="pf-c-check__input" type="checkbox" id="hide-managed" name="hide-managed" ?checked=${this.hideManaged} @change=${() => {
|
||||||
this.hideManaged = !this.hideManaged;
|
this.hideManaged = !this.hideManaged;
|
||||||
|
this.page = 1;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
}} />
|
}} />
|
||||||
<label class="pf-c-check__label" for="hide-managed">${gettext("Hide managed mappings")}</label>
|
<label class="pf-c-check__label" for="hide-managed">${gettext("Hide managed mappings")}</label>
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Invitation, StagesApi } from "authentik-api";
|
||||||
|
import { gettext } from "django";
|
||||||
|
import { customElement, property } from "lit-element";
|
||||||
|
import { html, TemplateResult } from "lit-html";
|
||||||
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
|
import { Form } from "../../elements/forms/Form";
|
||||||
|
import "../../elements/forms/HorizontalFormElement";
|
||||||
|
import "../../elements/CodeMirror";
|
||||||
|
import YAML from "yaml";
|
||||||
|
|
||||||
|
@customElement("ak-stage-invitation-form")
|
||||||
|
export class InvitationForm extends Form<Invitation> {
|
||||||
|
|
||||||
|
@property({attribute: false})
|
||||||
|
invitation?: Invitation;
|
||||||
|
|
||||||
|
getSuccessMessage(): string {
|
||||||
|
if (this.invitation) {
|
||||||
|
return gettext("Successfully updated invitation.");
|
||||||
|
} else {
|
||||||
|
return gettext("Successfully created invitation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send = (data: Invitation): Promise<Invitation> => {
|
||||||
|
if (this.invitation) {
|
||||||
|
return new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsUpdate({
|
||||||
|
inviteUuid: this.invitation.pk || "",
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsCreate({
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderForm(): TemplateResult {
|
||||||
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Expires")}
|
||||||
|
?required=${true}
|
||||||
|
name="expires">
|
||||||
|
<input type="date" class="pf-c-form-control" required>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Attributes")}
|
||||||
|
name="fixedData">
|
||||||
|
<ak-codemirror mode="yaml" value="${YAML.stringify(this.invitation?.fixedData)}">
|
||||||
|
</ak-codemirror>
|
||||||
|
<p class="pf-c-form__helper-text">${gettext("Optional data which is loaded into the flow's 'prompt_data' context variable.")}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,11 +6,12 @@ import { TablePage } from "../../elements/table/TablePage";
|
||||||
import "../../elements/buttons/ModalButton";
|
import "../../elements/buttons/ModalButton";
|
||||||
import "../../elements/buttons/SpinnerButton";
|
import "../../elements/buttons/SpinnerButton";
|
||||||
import "../../elements/forms/DeleteForm";
|
import "../../elements/forms/DeleteForm";
|
||||||
|
import "../../elements/forms/ModalForm";
|
||||||
|
import "./InvitationForm";
|
||||||
import { TableColumn } from "../../elements/table/Table";
|
import { TableColumn } from "../../elements/table/Table";
|
||||||
import { PAGE_SIZE } from "../../constants";
|
import { PAGE_SIZE } from "../../constants";
|
||||||
import { Invitation, StagesApi } from "authentik-api";
|
import { Invitation, StagesApi } from "authentik-api";
|
||||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
import { AdminURLManager } from "../../api/legacy";
|
|
||||||
|
|
||||||
@customElement("ak-stage-invitation-list")
|
@customElement("ak-stage-invitation-list")
|
||||||
export class InvitationListPage extends TablePage<Invitation> {
|
export class InvitationListPage extends TablePage<Invitation> {
|
||||||
|
@ -71,12 +72,19 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||||
|
|
||||||
renderToolbar(): TemplateResult {
|
renderToolbar(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ak-modal-button href=${AdminURLManager.stageInvitations("create/")}>
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
<span slot="submit">
|
||||||
${gettext("Create")}
|
${gettext("Create")}
|
||||||
</ak-spinner-button>
|
</span>
|
||||||
<div slot="modal"></div>
|
<span slot="header">
|
||||||
</ak-modal-button>
|
${gettext("Create Invitation")}
|
||||||
|
</span>
|
||||||
|
<ak-stage-invitation-form slot="form">
|
||||||
|
</ak-stage-invitation-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||||
|
${gettext("Create")}
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
${super.renderToolbar()}
|
${super.renderToolbar()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { Prompt, PromptTypeEnum, StagesApi } from "authentik-api";
|
||||||
|
import { gettext } from "django";
|
||||||
|
import { customElement, property } from "lit-element";
|
||||||
|
import { html, TemplateResult } from "lit-html";
|
||||||
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
|
import { Form } from "../../elements/forms/Form";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
import "../../elements/forms/HorizontalFormElement";
|
||||||
|
|
||||||
|
@customElement("ak-stage-prompt-form")
|
||||||
|
export class PromptForm extends Form<Prompt> {
|
||||||
|
|
||||||
|
@property({attribute: false})
|
||||||
|
prompt?: Prompt;
|
||||||
|
|
||||||
|
getSuccessMessage(): string {
|
||||||
|
if (this.prompt) {
|
||||||
|
return gettext("Successfully updated prompt.");
|
||||||
|
} else {
|
||||||
|
return gettext("Successfully created prompt.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send = (data: Prompt): Promise<Prompt> => {
|
||||||
|
if (this.prompt) {
|
||||||
|
return new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsUpdate({
|
||||||
|
promptUuid: this.prompt.pk || "",
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsCreate({
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTypes(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<option value=${PromptTypeEnum.Text} ?selected=${this.prompt?.type === PromptTypeEnum.Text}>
|
||||||
|
${gettext("Text: Simple Text input")}
|
||||||
|
</option>
|
||||||
|
<option value=${PromptTypeEnum.Username} ?selected=${this.prompt?.type === PromptTypeEnum.Username}>
|
||||||
|
${gettext("Username: Same as Text input, but checks for and prevents duplicate usernames.")}
|
||||||
|
</option>
|
||||||
|
<option value=${PromptTypeEnum.Email} ?selected=${this.prompt?.type === PromptTypeEnum.Email}>
|
||||||
|
${gettext("Email: Text field with Email type.")}
|
||||||
|
</option>
|
||||||
|
<option value=${PromptTypeEnum.Password} ?selected=${this.prompt?.type === PromptTypeEnum.Password}>
|
||||||
|
${gettext("Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical.")}
|
||||||
|
</option>
|
||||||
|
<option value=${PromptTypeEnum.Number} ?selected=${this.prompt?.type === PromptTypeEnum.Number}>
|
||||||
|
${gettext("Number")}
|
||||||
|
</option>
|
||||||
|
<option value=${PromptTypeEnum.Checkbox} ?selected=${this.prompt?.type === PromptTypeEnum.Checkbox}>
|
||||||
|
${gettext("Checkbox")}
|
||||||
|
</option>
|
||||||
|
<option value=${PromptTypeEnum.Date} ?selected=${this.prompt?.type === PromptTypeEnum.Date}>
|
||||||
|
${gettext("Date")}
|
||||||
|
</option>
|
||||||
|
<option value=${PromptTypeEnum.DateTime} ?selected=${this.prompt?.type === PromptTypeEnum.DateTime}>
|
||||||
|
${gettext("Date Time")}
|
||||||
|
</option>
|
||||||
|
<option value=${PromptTypeEnum.Separator} ?selected=${this.prompt?.type === PromptTypeEnum.Separator}>
|
||||||
|
${gettext("Separator: Static Separator Line")}
|
||||||
|
</option>
|
||||||
|
<option value=${PromptTypeEnum.Hidden} ?selected=${this.prompt?.type === PromptTypeEnum.Hidden}>
|
||||||
|
${gettext("Hidden: Hidden field, can be used to insert data into form.")}
|
||||||
|
</option>
|
||||||
|
<option value=${PromptTypeEnum.Static} ?selected=${this.prompt?.type === PromptTypeEnum.Static}>
|
||||||
|
${gettext("Static: Static value, displayed as-is.")}
|
||||||
|
</option>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderForm(): TemplateResult {
|
||||||
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Field Key")}
|
||||||
|
?required=${true}
|
||||||
|
name="fieldKey">
|
||||||
|
<input type="text" value="${ifDefined(this.prompt?.fieldKey)}" class="pf-c-form-control" required>
|
||||||
|
<p class="pf-c-form__helper-text">${gettext("Name of the form field, also used to store the value.")}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Label")}
|
||||||
|
?required=${true}
|
||||||
|
name="label">
|
||||||
|
<input type="text" value="${ifDefined(this.prompt?.label)}" class="pf-c-form-control" required>
|
||||||
|
<p class="pf-c-form__helper-text">${gettext("Label shown next to/above the prompt.")}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Type")}
|
||||||
|
?required=${true}
|
||||||
|
name="type">
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
${this.renderTypes()}
|
||||||
|
</select>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal name="required">
|
||||||
|
<div class="pf-c-check">
|
||||||
|
<input type="checkbox" class="pf-c-check__input" ?checked=${this.prompt?.required || false}>
|
||||||
|
<label class="pf-c-check__label">
|
||||||
|
${gettext("Required")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Placeholder")}
|
||||||
|
name="placeholder">
|
||||||
|
<input type="text" value="${ifDefined(this.prompt?.placeholder)}" class="pf-c-form-control" required>
|
||||||
|
<p class="pf-c-form__helper-text">${gettext("Optionally pre-fill the input value")}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Order")}
|
||||||
|
?required=${true}
|
||||||
|
name="order">
|
||||||
|
<input type="number" value="${ifDefined(this.prompt?.order)}" class="pf-c-form-control" required>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,11 +6,12 @@ import { TablePage } from "../../elements/table/TablePage";
|
||||||
import "../../elements/buttons/ModalButton";
|
import "../../elements/buttons/ModalButton";
|
||||||
import "../../elements/buttons/SpinnerButton";
|
import "../../elements/buttons/SpinnerButton";
|
||||||
import "../../elements/forms/DeleteForm";
|
import "../../elements/forms/DeleteForm";
|
||||||
|
import "../../elements/forms/ModalForm";
|
||||||
|
import "./PromptForm";
|
||||||
import { TableColumn } from "../../elements/table/Table";
|
import { TableColumn } from "../../elements/table/Table";
|
||||||
import { PAGE_SIZE } from "../../constants";
|
import { PAGE_SIZE } from "../../constants";
|
||||||
import { Prompt, StagesApi } from "authentik-api";
|
import { Prompt, StagesApi } from "authentik-api";
|
||||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
import { AdminURLManager } from "../../api/legacy";
|
|
||||||
|
|
||||||
@customElement("ak-stage-prompt-list")
|
@customElement("ak-stage-prompt-list")
|
||||||
export class PromptListPage extends TablePage<Prompt> {
|
export class PromptListPage extends TablePage<Prompt> {
|
||||||
|
@ -60,12 +61,19 @@ export class PromptListPage extends TablePage<Prompt> {
|
||||||
return html`<li>${stage.name}</li>`;
|
return html`<li>${stage.name}</li>`;
|
||||||
})}`,
|
})}`,
|
||||||
html`
|
html`
|
||||||
<ak-modal-button href="${AdminURLManager.stagePrompts(`${item.pk}/update/`)}">
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
<span slot="submit">
|
||||||
|
${gettext("Update")}
|
||||||
|
</span>
|
||||||
|
<span slot="header">
|
||||||
|
${gettext("Update Prompt")}
|
||||||
|
</span>
|
||||||
|
<ak-stage-prompt-form slot="form" .prompt=${item}>
|
||||||
|
</ak-stage-prompt-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||||
${gettext("Edit")}
|
${gettext("Edit")}
|
||||||
</ak-spinner-button>
|
</button>
|
||||||
<div slot="modal"></div>
|
</ak-forms-modal>
|
||||||
</ak-modal-button>
|
|
||||||
<ak-forms-delete
|
<ak-forms-delete
|
||||||
.obj=${item}
|
.obj=${item}
|
||||||
objectLabel=${gettext("Prompt")}
|
objectLabel=${gettext("Prompt")}
|
||||||
|
@ -83,12 +91,19 @@ export class PromptListPage extends TablePage<Prompt> {
|
||||||
|
|
||||||
renderToolbar(): TemplateResult {
|
renderToolbar(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ak-modal-button href=${AdminURLManager.stagePrompts("create/")}>
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
<span slot="submit">
|
||||||
${gettext("Create")}
|
${gettext("Create")}
|
||||||
</ak-spinner-button>
|
</span>
|
||||||
<div slot="modal"></div>
|
<span slot="header">
|
||||||
</ak-modal-button>
|
${gettext("Create Prompt")}
|
||||||
|
</span>
|
||||||
|
<ak-stage-prompt-form slot="form">
|
||||||
|
</ak-stage-prompt-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||||
|
${gettext("Create")}
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
${super.renderToolbar()}
|
${super.renderToolbar()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ export class TokenListPage extends TablePage<Token> {
|
||||||
row(item: Token): TemplateResult[] {
|
row(item: Token): TemplateResult[] {
|
||||||
return [
|
return [
|
||||||
html`${item.identifier}`,
|
html`${item.identifier}`,
|
||||||
html`${item.user.username}`,
|
html`${item.user?.username}`,
|
||||||
html`${item.expiring ? "Yes" : "No"}`,
|
html`${item.expiring ? "Yes" : "No"}`,
|
||||||
html`${item.expiring ? item.expires?.toLocaleString() : "-"}`,
|
html`${item.expiring ? item.expires?.toLocaleString() : "-"}`,
|
||||||
html`
|
html`
|
||||||
|
|
|
@ -18,8 +18,8 @@ import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
import { until } from "lit-html/directives/until";
|
import { until } from "lit-html/directives/until";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
import "../../elements/Tabs";
|
import "../../elements/Tabs";
|
||||||
|
import "./tokens/UserTokenList";
|
||||||
import "./UserDetailsPage";
|
import "./UserDetailsPage";
|
||||||
import "./UserTokenList";
|
|
||||||
import "./settings/UserSettingsAuthenticatorTOTP";
|
import "./settings/UserSettingsAuthenticatorTOTP";
|
||||||
import "./settings/UserSettingsAuthenticatorStatic";
|
import "./settings/UserSettingsAuthenticatorStatic";
|
||||||
import "./settings/UserSettingsAuthenticatorWebAuthn";
|
import "./settings/UserSettingsAuthenticatorWebAuthn";
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { CoreApi, Token } from "authentik-api";
|
||||||
|
import { gettext } from "django";
|
||||||
|
import { customElement, property } from "lit-element";
|
||||||
|
import { html, TemplateResult } from "lit-html";
|
||||||
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
|
import { Form } from "../../../elements/forms/Form";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
import "../../../elements/forms/HorizontalFormElement";
|
||||||
|
|
||||||
|
@customElement("ak-user-token-form")
|
||||||
|
export class UserTokenForm extends Form<Token> {
|
||||||
|
|
||||||
|
@property({attribute: false})
|
||||||
|
token?: Token;
|
||||||
|
|
||||||
|
getSuccessMessage(): string {
|
||||||
|
if (this.token) {
|
||||||
|
return gettext("Successfully updated token.");
|
||||||
|
} else {
|
||||||
|
return gettext("Successfully created token.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send = (data: Token): Promise<Token> => {
|
||||||
|
if (this.token) {
|
||||||
|
return new CoreApi(DEFAULT_CONFIG).coreTokensUpdate({
|
||||||
|
identifier: this.token.identifier,
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new CoreApi(DEFAULT_CONFIG).coreTokensCreate({
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderForm(): TemplateResult {
|
||||||
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Identifier")}
|
||||||
|
?required=${true}
|
||||||
|
name="identifier">
|
||||||
|
<input type="text" value="${ifDefined(this.token?.identifier)}" class="pf-c-form-control" required>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${gettext("Description")}
|
||||||
|
?required=${true}
|
||||||
|
name="description">
|
||||||
|
<input type="text" value="${ifDefined(this.token?.description)}" class="pf-c-form-control" required>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,16 +1,18 @@
|
||||||
import { gettext } from "django";
|
import { gettext } from "django";
|
||||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
|
||||||
import { AKResponse } from "../../api/Client";
|
import { AKResponse } from "../../../api/Client";
|
||||||
|
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||||
|
|
||||||
import "../../elements/forms/DeleteForm";
|
import "../../../elements/forms/DeleteForm";
|
||||||
import "../../elements/buttons/ModalButton";
|
import "../../../elements/forms/ModalForm";
|
||||||
import "../../elements/buttons/Dropdown";
|
import "../../../elements/buttons/ModalButton";
|
||||||
import "../../elements/buttons/TokenCopyButton";
|
import "../../../elements/buttons/Dropdown";
|
||||||
import { Table, TableColumn } from "../../elements/table/Table";
|
import "../../../elements/buttons/TokenCopyButton";
|
||||||
import { PAGE_SIZE } from "../../constants";
|
import { Table, TableColumn } from "../../../elements/table/Table";
|
||||||
|
import { PAGE_SIZE } from "../../../constants";
|
||||||
import { CoreApi, Token } from "authentik-api";
|
import { CoreApi, Token } from "authentik-api";
|
||||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
import { AdminURLManager } from "../../api/legacy";
|
import "./UserTokenForm";
|
||||||
|
|
||||||
@customElement("ak-user-token-list")
|
@customElement("ak-user-token-list")
|
||||||
export class UserTokenList extends Table<Token> {
|
export class UserTokenList extends Table<Token> {
|
||||||
|
@ -39,14 +41,25 @@ export class UserTokenList extends Table<Token> {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return super.styles.concat(PFDescriptionList);
|
||||||
|
}
|
||||||
|
|
||||||
renderToolbar(): TemplateResult {
|
renderToolbar(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ak-modal-button href="/-/user/tokens/create/">
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
<span slot="submit">
|
||||||
${gettext("Create")}
|
${gettext("Create")}
|
||||||
</ak-spinner-button>
|
</span>
|
||||||
<div slot="modal"></div>
|
<span slot="header">
|
||||||
</ak-modal-button>
|
${gettext("Create Token")}
|
||||||
|
</span>
|
||||||
|
<ak-user-token-form slot="form">
|
||||||
|
</ak-user-token-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||||
|
${gettext("Create")}
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
${super.renderToolbar()}
|
${super.renderToolbar()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -61,7 +74,7 @@ export class UserTokenList extends Table<Token> {
|
||||||
<span class="pf-c-description-list__text">${gettext("User")}</span>
|
<span class="pf-c-description-list__text">${gettext("User")}</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="pf-c-description-list__description">
|
<dd class="pf-c-description-list__description">
|
||||||
<div class="pf-c-description-list__text">${item.user.username}</div>
|
<div class="pf-c-description-list__text">${item.user?.username}</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-description-list__group">
|
<div class="pf-c-description-list__group">
|
||||||
|
@ -90,12 +103,19 @@ export class UserTokenList extends Table<Token> {
|
||||||
return [
|
return [
|
||||||
html`${item.identifier}`,
|
html`${item.identifier}`,
|
||||||
html`
|
html`
|
||||||
<ak-modal-button href="${AdminURLManager.tokens(`${item.identifier}/update/`)}">
|
<ak-forms-modal>
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
<span slot="submit">
|
||||||
|
${gettext("Update")}
|
||||||
|
</span>
|
||||||
|
<span slot="header">
|
||||||
|
${gettext("Update Token")}
|
||||||
|
</span>
|
||||||
|
<ak-user-token-form slot="form" .token=${item}>
|
||||||
|
</ak-user-token-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||||
${gettext("Edit")}
|
${gettext("Edit")}
|
||||||
</ak-spinner-button>
|
</button>
|
||||||
<div slot="modal"></div>
|
</ak-forms-modal>
|
||||||
</ak-modal-button>
|
|
||||||
<ak-forms-delete
|
<ak-forms-delete
|
||||||
.obj=${item}
|
.obj=${item}
|
||||||
objectLabel=${gettext("Token")}
|
objectLabel=${gettext("Token")}
|
|
@ -28,7 +28,7 @@ import "./pages/system-tasks/SystemTaskListPage";
|
||||||
import "./pages/tokens/TokenListPage";
|
import "./pages/tokens/TokenListPage";
|
||||||
import "./pages/users/UserListPage";
|
import "./pages/users/UserListPage";
|
||||||
import "./pages/users/UserViewPage";
|
import "./pages/users/UserViewPage";
|
||||||
import "./pages/users/UserSettingsPage";
|
import "./pages/user-settings/UserSettingsPage";
|
||||||
|
|
||||||
export const ROUTES: Route[] = [
|
export const ROUTES: Route[] = [
|
||||||
// Prevent infinite Shell loops
|
// Prevent infinite Shell loops
|
||||||
|
|
|
@ -56,3 +56,16 @@ export function camelToSnake(key: string): string {
|
||||||
const result = key.replace(/([A-Z])/g, " $1");
|
const result = key.replace(/([A-Z])/g, " $1");
|
||||||
return result.split(" ").join("_").toLowerCase();
|
return result.split(" ").join("_").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function groupBy<T>(objects: T[], callback: (obj: T) => string): Array<[string, T[]]> {
|
||||||
|
const m = new Map<string, T[]>();
|
||||||
|
objects.forEach(obj => {
|
||||||
|
const group = callback(obj);
|
||||||
|
if (!m.has(group)) {
|
||||||
|
m.set(group, []);
|
||||||
|
}
|
||||||
|
const tProviders = m.get(group) || [];
|
||||||
|
tProviders.push(obj);
|
||||||
|
});
|
||||||
|
return Array.from(m);
|
||||||
|
}
|
||||||
|
|
Reference in New Issue