From e68352b09c5cafb7e8c8cc68e5e253b9d25c6c37 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 16 May 2020 19:55:59 +0200 Subject: [PATCH] admin: add flow-stage-bindings, add policy-bindings, add prompts --- .../admin/templates/administration/base.html | 68 +++++++++--- .../administration/flowstagebinding/list.html | 70 ++++++++++++ .../administration/policybinding/list.html | 66 ++++++++++++ .../templates/administration/stage/list.html | 6 -- .../administration/stage_prompt/list.html | 66 ++++++++++++ passbook/admin/urls.py | 80 ++++++++++++-- .../admin/views/{policy.py => policies.py} | 10 +- passbook/admin/views/policies_bindings.py | 100 ++++++++++++++++++ passbook/admin/views/stages_bindings.py | 87 +++++++++++++++ passbook/admin/views/stages_prompts.py | 77 ++++++++++++++ passbook/flows/migrations/0001_initial.py | 2 +- .../flows/migrations/0002_default_flows.py | 2 +- passbook/flows/models.py | 2 + passbook/policies/forms.py | 21 ++++ passbook/policies/models.py | 2 + passbook/stages/prompt/forms.py | 19 ++++ passbook/stages/user_write/stage.py | 7 +- 17 files changed, 651 insertions(+), 34 deletions(-) create mode 100644 passbook/admin/templates/administration/flowstagebinding/list.html create mode 100644 passbook/admin/templates/administration/policybinding/list.html create mode 100644 passbook/admin/templates/administration/stage_prompt/list.html rename passbook/admin/views/{policy.py => policies.py} (94%) create mode 100644 passbook/admin/views/policies_bindings.py create mode 100644 passbook/admin/views/stages_bindings.py create mode 100644 passbook/admin/views/stages_prompts.py diff --git a/passbook/admin/templates/administration/base.html b/passbook/admin/templates/administration/base.html index bad5812f3..c180c6f96 100644 --- a/passbook/admin/templates/administration/base.html +++ b/passbook/admin/templates/administration/base.html @@ -52,23 +52,63 @@ {% trans 'Property Mappings' %} -
  • - - {% trans 'Flows' %} +
  • + {% trans 'Flows' %} + + + +
    + +
  • -
  • - - {% trans 'Stages' %} - -
  • -
  • - - {% trans 'Policies' %} +
  • + {% trans 'Policies and Bindings' %} + + + +
    + +
  • +
    +

    + + {% trans 'Stage Bindings' %} +

    +

    {% trans "Bind existing Stages to Flows." %}

    +
    + +
    +
    +
    + + {% include 'partials/pagination.html' %} +
    + + + + + + + + + + + {% for binding in object_list %} + + + + + + + {% endfor %} + +
    {% trans 'Order' %}{% trans 'Name' %}{% trans 'Stage Type' %}
    + + {{ binding.order }} + + +
    +
    {{ binding.stage.name }}
    + + {% blocktrans with flow=binding.flow %} + Bound to {{ flow }}. + {% endblocktrans %} + +
    +
    + + {{ binding.stage }} + + + {% trans 'Edit' %} + {% trans 'Delete' %} +
    +
    + {% include 'partials/pagination.html' %} +
    +
    +
    +{% endblock %} diff --git a/passbook/admin/templates/administration/policybinding/list.html b/passbook/admin/templates/administration/policybinding/list.html new file mode 100644 index 000000000..7bcde3fed --- /dev/null +++ b/passbook/admin/templates/administration/policybinding/list.html @@ -0,0 +1,66 @@ +{% extends "administration/base.html" %} + +{% load i18n %} +{% load passbook_utils %} + +{% block content %} +
    +
    +

    + + {% trans 'Policy Bindings' %} +

    +

    {% trans "Bind existing Policies to Models accepting policies." %}

    +
    +
    +
    +
    +
    + + {% include 'partials/pagination.html' %} +
    + + + + + + + + + + {% for binding in object_list %} + + + + + + {% endfor %} + +
    {% trans 'Name' %}{% trans 'Type' %}
    +
    +
    {{ binding.name }}
    + {% if not binding.bindings.exists %} + + {% trans 'Warning: Policy is not assigned.' %} + {% else %} + + {% blocktrans with object_count=binding.bindings.all|length %}Assigned to {{ object_count }} objects.{% endblocktrans %} + {% endif %} +
    +
    + + {{ binding|verbose_name }} + + + {% trans 'Edit' %} + {% trans 'Delete' %} +
    +
    + {% include 'partials/pagination.html' %} +
    +
    +
    +{% endblock %} diff --git a/passbook/admin/templates/administration/stage/list.html b/passbook/admin/templates/administration/stage/list.html index 8318bdf18..4d1b4bb4d 100644 --- a/passbook/admin/templates/administration/stage/list.html +++ b/passbook/admin/templates/administration/stage/list.html @@ -40,7 +40,6 @@ {% trans 'Name' %} {% trans 'Flows' %} - {% trans 'Enabled' %} @@ -60,11 +59,6 @@ {% endfor %} - - - {{ stage.enabled }} - - {% trans 'Edit' %} {% trans 'Delete' %} diff --git a/passbook/admin/templates/administration/stage_prompt/list.html b/passbook/admin/templates/administration/stage_prompt/list.html new file mode 100644 index 000000000..9489af705 --- /dev/null +++ b/passbook/admin/templates/administration/stage_prompt/list.html @@ -0,0 +1,66 @@ +{% extends "administration/base.html" %} + +{% load i18n %} +{% load passbook_utils %} +{% load admin_reflection %} + +{% block content %} +
    +
    +

    + + {% trans 'Prompts' %} +

    +

    {% trans "Prompts that can be used for Prompt Stages." %}

    +
    +
    +
    +
    +
    + + {% include 'partials/pagination.html' %} +
    + + + + + + + + + + {% for prompt in object_list %} + + + + + + {% endfor %} + +
    {% trans 'Name' %}{% trans 'Flows' %}
    +
    +
    {{ prompt.field_key }}
    + {{ prompt|verbose_name }} +
    +
    +
      + {% for flow in prompt.flow_set.all %} +
    • {{ flow.slug }}
    • + {% endfor %} +
    +
    + {% trans 'Edit' %} + {% trans 'Delete' %} + {% get_links prompt as links %} + {% for name, href in links.items %} + {% trans name %} + {% endfor %} +
    +
    + {% include 'partials/pagination.html' %} +
    +
    +
    +{% endblock %} diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py index 0f3526713..ae1492778 100644 --- a/passbook/admin/urls.py +++ b/passbook/admin/urls.py @@ -10,11 +10,14 @@ from passbook.admin.views import ( groups, invitations, overview, - policy, + policies, + policies_bindings, property_mapping, providers, sources, stages, + stages_bindings, + stages_prompts, users, ) @@ -53,20 +56,43 @@ urlpatterns = [ name="source-delete", ), # Policies - path("policies/", policy.PolicyListView.as_view(), name="policies"), - path("policies/create/", policy.PolicyCreateView.as_view(), name="policy-create"), + path("policies/", policies.PolicyListView.as_view(), name="policies"), + path("policies/create/", policies.PolicyCreateView.as_view(), name="policy-create"), path( "policies//update/", - policy.PolicyUpdateView.as_view(), + policies.PolicyUpdateView.as_view(), name="policy-update", ), path( "policies//delete/", - policy.PolicyDeleteView.as_view(), + policies.PolicyDeleteView.as_view(), name="policy-delete", ), path( - "policies//test/", policy.PolicyTestView.as_view(), name="policy-test" + "policies//test/", + policies.PolicyTestView.as_view(), + name="policy-test", + ), + # Policy bindings + path( + "policies/bindings/", + policies_bindings.PolicyBindingListView.as_view(), + name="policies-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", + ), + path( + "policies/bindings//delete/", + policies_bindings.PolicyBindingDeleteView.as_view(), + name="policy-binding-delete", ), # Providers path("providers/", providers.ProviderListView.as_view(), name="providers"), @@ -98,6 +124,48 @@ urlpatterns = [ stages.StageDeleteView.as_view(), name="stage-delete", ), + # Stage bindings + path( + "stages/bindings/", + stages_bindings.StageBindingListView.as_view(), + name="stage-bindings", + ), + path( + "stages/bindings/create/", + stages_bindings.StageBindingCreateView.as_view(), + name="stage-binding-create", + ), + path( + "stages/bindings//update/", + stages_bindings.StageBindingUpdateView.as_view(), + name="stage-binding-update", + ), + path( + "stages/bindings//delete/", + stages_bindings.StageBindingDeleteView.as_view(), + name="stage-binding-delete", + ), + # Stage Prompts + path( + "stages/prompts/", + stages_prompts.PromptListView.as_view(), + name="stage-prompts", + ), + path( + "stages/prompts/create/", + stages_prompts.PromptCreateView.as_view(), + name="stage-prompt-create", + ), + path( + "stages/prompts//update/", + stages_prompts.PromptUpdateView.as_view(), + name="stage-prompt-update", + ), + path( + "stages/prompts//delete/", + stages_prompts.PromptDeleteView.as_view(), + name="stage-prompt-delete", + ), # Flows path("flows/", flows.FlowListView.as_view(), name="flows"), path("flows/create/", flows.FlowCreateView.as_view(), name="flow-create",), diff --git a/passbook/admin/views/policy.py b/passbook/admin/views/policies.py similarity index 94% rename from passbook/admin/views/policy.py rename to passbook/admin/views/policies.py index 5faa67c5f..f1a835b06 100644 --- a/passbook/admin/views/policy.py +++ b/passbook/admin/views/policies.py @@ -23,7 +23,7 @@ class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView): """Show list of all policies""" model = Policy - permission_required = "passbook_core.view_policy" + permission_required = "passbook_policies.view_policy" paginate_by = 10 ordering = "order" template_name = "administration/policy/list.html" @@ -47,7 +47,7 @@ class PolicyCreateView( """Create new Policy""" model = Policy - permission_required = "passbook_core.add_policy" + permission_required = "passbook_policies.add_policy" template_name = "generic/create.html" success_url = reverse_lazy("passbook_admin:policies") @@ -74,7 +74,7 @@ class PolicyUpdateView( """Update policy""" model = Policy - permission_required = "passbook_core.change_policy" + permission_required = "passbook_policies.change_policy" template_name = "generic/update.html" success_url = reverse_lazy("passbook_admin:policies") @@ -104,7 +104,7 @@ class PolicyDeleteView( """Delete policy""" model = Policy - permission_required = "passbook_core.delete_policy" + permission_required = "passbook_policies.delete_policy" template_name = "generic/delete.html" success_url = reverse_lazy("passbook_admin:policies") @@ -125,7 +125,7 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo model = Policy form_class = PolicyTestForm - permission_required = "passbook_core.view_policy" + permission_required = "passbook_policies.view_policy" template_name = "administration/policy/test.html" object = None diff --git a/passbook/admin/views/policies_bindings.py b/passbook/admin/views/policies_bindings.py new file mode 100644 index 000000000..139f6513a --- /dev/null +++ b/passbook/admin/views/policies_bindings.py @@ -0,0 +1,100 @@ +"""passbook PolicyBinding administration""" +from django.contrib import messages +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 ugettext as _ +from django.views.generic import DeleteView, ListView, UpdateView +from guardian.mixins import PermissionListMixin, PermissionRequiredMixin + +from passbook.lib.utils.reflection import path_to_class +from passbook.lib.views import CreateAssignPermView +from passbook.policies.forms import PolicyBindingForm +from passbook.policies.models import PolicyBinding + + +class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView): + """Show list of all policies""" + + model = PolicyBinding + permission_required = "passbook_policies.view_policybinding" + paginate_by = 10 + ordering = "order" + template_name = "administration/policybinding/list.html" + + +class PolicyBindingCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): + """Create new PolicyBinding""" + + model = PolicyBinding + permission_required = "passbook_policies.add_policybinding" + form_class = PolicyBindingForm + + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:policies") + success_message = _("Successfully created PolicyBinding") + + def get_context_data(self, **kwargs): + kwargs = super().get_context_data(**kwargs) + form_cls = self.get_form_class() + if hasattr(form_cls, "template_name"): + kwargs["base_template"] = form_cls.template_name + return kwargs + + +class PolicyBindingUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): + """Update policybinding""" + + model = PolicyBinding + permission_required = "passbook_policies.change_policybinding" + form_class = PolicyBindingForm + + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:policies") + success_message = _("Successfully updated PolicyBinding") + + def get_context_data(self, **kwargs): + kwargs = super().get_context_data(**kwargs) + form_cls = self.get_form_class() + if hasattr(form_cls, "template_name"): + kwargs["base_template"] = form_cls.template_name + return kwargs + + def get_form_class(self): + form_class_path = self.get_object().form + form_class = path_to_class(form_class_path) + return form_class + + def get_object(self, queryset=None): + return ( + PolicyBinding.objects.filter(pk=self.kwargs.get("pk")) + .select_subclasses() + .first() + ) + + +class PolicyBindingDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): + """Delete policybinding""" + + model = PolicyBinding + permission_required = "passbook_policies.delete_policybinding" + + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:policies") + success_message = _("Successfully deleted PolicyBinding") + + def delete(self, request, *args, **kwargs): + messages.success(self.request, self.success_message) + return super().delete(request, *args, **kwargs) diff --git a/passbook/admin/views/stages_bindings.py b/passbook/admin/views/stages_bindings.py new file mode 100644 index 000000000..1a12f8870 --- /dev/null +++ b/passbook/admin/views/stages_bindings.py @@ -0,0 +1,87 @@ +"""passbook StageBinding administration""" +from django.contrib import messages +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 ugettext as _ +from django.views.generic import DeleteView, ListView, UpdateView +from guardian.mixins import PermissionListMixin, PermissionRequiredMixin + +from passbook.flows.forms import FlowStageBindingForm +from passbook.flows.models import FlowStageBinding +from passbook.lib.views import CreateAssignPermView + + +class StageBindingListView(LoginRequiredMixin, PermissionListMixin, ListView): + """Show list of all flows""" + + model = FlowStageBinding + permission_required = "passbook_flows.view_flowstagebinding" + paginate_by = 10 + ordering = "order" + template_name = "administration/flowstagebinding/list.html" + + +class StageBindingCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): + """Create new StageBinding""" + + model = FlowStageBinding + permission_required = "passbook_flows.add_flowstagebinding" + form_class = FlowStageBindingForm + + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:flows") + success_message = _("Successfully created StageBinding") + + def get_context_data(self, **kwargs): + kwargs = super().get_context_data(**kwargs) + form_cls = self.get_form_class() + if hasattr(form_cls, "template_name"): + kwargs["base_template"] = form_cls.template_name + return kwargs + + +class StageBindingUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): + """Update FlowStageBinding""" + + model = FlowStageBinding + permission_required = "passbook_flows.change_flowstagebinding" + form_class = FlowStageBindingForm + + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:flows") + success_message = _("Successfully updated StageBinding") + + def get_context_data(self, **kwargs): + kwargs = super().get_context_data(**kwargs) + form_cls = self.get_form_class() + if hasattr(form_cls, "template_name"): + kwargs["base_template"] = form_cls.template_name + return kwargs + + +class StageBindingDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): + """Delete FlowStageBinding""" + + model = FlowStageBinding + permission_required = "passbook_flows.delete_flowstagebinding" + + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:flows") + success_message = _("Successfully deleted FlowStageBinding") + + def delete(self, request, *args, **kwargs): + messages.success(self.request, self.success_message) + return super().delete(request, *args, **kwargs) diff --git a/passbook/admin/views/stages_prompts.py b/passbook/admin/views/stages_prompts.py new file mode 100644 index 000000000..4d0c9a1e0 --- /dev/null +++ b/passbook/admin/views/stages_prompts.py @@ -0,0 +1,77 @@ +"""passbook Prompt administration""" +from django.contrib import messages +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 ugettext as _ +from django.views.generic import DeleteView, ListView, UpdateView +from guardian.mixins import PermissionListMixin, PermissionRequiredMixin + +from passbook.lib.views import CreateAssignPermView +from passbook.stages.prompt.forms import PromptAdminForm +from passbook.stages.prompt.models import Prompt + + +class PromptListView(LoginRequiredMixin, PermissionListMixin, ListView): + """Show list of all prompts""" + + model = Prompt + permission_required = "passbook_stages_prompt.view_prompt" + ordering = "field_key" + paginate_by = 40 + template_name = "administration/stage_prompt/list.html" + + +class PromptCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): + """Create new Prompt""" + + model = Prompt + form_class = PromptAdminForm + permission_required = "passbook_stages_prompt.add_prompt" + + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:prompts") + success_message = _("Successfully created Prompt") + + def get_context_data(self, **kwargs): + kwargs["type"] = "Prompt" + return super().get_context_data(**kwargs) + + +class PromptUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): + """Update prompt""" + + model = Prompt + form_class = PromptAdminForm + permission_required = "passbook_stages_prompt.change_prompt" + + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:prompts") + success_message = _("Successfully updated Prompt") + + +class PromptDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): + """Delete prompt""" + + model = Prompt + permission_required = "passbook_stages_prompt.delete_prompt" + + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:prompts") + success_message = _("Successfully deleted Prompt") + + def delete(self, request, *args, **kwargs): + messages.success(self.request, self.success_message) + return super().delete(request, *args, **kwargs) diff --git a/passbook/flows/migrations/0001_initial.py b/passbook/flows/migrations/0001_initial.py index 5253b8b6c..d86acc19c 100644 --- a/passbook/flows/migrations/0001_initial.py +++ b/passbook/flows/migrations/0001_initial.py @@ -11,7 +11,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - # ("passbook_policies", "0001_initial"), + ("passbook_policies", "0001_initial"), ] operations = [ diff --git a/passbook/flows/migrations/0002_default_flows.py b/passbook/flows/migrations/0002_default_flows.py index e146b2532..21d61d338 100644 --- a/passbook/flows/migrations/0002_default_flows.py +++ b/passbook/flows/migrations/0002_default_flows.py @@ -76,7 +76,7 @@ def create_default_invalidation_flow( return if not UserLogoutStage.objects.using(db_alias).exists(): - UserLogoutStage.objects.using(db_alias).create(name="authentication") + UserLogoutStage.objects.using(db_alias).create(name="logout") flow = Flow.objects.using(db_alias).create( name="default-invalidation-flow", diff --git a/passbook/flows/models.py b/passbook/flows/models.py index 11735de39..e21101808 100644 --- a/passbook/flows/models.py +++ b/passbook/flows/models.py @@ -89,6 +89,8 @@ class FlowStageBinding(PolicyBindingModel, UUIDModel): order = models.IntegerField() + objects = InheritanceManager() + def __str__(self) -> str: return f"Flow Stage Binding #{self.order} {self.flow} -> {self.stage}" diff --git a/passbook/policies/forms.py b/passbook/policies/forms.py index 9c9e4d865..f8752f951 100644 --- a/passbook/policies/forms.py +++ b/passbook/policies/forms.py @@ -1,4 +1,25 @@ """General fields""" +from django import forms + +from passbook.policies.models import PolicyBinding, PolicyBindingModel GENERAL_FIELDS = ["name", "negate", "order", "timeout"] GENERAL_SERIALIZER_FIELDS = ["pk", "name", "negate", "order", "timeout"] + + +class PolicyBindingForm(forms.ModelForm): + """Form to edit Policy to PolicyBindingModel Binding""" + + target = forms.ModelChoiceField( + queryset=PolicyBindingModel.objects.all().select_subclasses() + ) + + class Meta: + + model = PolicyBinding + fields = [ + "enabled", + "policy", + "target", + "order", + ] diff --git a/passbook/policies/models.py b/passbook/policies/models.py index d44e1d39f..dd1045435 100644 --- a/passbook/policies/models.py +++ b/passbook/policies/models.py @@ -15,6 +15,8 @@ class PolicyBindingModel(models.Model): "Policy", through="PolicyBinding", related_name="bindings", blank=True ) + objects = InheritanceManager() + class Meta: verbose_name = _("Policy Binding Model") diff --git a/passbook/stages/prompt/forms.py b/passbook/stages/prompt/forms.py index ac520c2da..14830f36d 100644 --- a/passbook/stages/prompt/forms.py +++ b/passbook/stages/prompt/forms.py @@ -19,6 +19,25 @@ class PromptStageForm(forms.ModelForm): } +class PromptAdminForm(forms.ModelForm): + """Form to edit Prompt instances for admins""" + + class Meta: + + model = Prompt + fields = [ + "field_key", + "label", + "type", + "required", + "placeholder", + ] + widgets = { + "label": forms.TextInput(), + "placeholder": forms.TextInput(), + } + + class PromptForm(forms.Form): """Dynamically created form based on PromptStage""" diff --git a/passbook/stages/user_write/stage.py b/passbook/stages/user_write/stage.py index 20c689064..1ffc7ba35 100644 --- a/passbook/stages/user_write/stage.py +++ b/passbook/stages/user_write/stage.py @@ -29,12 +29,17 @@ class UserWriteStageView(AuthenticationStage): user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] for key, value in data.items(): setter_name = f"set_{key}" + # Check if user has a setter for this key, like set_password if hasattr(user, setter_name): setter = getattr(user, setter_name) if callable(setter): setter(value) - else: + # User has this key already + elif hasattr(user, key): setattr(user, key, value) + # Otherwise we just save it as custom attribute + else: + user.attributes[key] = value user.save() LOGGER.debug( "Updated existing user", user=user, flow_slug=self.executor.flow.slug,