From 2d7e8f1b507890d7d29b816205ba8cb3b10d8d88 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 8 Mar 2019 15:49:45 +0100 Subject: [PATCH 1/5] add group administration --- .../admin/templates/administration/base.html | 69 ++++++++------- .../templates/administration/group/list.html | 45 ++++++++++ .../templates/administration/groups/list.html | 83 ------------------- passbook/admin/urls.py | 5 ++ passbook/admin/views/groups.py | 51 +++++++++++- passbook/core/forms/groups.py | 30 +++++++ .../migrations/0017_auto_20190308_1417.py | 25 ++++++ passbook/core/models.py | 4 +- passbook/core/settings.py | 1 + 9 files changed, 195 insertions(+), 118 deletions(-) create mode 100644 passbook/admin/templates/administration/group/list.html delete mode 100644 passbook/admin/templates/administration/groups/list.html create mode 100644 passbook/core/forms/groups.py create mode 100644 passbook/core/migrations/0017_auto_20190308_1417.py diff --git a/passbook/admin/templates/administration/base.html b/passbook/admin/templates/administration/base.html index fadc37d70..d33970b78 100644 --- a/passbook/admin/templates/administration/base.html +++ b/passbook/admin/templates/administration/base.html @@ -5,35 +5,44 @@ {% block nav_secondary %} {% endblock %} diff --git a/passbook/admin/templates/administration/group/list.html b/passbook/admin/templates/administration/group/list.html new file mode 100644 index 000000000..4a87521b0 --- /dev/null +++ b/passbook/admin/templates/administration/group/list.html @@ -0,0 +1,45 @@ +{% extends "administration/base.html" %} + +{% load i18n %} +{% load utils %} + +{% block title %} +{% title %} +{% endblock %} + +{% block content %} +
+

{% trans "Groups" %}

+ {% trans "Group users together and give them permissions based on the membership." %} +
+ + {% trans 'Create...' %} + +
+ + + + + + + + + + + {% for group in object_list %} + + + + + + + {% endfor %} + +
{% trans 'Name' %}{% trans 'Parent' %}{% trans 'Members' %}
{{ group.name }}{{ group.parent }}{{ group.user_set.all|length }} + {% trans 'Edit' %} + {% trans 'Delete' %} +
+
+{% endblock %} diff --git a/passbook/admin/templates/administration/groups/list.html b/passbook/admin/templates/administration/groups/list.html deleted file mode 100644 index b8df11dc8..000000000 --- a/passbook/admin/templates/administration/groups/list.html +++ /dev/null @@ -1,83 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load static %} -{% load utils %} - -{% block head %} -{{ block.super }} - -{% endblock %} - -{% block scripts %} -{{ block.super }} - - -{% endblock %} - -{% block title %} -{% title %} -{% endblock %} - -{% block content %} -
-
-
-
-
-

{% trans "Invitations" %}

