From 408e205c5f2cecaf7cea544cff511f27eb39f84e Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 25 Feb 2019 15:41:36 +0100 Subject: [PATCH] add signal for password change, add field for password policies --- passbook/admin/views/factors.py | 20 +++++++-------- passbook/core/forms/factors.py | 2 +- .../migrations/0011_auto_20190225_1438.py | 25 +++++++++++++++++++ passbook/core/models.py | 16 ++++++++++++ passbook/core/signals.py | 1 + passbook/ldap/models.py | 2 +- 6 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 passbook/core/migrations/0011_auto_20190225_1438.py diff --git a/passbook/admin/views/factors.py b/passbook/admin/views/factors.py index 4dcab93b3..07b2c3d4b 100644 --- a/passbook/admin/views/factors.py +++ b/passbook/admin/views/factors.py @@ -39,19 +39,18 @@ class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) - source_type = self.request.GET.get('type') - model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type) + factor_type = self.request.GET.get('type') + model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type) kwargs['type'] = model._meta.verbose_name return kwargs def get_form_class(self): - source_type = self.request.GET.get('type') - model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type) + factor_type = self.request.GET.get('type') + model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type) if not model: raise Http404 return path_to_class(model.form) - class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView): """Update factor""" @@ -61,11 +60,12 @@ class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView): success_message = _('Successfully updated Factor') def get_form_class(self): - source_type = self.request.GET.get('type') - model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type) - if not model: - raise Http404 - return path_to_class(model.form) + 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 Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): """Delete factor""" diff --git a/passbook/core/forms/factors.py b/passbook/core/forms/factors.py index 692066e57..30a587546 100644 --- a/passbook/core/forms/factors.py +++ b/passbook/core/forms/factors.py @@ -11,7 +11,7 @@ class PasswordFactorForm(forms.ModelForm): class Meta: model = PasswordFactor - fields = GENERAL_FIELDS + ['backends'] + fields = GENERAL_FIELDS + ['backends', 'password_policies'] widgets = { 'name': forms.TextInput(), 'order': forms.NumberInput(), diff --git a/passbook/core/migrations/0011_auto_20190225_1438.py b/passbook/core/migrations/0011_auto_20190225_1438.py new file mode 100644 index 000000000..690c6f4e7 --- /dev/null +++ b/passbook/core/migrations/0011_auto_20190225_1438.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1.7 on 2019-02-25 14:38 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('passbook_core', '0010_auto_20190224_1016'), + ] + + operations = [ + migrations.AddField( + model_name='passwordfactor', + name='password_policies', + field=models.ManyToManyField(blank=True, to='passbook_core.Policy'), + ), + migrations.AddField( + model_name='user', + name='password_change_date', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/passbook/core/models.py b/passbook/core/models.py index f06345380..25f5d81b6 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -9,9 +9,11 @@ from django.contrib.auth.models import AbstractUser from django.contrib.postgres.fields import ArrayField from django.db import models from django.urls import reverse_lazy +from django.utils.timezone import now from django.utils.translation import gettext as _ from model_utils.managers import InheritanceManager +from passbook.core.signals import password_changed from passbook.lib.models import CreatedUpdatedModel, UUIDModel LOGGER = getLogger(__name__) @@ -38,6 +40,12 @@ class User(AbstractUser): sources = models.ManyToManyField('Source', through='UserSourceConnection') applications = models.ManyToManyField('Application') groups = models.ManyToManyField('Group') + password_change_date = models.DateTimeField(auto_now_add=True) + + def set_password(self, password): + password_changed.send(sender=self, user=self, password=password) + self.password_change_date = now() + return super().set_password(password) class Provider(models.Model): """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" @@ -87,6 +95,7 @@ class PasswordFactor(Factor): """Password-based Django-backend Authentication Factor""" backends = ArrayField(models.TextField()) + password_policies = models.ManyToManyField('Policy', blank=True) type = 'passbook.core.auth.factors.password.PasswordFactor' form = 'passbook.core.forms.factors.PasswordFactorForm' @@ -94,6 +103,13 @@ class PasswordFactor(Factor): def has_user_settings(self): return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password' + def password_passes(self, user: User) -> bool: + """Return true if user's password passes, otherwise False or raise Exception""" + for policy in self.policies.all(): + if not policy.passes(user): + return False + return True + def __str__(self): return "Password Factor %s" % self.slug diff --git a/passbook/core/signals.py b/passbook/core/signals.py index 8057c7f74..e497ce34c 100644 --- a/passbook/core/signals.py +++ b/passbook/core/signals.py @@ -9,3 +9,4 @@ from django.core.signals import Signal user_signed_up = Signal(providing_args=['request', 'user']) invitation_created = Signal(providing_args=['request', 'invitation']) invitation_used = Signal(providing_args=['request', 'invitation', 'user']) +password_changed = Signal(providing_args=['user', 'password']) diff --git a/passbook/ldap/models.py b/passbook/ldap/models.py index bad25e50f..ca98c7276 100644 --- a/passbook/ldap/models.py +++ b/passbook/ldap/models.py @@ -29,7 +29,7 @@ class LDAPSource(Source): form = 'passbook.ldap.forms.LDAPSourceForm' @property - def get_url(self): + def get_login_button(self): raise NotImplementedError() class Meta: