diff --git a/authentik/admin/tests/test_policy_binding.py b/authentik/admin/tests/test_policy_binding.py deleted file mode 100644 index 034cfd1d1..000000000 --- a/authentik/admin/tests/test_policy_binding.py +++ /dev/null @@ -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, - ) - ) diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index 0135dde93..73615509d 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -4,7 +4,6 @@ from django.urls import path from authentik.admin.views import ( outposts_service_connections, policies, - policies_bindings, property_mappings, providers, sources, @@ -27,17 +26,6 @@ urlpatterns = [ policies.PolicyUpdateView.as_view(), name="policy-update", ), - # Policy bindings - path( - "policies/bindings/create/", - policies_bindings.PolicyBindingCreateView.as_view(), - name="policy-binding-create", - ), - path( - "policies/bindings//update/", - policies_bindings.PolicyBindingUpdateView.as_view(), - name="policy-binding-update", - ), # Providers path( "providers/create/", diff --git a/authentik/admin/views/policies_bindings.py b/authentik/admin/views/policies_bindings.py deleted file mode 100644 index fbbd33f29..000000000 --- a/authentik/admin/views/policies_bindings.py +++ /dev/null @@ -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") diff --git a/authentik/flows/transfer/common.py b/authentik/flows/transfer/common.py index 30b3b3bbb..eee22f10e 100644 --- a/authentik/flows/transfer/common.py +++ b/authentik/flows/transfer/common.py @@ -29,6 +29,9 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]: for to_remove_name in to_remove: if to_remove_name in data: data.pop(to_remove_name) + for key in list(data.keys()): + if key.endswith("_obj"): + data.pop(key) return data diff --git a/authentik/policies/api/bindings.py b/authentik/policies/api/bindings.py index 8d08bd5fd..a10f1d968 100644 --- a/authentik/policies/api/bindings.py +++ b/authentik/policies/api/bindings.py @@ -35,7 +35,7 @@ class PolicyBindingModelForeignKey(PrimaryKeyRelatedField): # checks the PK of PolicyBindingModel (for example), # but we get given the Primary Key of the inheriting class for model in self.get_queryset().select_subclasses().all(): - if str(model.pk) == data: + if str(model.pk) == str(data): return model # as a fallback we still try a direct lookup return self.get_queryset().get_subclass(pk=data) @@ -82,7 +82,13 @@ class PolicyBindingSerializer(ModelSerializer): def validate(self, data: OrderedDict) -> OrderedDict: """Check that either policy, group or user is set.""" - count = sum([bool(data["policy"]), bool(data["group"]), bool(data["user"])]) + 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: diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts index 3692be40e..bd6a9dc33 100644 --- a/web/src/api/legacy.ts +++ b/web/src/api/legacy.ts @@ -4,10 +4,6 @@ export class AdminURLManager { return `/administration/policies/${rest}`; } - static policyBindings(rest: string): string { - return `/administration/policies/bindings/${rest}`; - } - static providers(rest: string): string { return `/administration/providers/${rest}`; } diff --git a/web/src/pages/policies/BoundPoliciesList.ts b/web/src/pages/policies/BoundPoliciesList.ts index 373ebc848..e8b461a9f 100644 --- a/web/src/pages/policies/BoundPoliciesList.ts +++ b/web/src/pages/policies/BoundPoliciesList.ts @@ -16,6 +16,9 @@ import { AdminURLManager } from "../../api/legacy"; import "../../elements/forms/ModalForm"; import "../groups/GroupForm"; +import "../users/UserForm"; +import "./PolicyBindingForm"; +import { ifDefined } from "lit-html/directives/if-defined"; @customElement("ak-bound-policies-list") export class BoundPoliciesList extends Table { @@ -43,11 +46,11 @@ export class BoundPoliciesList extends Table { getPolicyUserGroupRow(item: PolicyBinding): string { if (item.policy) { - return gettext(`Policy ${item.policy.name}`); + return gettext(`Policy ${item.policyObj?.name}`); } else if (item.group) { - return gettext(`Group ${item.group.name}`); + return gettext(`Group ${item.groupObj?.name}`); } else if (item.user) { - return gettext(`User ${item.user.name}`); + return gettext(`User ${item.userObj?.name}`); } else { return gettext(""); } @@ -55,7 +58,7 @@ export class BoundPoliciesList extends Table { getObjectEditButton(item: PolicyBinding): TemplateResult { if (item.policy) { - return html` + return html` ${gettext("Edit Policy")} @@ -69,19 +72,26 @@ export class BoundPoliciesList extends Table { ${gettext("Update Group")} - + `; } else if (item.user) { - return html` - - ${gettext("Edit User")} - -
-
`; + return html` + + ${gettext("Update")} + + + ${gettext("Update User")} + + + + + `; } else { return html``; } @@ -95,12 +105,19 @@ export class BoundPoliciesList extends Table { html`${item.timeout}`, html` ${this.getObjectEditButton(item)} - - + + + ${gettext("Update")} + + + ${gettext("Update Binding")} + + + + + { ${gettext("No policies are currently bound to this object.")}
- - - ${gettext("Bind Policy")} - -
-
+ + + ${gettext("Create")} + + + ${gettext("Create Binding")} + + + + +
`); } @@ -154,12 +178,19 @@ export class BoundPoliciesList extends Table { }), html``)} - - - ${gettext("Bind Policy")} - -
-
+ + + ${gettext("Create")} + + + ${gettext("Create Binding")} + + + + + ${super.renderToolbar()} `; } diff --git a/web/src/pages/policies/PolicyBindingForm.ts b/web/src/pages/policies/PolicyBindingForm.ts new file mode 100644 index 000000000..eced5877c --- /dev/null +++ b/web/src/pages/policies/PolicyBindingForm.ts @@ -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 { + + @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 { + return form; + } + + send = (data: PolicyBinding): Promise => { + 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(policies, (p => p.verboseName || "")).map(([group, policies]) => { + return html` + ${policies.map(p => { + const selected = (this.binding?.policy === p.pk); + return html``; + })} + `; + })} + `; + } + + getOrder(): Promise { + 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`
+ + + + + + + + + + + +
+ + +
+
+ + + + + + +
`; + } + +}