Skip to content

Commit

Permalink
Working on #7.
Browse files Browse the repository at this point in the history
  • Loading branch information
Tarak Blah committed Oct 13, 2013
1 parent 0a971c7 commit 0d189a2
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 27 deletions.
9 changes: 9 additions & 0 deletions docs/api/password_policies.models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ Models
.. automodule:: password_policies.models


.. model:: PasswordChangeRequired

``PasswordChangeRequired``
-------------------

.. autoclass:: password_policies.models.PasswordChangeRequired
:members:


.. model:: PasswordHistory

``PasswordHistory``
Expand Down
26 changes: 18 additions & 8 deletions docs/topics/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,29 @@ Debian based distributions the according package is called:

* python-levenshtein

.. _install-download:
.. _install-pypi:

Download
========
From Pypi
=========

The latest release package can be downloaded from `the GitHub download page`_.
To install from `PyPi`_::

.. _`the GitHub download page`: https://github.com/tarak/django-password-policies/releases
[sudo] pip install django-password-policies

or::

[sudo] easy_install django-password-policies

.. _`PyPi`: https://pypi.python.org/pypi/django-password-policies

.. _install-install:
.. _install-source:

Installing
==========
From source
===========

The latest release package can be downloaded from `the GitHub download page`_.

.. _`the GitHub download page`: https://github.com/tarak/django-password-policies/releases

Once you've downloaded the package, unpack it (on most operating systems, simply
double-click; alternately, at a command line on Linux, Mac OS X or other
Expand Down
45 changes: 45 additions & 0 deletions password_policies/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _

from password_policies.conf import settings
from password_policies.models import PasswordHistory
from password_policies.models import PasswordChangeRequired


def force_password_change(modeladmin, request, queryset):
for user in queryset.all():
PasswordChangeRequired.objects.create(user=user)
force_password_change.short_description = _('Force password change for selected'
' users')

class PasswordHistoryAdmin(admin.ModelAdmin):
date_hierarchy = 'created'
exclude = ('password',)
list_display = ('id', 'user', 'created')
list_display_links = ('id', 'user',)
search_fields = settings.PASSWORD_HISTORY_ADMIN_SEARCH_FIELDS
readonly_fields = ('user', 'created')

def has_add_permission(self, request):
return False

class PasswordChangeRequiredAdmin(admin.ModelAdmin):
date_hierarchy = 'created'
list_display = ('id', 'user', 'created')
list_display_links = ('id', 'user',)
search_fields = settings.PASSWORD_CHANGE_REQUIRED_ADMIN_SEARCH_FIELDS
readonly_fields = ('user', 'created')

def get_readonly_fields(self, request, obj=None):
"""
Sets the ``user`` and the ``created`` field to read only if an instance
already exists.
"""
if obj:
return ['user']
else:
return []


admin.site.register(PasswordHistory, PasswordHistoryAdmin)
admin.site.register(PasswordChangeRequired, PasswordChangeRequiredAdmin)
16 changes: 16 additions & 0 deletions password_policies/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ class Settings(AppSettings):
"""
Default settings for django-password-policies.
"""
#: Determines which fields should be searched upon
#: in the admin change list of
#: the :class:`~password_policies.models.PasswordChangeRequired`
PASSWORD_CHANGE_REQUIRED_ADMIN_SEARCH_FIELDS = ['id', 'user__id',
'user__first_name',
'user__email',
'user__last_name',
'user__username',]
#: Determines wether the :middleware:`PasswordChangeMiddleware`
#: should ignore the logout views, allowing the user to log out
#: even if a password change is required.
Expand Down Expand Up @@ -51,6 +59,14 @@ class Settings(AppSettings):
#:
#: Defaults to 60 days.
PASSWORD_DURATION_SECONDS = 24 * 60**3
#: Determines which fields should be searched upon
#: in the admin change list of
#: the :class:`~password_policies.models.PasswordHistory`
PASSWORD_HISTORY_ADMIN_SEARCH_FIELDS = ['id', 'user__id',
'user__first_name',
'user__email',
'user__last_name',
'user__username',]
#: Specifies the number of user's previous passwords to
#: remember when the password history is being used.
#:
Expand Down
3 changes: 3 additions & 0 deletions password_policies/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from password_policies.conf import settings
from password_policies.forms.fields import PasswordPoliciesField
from password_policies.models import PasswordHistory
from password_policies.models import PasswordChangeRequired


class PasswordPoliciesForm(forms.Form):
Expand Down Expand Up @@ -87,6 +88,8 @@ def save(self, commit=True):
password = make_password(new_password)
PasswordHistory.objects.create(password=password, user=self.user)
PasswordHistory.objects.delete_expired(self.user)
if PasswordChangeRequired.objects.filter(user=self.user).count():
PasswordChangeRequired.objects.filter(user=self.user).delete()
return self.user


Expand Down
15 changes: 12 additions & 3 deletions password_policies/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
#from datetime import datetime
from datetime import timedelta

from django.utils import timezone
Expand All @@ -7,6 +7,7 @@

from password_policies.conf import settings
from password_policies.models import PasswordHistory
from password_policies.models import PasswordChangeRequired

import re

Expand Down Expand Up @@ -51,7 +52,7 @@ class PasswordChangeMiddleware(object):
last = '_password_policies_last_changed'
required = '_password_policies_change_required'

def _check(self, request):
def _check_history(self, request):
if not request.session.get(self.last, None):
newest = PasswordHistory.objects.get_newest(request.user)
if newest:
Expand All @@ -63,12 +64,19 @@ def _check(self, request):
request.session[self.expired] = expired_date
if request.session[self.last] < request.session[self.expired]:
request.session[self.required] = True
if not PasswordChangeRequired.objects.filter(user=request.user).count():
PasswordChangeRequired.objects.create(user=request.user)
else:
request.session[self.required] = False

