diff --git a/authentik/admin/templates/administration/flow/list.html b/authentik/admin/templates/administration/flow/list.html
index f1a27a0d3..5bb2dfcd4 100644
--- a/authentik/admin/templates/administration/flow/list.html
+++ b/authentik/admin/templates/administration/flow/list.html
@@ -53,10 +53,10 @@
{% for flow in object_list %}
-
+
|
diff --git a/authentik/admin/templates/administration/source/list.html b/authentik/admin/templates/administration/source/list.html
index a4a01442e..e9af3c44e 100644
--- a/authentik/admin/templates/administration/source/list.html
+++ b/authentik/admin/templates/administration/source/list.html
@@ -63,12 +63,12 @@
{% for source in object_list %}
-
+
|
diff --git a/authentik/admin/tests/__init__.py b/authentik/admin/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/authentik/admin/tests.py b/authentik/admin/tests/test_generated.py
similarity index 100%
rename from authentik/admin/tests.py
rename to authentik/admin/tests/test_generated.py
diff --git a/authentik/admin/tests/test_policy_binding.py b/authentik/admin/tests/test_policy_binding.py
new file mode 100644
index 000000000..5a964bf81
--- /dev/null
+++ b/authentik/admin/tests/test_policy_binding.py
@@ -0,0 +1,26 @@
+"""admin tests"""
+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
+
+
+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_param(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})
diff --git a/authentik/admin/tests/test_stage_bindings.py b/authentik/admin/tests/test_stage_bindings.py
new file mode 100644
index 000000000..169048e4f
--- /dev/null
+++ b/authentik/admin/tests/test_stage_bindings.py
@@ -0,0 +1,26 @@
+"""admin tests"""
+from django.test import TestCase
+from django.test.client import RequestFactory
+
+from authentik.admin.views.stages_bindings import StageBindingCreateView
+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_param(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})
diff --git a/authentik/admin/views/policies_bindings.py b/authentik/admin/views/policies_bindings.py
index 9f7a8f97a..be19eb815 100644
--- a/authentik/admin/views/policies_bindings.py
+++ b/authentik/admin/views/policies_bindings.py
@@ -1,10 +1,12 @@
"""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 QuerySet
+from django.db.models import Max, QuerySet
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
@@ -18,7 +20,7 @@ from authentik.admin.views.utils import (
)
from authentik.lib.views import CreateAssignPermView
from authentik.policies.forms import PolicyBindingForm
-from authentik.policies.models import PolicyBinding
+from authentik.policies.models import PolicyBinding, PolicyBindingModel
class PolicyBindingListView(
@@ -67,6 +69,22 @@ class PolicyBindingCreateView(
success_url = reverse_lazy("authentik_admin:policies-bindings")
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,
diff --git a/authentik/admin/views/stages_bindings.py b/authentik/admin/views/stages_bindings.py
index d048764e8..7e416d19f 100644
--- a/authentik/admin/views/stages_bindings.py
+++ b/authentik/admin/views/stages_bindings.py
@@ -1,9 +1,12 @@
"""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 ListView, UpdateView
@@ -15,7 +18,7 @@ from authentik.admin.views.utils import (
UserPaginateListMixin,
)
from authentik.flows.forms import FlowStageBindingForm
-from authentik.flows.models import FlowStageBinding
+from authentik.flows.models import Flow, FlowStageBinding
from authentik.lib.views import CreateAssignPermView
@@ -47,6 +50,20 @@ class StageBindingCreateView(
success_url = reverse_lazy("authentik_admin:stage-bindings")
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,
diff --git a/authentik/core/api/sources.py b/authentik/core/api/sources.py
index e19acf27e..120dd8ac1 100644
--- a/authentik/core/api/sources.py
+++ b/authentik/core/api/sources.py
@@ -15,6 +15,12 @@ class SourceSerializer(ModelSerializer):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace("source", "")
+ def to_representation(self, instance: Source):
+ # pyright: reportGeneralTypeIssues=false
+ if instance.__class__ == Source:
+ return super().to_representation(instance)
+ return instance.serializer(instance=instance).data
+
class Meta:
model = Source
@@ -26,6 +32,7 @@ class SourceViewSet(ReadOnlyModelViewSet):
queryset = Source.objects.all()
serializer_class = SourceSerializer
+ lookup_field = "slug"
def get_queryset(self):
return Source.objects.select_subclasses()
diff --git a/authentik/core/models.py b/authentik/core/models.py
index b55acac51..d1fbd9d67 100644
--- a/authentik/core/models.py
+++ b/authentik/core/models.py
@@ -20,7 +20,7 @@ from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.signals import password_changed
from authentik.core.types import UILoginButton
from authentik.flows.models import Flow
-from authentik.lib.models import CreatedUpdatedModel
+from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.policies.models import PolicyBindingModel
LOGGER = get_logger()
@@ -200,7 +200,7 @@ class Application(PolicyBindingModel):
verbose_name_plural = _("Applications")
-class Source(PolicyBindingModel):
+class Source(SerializerModel, PolicyBindingModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
name = models.TextField(help_text=_("Source's display Name."))
diff --git a/authentik/core/templates/generic/delete.html b/authentik/core/templates/generic/delete.html
index 2cfab27f2..234dab40e 100644
--- a/authentik/core/templates/generic/delete.html
+++ b/authentik/core/templates/generic/delete.html
@@ -20,7 +20,7 @@
{% endblock %}
diff --git a/authentik/core/templates/login/base.html b/authentik/core/templates/login/base.html
index 401c5c1cd..c029b284c 100644
--- a/authentik/core/templates/login/base.html
+++ b/authentik/core/templates/login/base.html
@@ -1,7 +1,7 @@
{% load static %}
{% load i18n %}
-
+
diff --git a/authentik/core/templates/login/base_full.html b/authentik/core/templates/login/base_full.html
index bae02d2b7..cb85bdd94 100644
--- a/authentik/core/templates/login/base_full.html
+++ b/authentik/core/templates/login/base_full.html
@@ -28,7 +28,7 @@
-
+
@@ -56,21 +70,59 @@ export abstract class Table extends LitElement {
| `;
}
+ renderEmpty(inner?: TemplateResult): TemplateResult {
+ return html`
+
+
+
+ ${inner ? inner : html` `}
+
+ |
+
+ `;
+ }
+
private renderRows(): TemplateResult[] | undefined {
if (!this.data) {
return;
}
- return this.data.results.map((item) => {
- const fullRow = [""].concat(
- this.row(item).map((col) => {
- return `${col} | `;
- })
- );
- fullRow.push(" ");
- return htmlFromString(...fullRow);
+ if (this.data.pagination.count === 0) {
+ return [this.renderEmpty()];
+ }
+ return this.data.results.map((item: T, idx: number) => {
+ if ((this.expandedRows.length - 1) < idx) {
+ this.expandedRows[idx] = false;
+ }
+ return html`
+
+ ${this.expandable ? html`
+
+ | ` : html``}
+ ${this.row(item).map((col) => {
+ return html`${col} | `;
+ })}
+
+
+ |
+ ${this.renderExpanded(item)}
+
+ `;
});
}
+ renderToolbar(): TemplateResult {
+ return html` `;
+ }
+
renderTable(): TemplateResult {
if (!this.data) {
this.fetch();
@@ -78,12 +130,7 @@ export abstract class Table extends LitElement {
return html`
-
+
+ ${this.expandable ? html`` : html``}
${this.columns().map((col) => html` | ${gettext(col)} | `)}
-
- ${this.data ? this.renderRows() : this.renderLoading()}
-
+ ${this.data ? this.renderRows() : this.renderLoading()}
|