- - {% trans 'Create...' %} - -
- - - - - - - - - - {% for invitation in object_list %} - - - - - - {% endfor %} - -
{% trans 'Expiry' %}{% trans 'Link' %}
{{ invitation.expires|default:"Never" }} -
{{ invitation.link }}
-
- {% - trans 'Delete' %} -
-
-{% endblock %} diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py index e2d3a6236..deb4472bd 100644 --- a/passbook/admin/urls.py +++ b/passbook/admin/urls.py @@ -58,6 +58,11 @@ urlpatterns = [ users.UserDeleteView.as_view(), name='user-delete'), path('users//reset/', users.UserPasswordResetView.as_view(), name='user-password-reset'), + # Groups + path('group/', groups.GroupListView.as_view(), name='group'), + path('group/create/', groups.GroupCreateView.as_view(), name='group-create'), + path('group//update/', groups.GroupUpdateView.as_view(), name='group-update'), + path('group//delete/', groups.GroupDeleteView.as_view(), name='group-delete'), # Audit Log path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'), # Groups diff --git a/passbook/admin/views/groups.py b/passbook/admin/views/groups.py index 629ed4aa0..1e120669d 100644 --- a/passbook/admin/views/groups.py +++ b/passbook/admin/views/groups.py @@ -1,12 +1,57 @@ """passbook Group administration""" -from django.views.generic import ListView +from django.contrib import messages +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 CreateView, DeleteView, ListView, UpdateView from passbook.admin.mixins import AdminRequiredMixin +from passbook.core.forms.groups import GroupForm from passbook.core.models import Group class GroupListView(AdminRequiredMixin, ListView): - """Show list of all invitations""" + """Show list of all groups""" model = Group - template_name = 'administration/groups/list.html' + ordering = 'name' + template_name = 'administration/group/list.html' + + +class GroupCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): + """Create new Group""" + + form_class = GroupForm + + template_name = 'generic/create.html' + success_url = reverse_lazy('passbook_admin:groups') + success_message = _('Successfully created Group') + + def get_context_data(self, **kwargs): + kwargs['type'] = 'Group' + return super().get_context_data(**kwargs) + + +class GroupUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView): + """Update group""" + + model = Group + form_class = GroupForm + + template_name = 'generic/update.html' + success_url = reverse_lazy('passbook_admin:groups') + success_message = _('Successfully updated Group') + + +class GroupDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): + """Delete group""" + + model = Group + + template_name = 'generic/delete.html' + success_url = reverse_lazy('passbook_admin:groups') + success_message = _('Successfully deleted Group') + + def delete(self, request, *args, **kwargs): + messages.success(self.request, self.success_message) + return super().delete(request, *args, **kwargs) diff --git a/passbook/core/forms/groups.py b/passbook/core/forms/groups.py new file mode 100644 index 000000000..76b10b667 --- /dev/null +++ b/passbook/core/forms/groups.py @@ -0,0 +1,30 @@ +"""passbook Core Group forms""" +from django import forms + +from passbook.core.models import Group, User + + +class GroupForm(forms.ModelForm): + """Group Form""" + + members = forms.ModelMultipleChoiceField(User.objects.all(), required=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + self.initial['members'] = self.instance.user_set.values_list('pk', flat=True) + + def save(self, *args, **kwargs): + instance = super().save(*args, **kwargs) + if instance.pk: + instance.user_set.clear() + instance.user_set.add(*self.cleaned_data['users']) + return instance + + class Meta: + + model = Group + fields = ['name', 'parent', 'members', 'tags'] + widgets = { + 'name': forms.TextInput(), + } diff --git a/passbook/core/migrations/0017_auto_20190308_1417.py b/passbook/core/migrations/0017_auto_20190308_1417.py new file mode 100644 index 000000000..d32d6fa48 --- /dev/null +++ b/passbook/core/migrations/0017_auto_20190308_1417.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1.7 on 2019-03-08 14:17 + +import django.contrib.postgres.fields.hstore +from django.contrib.postgres.operations import HStoreExtension +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('passbook_core', '0016_auto_20190227_1355'), + ] + + operations = [ + migrations.RemoveField( + model_name='group', + name='extra_data', + ), + HStoreExtension(), + migrations.AddField( + model_name='group', + name='tags', + field=django.contrib.postgres.fields.hstore.HStoreField(default=dict), + ), + ] diff --git a/passbook/core/models.py b/passbook/core/models.py index 2f6d526d8..25608b67a 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -8,7 +8,7 @@ from typing import Tuple, Union from uuid import uuid4 from django.contrib.auth.models import AbstractUser -from django.contrib.postgres.fields import ArrayField +from django.contrib.postgres.fields import ArrayField, HStoreField from django.db import models from django.urls import reverse_lazy from django.utils.timezone import now @@ -31,7 +31,7 @@ class Group(UUIDModel): name = models.CharField(_('name'), max_length=80) parent = models.ForeignKey('Group', blank=True, null=True, on_delete=models.SET_NULL, related_name='children') - extra_data = models.TextField(blank=True) + tags = HStoreField(default=dict) def __str__(self): return "Group %s" % self.name diff --git a/passbook/core/settings.py b/passbook/core/settings.py index 580c54f6f..495dd9282 100644 --- a/passbook/core/settings.py +++ b/passbook/core/settings.py @@ -60,6 +60,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.postgres', 'rest_framework', 'drf_yasg', 'raven.contrib.django.raven_compat', From 2b8c2b23466eea7f25e59621ade13076247e17bd Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 10 Mar 2019 18:06:06 +0100 Subject: [PATCH 2/5] use Django's Admin FilteredSelectMultiple for Group Membership --- passbook/admin/templates/generic/form.html | 10 + passbook/core/forms/groups.py | 6 +- ...308_1417.py => 0019_auto_20190310_1615.py} | 4 +- passbook/core/static/css/passbook.css | 174 ++++++++++++++++++ 4 files changed, 190 insertions(+), 4 deletions(-) rename passbook/core/migrations/{0017_auto_20190308_1417.py => 0019_auto_20190310_1615.py} (83%) diff --git a/passbook/admin/templates/generic/form.html b/passbook/admin/templates/generic/form.html index 88bbec856..13cd7d8df 100644 --- a/passbook/admin/templates/generic/form.html +++ b/passbook/admin/templates/generic/form.html @@ -2,10 +2,20 @@ {% load i18n %} {% load utils %} +{% load static %} {% block head %} {{ block.super }} {{ form.media.css }} + + + + + + + + + {% endblock %} {% block content %} diff --git a/passbook/core/forms/groups.py b/passbook/core/forms/groups.py index 76b10b667..4f2052623 100644 --- a/passbook/core/forms/groups.py +++ b/passbook/core/forms/groups.py @@ -2,12 +2,14 @@ from django import forms from passbook.core.models import Group, User +from django.contrib.admin.widgets import FilteredSelectMultiple class GroupForm(forms.ModelForm): """Group Form""" - members = forms.ModelMultipleChoiceField(User.objects.all(), required=False) + members = forms.ModelMultipleChoiceField( + User.objects.all(), required=False, widget=FilteredSelectMultiple('users', False)) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -18,7 +20,7 @@ class GroupForm(forms.ModelForm): instance = super().save(*args, **kwargs) if instance.pk: instance.user_set.clear() - instance.user_set.add(*self.cleaned_data['users']) + instance.user_set.add(*self.cleaned_data['members']) return instance class Meta: diff --git a/passbook/core/migrations/0017_auto_20190308_1417.py b/passbook/core/migrations/0019_auto_20190310_1615.py similarity index 83% rename from passbook/core/migrations/0017_auto_20190308_1417.py rename to passbook/core/migrations/0019_auto_20190310_1615.py index d32d6fa48..8b59ed40e 100644 --- a/passbook/core/migrations/0017_auto_20190308_1417.py +++ b/passbook/core/migrations/0019_auto_20190310_1615.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1.7 on 2019-03-08 14:17 +# Generated by Django 2.1.7 on 2019-03-10 16:15 import django.contrib.postgres.fields.hstore from django.contrib.postgres.operations import HStoreExtension @@ -8,7 +8,7 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('passbook_core', '0016_auto_20190227_1355'), + ('passbook_core', '0018_provider_property_mappings'), ] operations = [ diff --git a/passbook/core/static/css/passbook.css b/passbook/core/static/css/passbook.css index 86711d8b1..ca643f80d 100644 --- a/passbook/core/static/css/passbook.css +++ b/passbook/core/static/css/passbook.css @@ -21,3 +21,177 @@ .dynamic-array-widget .remove:hover { cursor: pointer; } + +/* Selector */ + +.selector { + display: flex; + width: 100%; + height: 45vh; +} + +.selector .selector-filter { + display: flex; + align-items: center; +} + +.selector .selector-filter label { + margin: 0 8px 0 0; +} + +.selector .selector-filter input { + width: auto; + min-height: 0; + flex: 1 1; +} + +.selector-available, .selector-chosen { + width: auto; + flex: 1 1; + display: flex; + flex-direction: column; +} + +.selector select { + width: 100%; + flex: 1 0 auto; + margin-bottom: 5px; +} + +.selector ul.selector-chooser { + width: 26px; + height: 52px; + padding: 2px 0; + margin: auto 15px; + border-radius: 20px; + transform: translateY(-10px); + list-style: none; +} + +.selector-add, .selector-remove { + width: 20px; + height: 20px; + background-size: 20px auto; +} + +.selector-add { + background-position: 0 -120px; +} + +.selector-remove { + background-position: 0 -80px; +} + +a.selector-chooseall, a.selector-clearall { + align-self: center; +} + +.stacked { + flex-direction: column; + max-width: 480px; +} + +.stacked > * { + flex: 0 1 auto; +} + +.stacked select { + margin-bottom: 0; +} + +.stacked .selector-available, .stacked .selector-chosen { + width: auto; +} + +.stacked ul.selector-chooser { + width: 52px; + height: 26px; + padding: 0 2px; + margin: 15px auto; + transform: none; +} + +.stacked .selector-chooser li { + padding: 3px; +} + +.stacked .selector-add, .stacked .selector-remove { + background-size: 20px auto; +} + +.stacked .selector-add { + background-position: 0 -40px; +} + +.stacked .active.selector-add { + background-position: 0 -60px; +} + +.stacked .selector-remove { + background-position: 0 0; +} + +.stacked .active.selector-remove { + background-position: 0 -20px; +} + +.help-tooltip, .selector .help-icon { + display: none; +} + +form .form-row p.datetime { + width: 100%; +} + +.datetime input { + width: 50%; + max-width: 120px; +} + +.datetime span { + font-size: 13px; +} + +.datetime .timezonewarning { + display: block; + font-size: 11px; + color: #999; +} + +.datetimeshortcuts { + color: #ccc; +} + +.inline-group { + overflow: auto; +} + +.selector-add, .selector-remove { + width: 16px; + height: 16px; + display: block; + text-indent: -3000px; + overflow: hidden; + cursor: default; + opacity: 0.3; +} + +.active.selector-add, .active.selector-remove { + opacity: 1; +} + +.active.selector-add:hover, .active.selector-remove:hover { + cursor: pointer; +} + +.selector-add { + background: url(../admin/img/selector-icons.svg) 0 -96px no-repeat; +} + +.active.selector-add:focus, .active.selector-add:hover { + background-position: 0 -112px; +} + +.selector-remove { + background: url(../admin/img/selector-icons.svg) 0 -64px no-repeat; +} From 364f040b360d74adfa677856d7375ab188ccace2 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 10 Mar 2019 18:34:09 +0100 Subject: [PATCH 3/5] always use FilteredSelectMultiple for many-to-many fields --- passbook/core/forms/applications.py | 2 ++ passbook/core/forms/factors.py | 4 ++++ passbook/core/forms/groups.py | 2 +- passbook/hibp_policy/forms.py | 3 +++ passbook/ldap/forms.py | 2 ++ passbook/oauth_client/forms.py | 2 ++ passbook/otp/forms.py | 2 ++ passbook/password_expiry_policy/forms.py | 2 ++ passbook/saml_idp/forms.py | 3 +++ 9 files changed, 21 insertions(+), 1 deletion(-) diff --git a/passbook/core/forms/applications.py b/passbook/core/forms/applications.py index 47693fea5..254c999e6 100644 --- a/passbook/core/forms/applications.py +++ b/passbook/core/forms/applications.py @@ -1,5 +1,6 @@ """passbook Core Application forms""" from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext_lazy as _ from passbook.core.models import Application, Provider @@ -20,6 +21,7 @@ class ApplicationForm(forms.ModelForm): 'name': forms.TextInput(), 'launch_url': forms.TextInput(), 'icon_url': forms.TextInput(), + 'policies': FilteredSelectMultiple(_('policies'), False) } labels = { 'launch_url': _('Launch URL'), diff --git a/passbook/core/forms/factors.py b/passbook/core/forms/factors.py index 11d0061c6..79d52d0ed 100644 --- a/passbook/core/forms/factors.py +++ b/passbook/core/forms/factors.py @@ -1,5 +1,7 @@ """passbook administration forms""" from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple +from django.utils.translation import gettext as _ from passbook.core.models import DummyFactor, PasswordFactor from passbook.lib.fields import DynamicArrayField @@ -16,6 +18,7 @@ class PasswordFactorForm(forms.ModelForm): widgets = { 'name': forms.TextInput(), 'order': forms.NumberInput(), + 'policies': FilteredSelectMultiple(_('policies'), False) } field_classes = { 'backends': DynamicArrayField @@ -31,4 +34,5 @@ class DummyFactorForm(forms.ModelForm): widgets = { 'name': forms.TextInput(), 'order': forms.NumberInput(), + 'policies': FilteredSelectMultiple(_('policies'), False) } diff --git a/passbook/core/forms/groups.py b/passbook/core/forms/groups.py index 4f2052623..3a776cd17 100644 --- a/passbook/core/forms/groups.py +++ b/passbook/core/forms/groups.py @@ -1,8 +1,8 @@ """passbook Core Group forms""" from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple from passbook.core.models import Group, User -from django.contrib.admin.widgets import FilteredSelectMultiple class GroupForm(forms.ModelForm): diff --git a/passbook/hibp_policy/forms.py b/passbook/hibp_policy/forms.py index 08254a53b..643bf4791 100644 --- a/passbook/hibp_policy/forms.py +++ b/passbook/hibp_policy/forms.py @@ -1,6 +1,8 @@ """passbook HaveIBeenPwned Policy forms""" from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple +from django.utils.translation import gettext as _ from passbook.core.forms.policies import GENERAL_FIELDS from passbook.hibp_policy.models import HaveIBeenPwendPolicy @@ -16,4 +18,5 @@ class HaveIBeenPwnedPolicyForm(forms.ModelForm): widgets = { 'name': forms.TextInput(), 'order': forms.NumberInput(), + 'policies': FilteredSelectMultiple(_('policies'), False) } diff --git a/passbook/ldap/forms.py b/passbook/ldap/forms.py index cb47328c1..b16658886 100644 --- a/passbook/ldap/forms.py +++ b/passbook/ldap/forms.py @@ -1,6 +1,7 @@ """passbook LDAP Forms""" from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext_lazy as _ from passbook.admin.forms.source import SOURCE_FORM_FIELDS @@ -23,6 +24,7 @@ class LDAPSourceForm(forms.ModelForm): 'bind_password': forms.TextInput(), 'domain': forms.TextInput(), 'base_dn': forms.TextInput(), + 'policies': FilteredSelectMultiple(_('policies'), False) } labels = { 'server_uri': _('Server URI'), diff --git a/passbook/oauth_client/forms.py b/passbook/oauth_client/forms.py index 6187f4869..cd9036316 100644 --- a/passbook/oauth_client/forms.py +++ b/passbook/oauth_client/forms.py @@ -1,6 +1,7 @@ """passbook oauth_client forms""" from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext as _ from passbook.admin.forms.source import SOURCE_FORM_FIELDS @@ -29,6 +30,7 @@ class OAuthSourceForm(forms.ModelForm): 'consumer_key': forms.TextInput(), 'consumer_secret': forms.TextInput(), 'provider_type': forms.Select(choices=MANAGER.get_name_tuple()), + 'policies': FilteredSelectMultiple(_('policies'), False) } labels = { 'request_token_url': _('Request Token URL'), diff --git a/passbook/otp/forms.py b/passbook/otp/forms.py index 52c00b3d2..bfe41e0bc 100644 --- a/passbook/otp/forms.py +++ b/passbook/otp/forms.py @@ -1,6 +1,7 @@ """passbook OTP Forms""" from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple from django.core.validators import RegexValidator from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -63,4 +64,5 @@ class OTPFactorForm(forms.ModelForm): widgets = { 'name': forms.TextInput(), 'order': forms.NumberInput(), + 'policies': FilteredSelectMultiple(_('policies'), False) } diff --git a/passbook/password_expiry_policy/forms.py b/passbook/password_expiry_policy/forms.py index cd957af0a..be496cd0b 100644 --- a/passbook/password_expiry_policy/forms.py +++ b/passbook/password_expiry_policy/forms.py @@ -1,6 +1,7 @@ """passbook PasswordExpiry Policy forms""" from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext as _ from passbook.core.forms.policies import GENERAL_FIELDS @@ -18,6 +19,7 @@ class PasswordExpiryPolicyForm(forms.ModelForm): 'name': forms.TextInput(), 'order': forms.NumberInput(), 'days': forms.NumberInput(), + 'policies': FilteredSelectMultiple(_('policies'), False) } labels = { 'deny_only': _("Only fail the policy, don't set user's password.") diff --git a/passbook/saml_idp/forms.py b/passbook/saml_idp/forms.py index e54bd6306..d305b2990 100644 --- a/passbook/saml_idp/forms.py +++ b/passbook/saml_idp/forms.py @@ -1,6 +1,8 @@ """passbook SAML IDP Forms""" from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple +from django.utils.translation import gettext as _ from passbook.lib.fields import DynamicArrayField from passbook.saml_idp.models import (SAMLPropertyMapping, SAMLProvider, @@ -32,6 +34,7 @@ class SAMLProviderForm(forms.ModelForm): widgets = { 'name': forms.TextInput(), 'issuer': forms.TextInput(), + 'property_mappings': FilteredSelectMultiple(_('Property Mappings'), False) } From e4baf8c21ebe6fadc32e7d5f89c54d038655fed4 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 10 Mar 2019 19:32:18 +0100 Subject: [PATCH 4/5] Add Group Member policy --- passbook/core/forms/policies.py | 14 +++++++++- .../migrations/0020_groupmembershippolicy.py | 26 +++++++++++++++++++ passbook/core/models.py | 15 +++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 passbook/core/migrations/0020_groupmembershippolicy.py diff --git a/passbook/core/forms/policies.py b/passbook/core/forms/policies.py index 3fd518c1b..f8d54a6c0 100644 --- a/passbook/core/forms/policies.py +++ b/passbook/core/forms/policies.py @@ -4,7 +4,8 @@ from django import forms from django.utils.translation import gettext as _ from passbook.core.models import (DebugPolicy, FieldMatcherPolicy, - PasswordPolicy, WebhookPolicy) + GroupMembershipPolicy, PasswordPolicy, + WebhookPolicy) GENERAL_FIELDS = ['name', 'action', 'negate', 'order', ] @@ -53,6 +54,17 @@ class DebugPolicyForm(forms.ModelForm): } +class GroupMembershipPolicyForm(forms.ModelForm): + """GroupMembershipPolicy Form""" + + class Meta: + + model = GroupMembershipPolicy + fields = GENERAL_FIELDS + ['group', ] + widgets = { + 'name': forms.TextInput(), + } + class PasswordPolicyForm(forms.ModelForm): """PasswordPolicy Form""" diff --git a/passbook/core/migrations/0020_groupmembershippolicy.py b/passbook/core/migrations/0020_groupmembershippolicy.py new file mode 100644 index 000000000..f120f4908 --- /dev/null +++ b/passbook/core/migrations/0020_groupmembershippolicy.py @@ -0,0 +1,26 @@ +# Generated by Django 2.1.7 on 2019-03-10 18:25 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('passbook_core', '0019_auto_20190310_1615'), + ] + + operations = [ + migrations.CreateModel( + name='GroupMembershipPolicy', + fields=[ + ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Group')), + ], + options={ + 'verbose_name': 'Group Membership Policy', + 'verbose_name_plural': 'Group Membership Policies', + }, + bases=('passbook_core.policy',), + ), + ] diff --git a/passbook/core/models.py b/passbook/core/models.py index c60f580b3..f02c3a482 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -393,6 +393,21 @@ class DebugPolicy(Policy): verbose_name = _('Debug Policy') verbose_name_plural = _('Debug Policies') +class GroupMembershipPolicy(Policy): + """Policy to check if the user is member in a certain group""" + + group = models.ForeignKey('Group', on_delete=models.CASCADE) + + form = 'passbook.core.forms.policies.GroupMembershipPolicyForm' + + def passes(self, user: User) -> Union[bool, Tuple[bool, str]]: + return self.group.user_set.filter(pk=user.pk).exists() + + class Meta: + + verbose_name = _('Group Membership Policy') + verbose_name_plural = _('Group Membership Policies') + class Invitation(UUIDModel): """Single-use invitation link""" From f7c0c0146ab976259fecd4d4cdceaae5dbd26159 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 10 Mar 2019 19:45:16 +0100 Subject: [PATCH 5/5] add LDAP Group Membership Policy --- passbook/ldap/forms.py | 67 ++++--------------- .../0002_ldapgroupmembershippolicy.py | 28 ++++++++ passbook/ldap/models.py | 35 ++++------ 3 files changed, 54 insertions(+), 76 deletions(-) create mode 100644 passbook/ldap/migrations/0002_ldapgroupmembershippolicy.py diff --git a/passbook/ldap/forms.py b/passbook/ldap/forms.py index b16658886..c663a1c00 100644 --- a/passbook/ldap/forms.py +++ b/passbook/ldap/forms.py @@ -5,7 +5,8 @@ from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext_lazy as _ from passbook.admin.forms.source import SOURCE_FORM_FIELDS -from passbook.ldap.models import LDAPSource +from passbook.core.forms.policies import GENERAL_FIELDS +from passbook.ldap.models import LDAPGroupMembershipPolicy, LDAPSource class LDAPSourceForm(forms.ModelForm): @@ -32,58 +33,18 @@ class LDAPSourceForm(forms.ModelForm): 'base_dn': _('Base DN'), } -# class GeneralSettingsForm(SettingsForm): -# """general settings form""" -# MODE_AUTHENTICATION_BACKEND = 'auth_backend' -# MODE_CREATE_USERS = 'create_users' -# MODE_CHOICES = ( -# (MODE_AUTHENTICATION_BACKEND, _('Authentication Backend')), -# (MODE_CREATE_USERS, _('Create Users')) -# ) -# namespace = 'passbook.ldap' -# settings = ['enabled', 'mode'] +class LDAPGroupMembershipPolicyForm(forms.ModelForm): + """LDAPGroupMembershipPolicy Form""" -# widgets = { -# 'enabled': forms.BooleanField(required=False), -# 'mode': forms.ChoiceField(widget=forms.RadioSelect, choices=MODE_CHOICES), -# } + class Meta: - -# class ConnectionSettings(SettingsForm): -# """Connection settings form""" - -# namespace = 'passbook.ldap' -# settings = ['server', 'server:tls', 'bind:user', 'bind:password', 'domain'] - -# attrs_map = { -# 'server': {'placeholder': 'dc1.corp.exmaple.com'}, -# 'bind:user': {'placeholder': 'Administrator'}, -# 'domain': {'placeholder': 'corp.example.com'}, -# } - -# widgets = { -# 'server:tls': forms.BooleanField(required=False, label=_('Server TLS')), -# } - - -# class AuthenticationBackendSettings(SettingsForm): -# """Authentication backend settings""" - -# namespace = 'passbook.ldap' -# settings = ['base'] - -# attrs_map = { -# 'base': {'placeholder': 'DN in which to search for users'}, -# } - - -# class CreateUsersSettings(SettingsForm): -# """Create users settings""" - -# namespace = 'passbook.ldap' -# settings = ['create_base'] - -# attrs_map = { -# 'create_base': {'placeholder': 'DN in which to create users'}, -# } + model = LDAPGroupMembershipPolicy + fields = GENERAL_FIELDS + ['dn', ] + widgets = { + 'name': forms.TextInput(), + 'dn': forms.TextInput(), + } + labels = { + 'dn': _('DN') + } diff --git a/passbook/ldap/migrations/0002_ldapgroupmembershippolicy.py b/passbook/ldap/migrations/0002_ldapgroupmembershippolicy.py new file mode 100644 index 000000000..a7f2bed7e --- /dev/null +++ b/passbook/ldap/migrations/0002_ldapgroupmembershippolicy.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1.7 on 2019-03-10 18:38 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('passbook_core', '0020_groupmembershippolicy'), + ('passbook_ldap', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='LDAPGroupMembershipPolicy', + fields=[ + ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), + ('dn', models.TextField()), + ('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_ldap.LDAPSource')), + ], + options={ + 'verbose_name': 'LDAP Group Membership Policy', + 'verbose_name_plural': 'LDAP Group Membership Policys', + }, + bases=('passbook_core.policy',), + ), + ] diff --git a/passbook/ldap/models.py b/passbook/ldap/models.py index 6d112cfc7..190da2ebe 100644 --- a/passbook/ldap/models.py +++ b/passbook/ldap/models.py @@ -3,7 +3,7 @@ from django.db import models from django.utils.translation import gettext as _ -from passbook.core.models import Source +from passbook.core.models import Policy, Source, User class LDAPSource(Source): @@ -37,30 +37,19 @@ class LDAPSource(Source): verbose_name = _('LDAP Source') verbose_name_plural = _('LDAP Sources') +class LDAPGroupMembershipPolicy(Policy): + """Policy to check if a user is in a certain LDAP Group""" -# class LDAPModification(UUIDModel, CreatedUpdatedModel): -# """Store LDAP Data in DB if LDAP Server is unavailable""" -# ACTION_ADD = 'ADD' -# ACTION_MODIFY = 'MODIFY' + dn = models.TextField() + source = models.ForeignKey('LDAPSource', on_delete=models.CASCADE) -# ACTIONS = ( -# (ACTION_ADD, 'ADD'), -# (ACTION_MODIFY, 'MODIFY'), -# ) + form = 'passbook.ldap.forms.LDAPGroupMembershipPolicyForm' -# dn = models.CharField(max_length=255) -# action = models.CharField(max_length=17, choices=ACTIONS, default=ACTION_MODIFY) -# data = JSONField() + def passes(self, user: User): + """Check if user instance passes this policy""" + raise NotImplementedError() -# def __str__(self): -# return "LDAPModification %d from %s" % (self.pk, self.created) + class Meta: - -# class LDAPGroupMapping(UUIDModel, CreatedUpdatedModel): -# """Model to map an LDAP Group to a passbook group""" - -# ldap_dn = models.TextField() -# group = models.ForeignKey(Group, on_delete=models.CASCADE) - -# def __str__(self): -# return "LDAPGroupMapping %s -> %s" % (self.ldap_dn, self.group.name) + verbose_name = _('LDAP Group Membership Policy') + verbose_name_plural = _('LDAP Group Membership Policys')