+
+
+
+ {% trans 'Stage Bindings' %}
+
+
{% trans "Bind existing Stages to Flows." %}
+
+
+
+
+
+
+
+
+ {% trans 'Order' %} |
+ {% trans 'Name' %} |
+ {% trans 'Stage Type' %} |
+ |
+
+
+
+ {% for binding in object_list %}
+
+
+
+ {{ binding.order }}
+
+ |
+
+
+ {{ binding.stage.name }}
+
+ {% blocktrans with flow=binding.flow %}
+ Bound to {{ flow }}.
+ {% endblocktrans %}
+
+
+ |
+
+
+ {{ binding.stage }}
+
+ |
+
+ {% trans 'Edit' %}
+ {% trans 'Delete' %}
+ |
+
+ {% endfor %}
+
+
+
+ {% 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." %}
+
+
+
+
+
+
+
+
+ {% trans 'Name' %} |
+ {% trans 'Type' %} |
+ |
+
+
+
+ {% for binding in object_list %}
+
+
+
+ {{ 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' %}
+ |
+
+ {% endfor %}
+
+
+
+ {% 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." %}
+
+
+
+
+
+
+
+
+ {% trans 'Name' %} |
+ {% trans 'Flows' %} |
+ |
+
+
+
+ {% for prompt in object_list %}
+
+
+
+ {{ prompt.field_key }}
+ {{ prompt|verbose_name }}
+
+ |
+
+
+ |
+
+ {% trans 'Edit' %}
+ {% trans 'Delete' %}
+ {% get_links prompt as links %}
+ {% for name, href in links.items %}
+ {% trans name %}
+ {% endfor %}
+ |
+
+ {% 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,
|