def _check_necessary(self, request):
if not request.session.get(self.checked, None):
request.session[self.checked] = self.now
# If a password change is enforced we won't check
# the user's password history, thus reducing DB hits...
if PasswordChangeRequired.objects.filter(user=request.user).count():
request.session[self.required] = True
return
seconds = settings.PASSWORD_CHECK_SECONDS
d = timedelta(seconds=seconds)
if request.session[self.checked] < self.now - d:
Expand All @@ -79,6 +87,8 @@ def _check_necessary(self, request):
del request.session[self.expired]
except KeyError:
pass
if settings.PASSWORD_USE_HISTORY:
self._check_history(request)

def _is_excluded_path(self, actual_path):
paths = settings.PASSWORD_CHANGE_MIDDLEWARE_EXCLUDED_PATHS
Expand Down Expand Up @@ -131,5 +141,4 @@ def process_request(self, request):
request.user.is_authenticated() and \
not self._is_excluded_path(request.path):
self._check_necessary(request)
self._check(request)
return self._redirect(request)
31 changes: 27 additions & 4 deletions password_policies/models.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,49 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _

from password_policies.conf import settings
from password_policies.managers import PasswordHistoryManager


class PasswordChangeRequired(models.Model):
"""
Stores an entry to enforce password changes, related to :model:`auth.User`.
Has the following fields:
"""
created = models.DateTimeField(auto_now_add=True,
verbose_name=_('created'), db_index=True,
help_text=_('The date the entry was '
'created.'))
user = models.OneToOneField(settings.AUTH_USER_MODEL,
verbose_name=_('user'),
help_text=_('The user who needs to change '
'his/her password.'),
related_name='password_change_required')

class Meta:
get_latest_by = 'created'
ordering = ['-created']
verbose_name = _('enforced password change')
verbose_name_plural = _('enforced password changes')


class PasswordHistory(models.Model):
"""
Stores a single password history entry, related to :model:`auth.User`.
Has the following fields:
"""
created = models.DateTimeField(auto_now_add=True, verbose_name=_('created'),
db_index=True,
created = models.DateTimeField(auto_now_add=True,
verbose_name=_('created'), db_index=True,
help_text=_('The date the entry was '
'created.'))
password = models.CharField(max_length=128, verbose_name=_('password'),
help_text=_('The encrypted password.'))
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'),
help_text=_('The user this password history '
'entry belongs to.'))
'entry belongs to.'),
related_name='password_history_entries')

objects = PasswordHistoryManager()

Expand Down
30 changes: 18 additions & 12 deletions password_policies/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
from datetime import datetime

from django.utils import timezone
from django.contrib.auth.decorators import login_required
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User
from django.core import signing
from django.core.urlresolvers import reverse
Expand All @@ -18,8 +15,9 @@
from django.views.decorators.debug import sensitive_post_parameters

from password_policies.conf import settings
from password_policies.forms import PasswordPoliciesForm, PasswordPoliciesChangeForm, PasswordResetForm
from password_policies.models import PasswordHistory
from password_policies.forms import PasswordPoliciesForm
from password_policies.forms import PasswordPoliciesChangeForm
from password_policies.forms import PasswordResetForm


class LoggedOutMixin(View):
Expand Down Expand Up @@ -78,7 +76,7 @@ def dispatch(self, *args, **kwargs):
return super(PasswordChangeFormView, self).dispatch(*args, **kwargs)

def form_valid(self, form):
user = form.save()
form.save()
return super(PasswordChangeFormView, self).form_valid(form)

def get_form(self, form_class):
Expand Down Expand Up @@ -162,20 +160,24 @@ def dispatch(self, request, *args, **kwargs):
max_age = settings.PASSWORD_RESET_TIMEOUT_DAYS * 24 * 60 * 60
l = (self.user.password, self.timestamp, self.signature)
try:
value = signer.unsign(':'.join(l), max_age=max_age)
signer.unsign(':'.join(l), max_age=max_age)
except (signing.BadSignature, signing.SignatureExpired):
pass
else:
self.validlink = True
return super(PasswordResetConfirmView, self).dispatch(request, *args, **kwargs)
return super(PasswordResetConfirmView, self).dispatch(request,
*args,
**kwargs)

def form_valid(self, form):
user = form.save()
form.save()
return super(PasswordResetConfirmView, self).form_valid(form)

def get(self, request, *args, **kwargs):
if self.validlink:
return super(PasswordResetConfirmView, self).get(request, *args, **kwargs)
return super(PasswordResetConfirmView, self).get(request,
*args,
**kwargs)
return self.render_to_response(self.get_context_data())

def get_context_data(self, **kwargs):
Expand All @@ -199,7 +201,9 @@ def get_success_url(self):

def post(self, request, *args, **kwargs):
if self.validlink:
return super(PasswordResetConfirmView, self).post(request, *args, **kwargs)
return super(PasswordResetConfirmView, self).post(request,
*args,
**kwargs)
return self.render_to_response(self.get_context_data())


Expand Down Expand Up @@ -258,7 +262,9 @@ def form_valid(self, form):

@method_decorator(csrf_protect)
def dispatch(self, request, *args, **kwargs):
return super(PasswordResetFormView, self).dispatch(request, *args, **kwargs)
return super(PasswordResetFormView, self).dispatch(request,
*args,
**kwargs)

def get_success_url(self):
"""
Expand Down

0 comments on commit 0d189a2

Please sign in to comment.