Skip to content

Commit

Permalink
Merge pull request #224 from pinax/expiry-docs
Browse files Browse the repository at this point in the history
Finish password expiration
  • Loading branch information
brosner committed Sep 19, 2016
2 parents 1a5cab5 + 32b7623 commit c4a40be
Show file tree
Hide file tree
Showing 24 changed files with 668 additions and 67 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ version with these. Your code will need to be updated to continue working.

* BI: account deletion callbacks moved to hooksets
* BI: dropped Django 1.7 support
* BI: removed deprecated `ACCOUNT_USE_AUTH_AUTHENTICATE` setting with behavior matching its `True` value
* BI: removed deprecated `ACCOUNT_USE_AUTH_AUTHENTICATE` setting with behavior matching its `True` value
* BI: remove Python 3.2 from test compatibility matrix
* add Django v1.10 to test compatibility matrix
* added Turkish translations
* fixed migration with language codes to dynamically set
* add password expiration

## 1.3.0

Expand Down
7 changes: 4 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Features
- Email confirmation
- Signup tokens for private betas
- Password reset
- Password expiration
- Account management (update account settings and change password)
- Account deletion

Expand All @@ -57,7 +58,7 @@ Features
Requirements
--------------

* Django 1.8 or 1.9
* Django 1.8, 1.9, or 1.10
* Python 2.7, 3.3, 3.4 or 3.5
* django-appconf (included in ``install_requires``)
* pytz (included in ``install_requires``)
Expand All @@ -79,13 +80,13 @@ See this blog post http://blog.pinaxproject.com/2016/02/26/recap-february-pinax-

In case of any questions we recommend you join our Pinax Slack team (http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack.

We also highly recommend reading our Open Source and Self-Care blog post (http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/).
We also highly recommend reading our Open Source and Self-Care blog post (http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/).


Code of Conduct
-----------------

In order to foster a kind, inclusive, and harassment-free community, the Pinax Project has a code of conduct, which can be found here http://pinaxproject.com/pinax/code_of_conduct/.
In order to foster a kind, inclusive, and harassment-free community, the Pinax Project has a code of conduct, which can be found here http://pinaxproject.com/pinax/code_of_conduct/.
We ask you to treat everyone as a smart human programmer that shares an interest in Python, Django, and Pinax with you.


Expand Down
2 changes: 1 addition & 1 deletion account/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.0.0.dev1"
__version__ = "2.0.0.dev2"
24 changes: 23 additions & 1 deletion account/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

from django.contrib import admin

from account.models import Account, SignupCode, AccountDeletion, EmailAddress
from account.models import (
Account,
AccountDeletion,
EmailAddress,
PasswordExpiry,
PasswordHistory,
SignupCode,
)


class SignupCodeAdmin(admin.ModelAdmin):
Expand All @@ -29,7 +36,22 @@ class EmailAddressAdmin(AccountAdmin):
search_fields = ["email", "user__username"]


class PasswordExpiryAdmin(admin.ModelAdmin):

raw_id_fields = ["user"]


class PasswordHistoryAdmin(admin.ModelAdmin):

raw_id_fields = ["user"]
list_display = ["user", "timestamp"]
list_filter = ["user"]
ordering = ["user__username", "-timestamp"]


admin.site.register(Account, AccountAdmin)
admin.site.register(SignupCode, SignupCodeAdmin)
admin.site.register(AccountDeletion, AccountDeletionAdmin)
admin.site.register(EmailAddress, EmailAddressAdmin)
admin.site.register(PasswordExpiry, PasswordExpiryAdmin)
admin.site.register(PasswordHistory, PasswordHistoryAdmin)
1 change: 1 addition & 0 deletions account/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class AccountAppConf(AppConf):

OPEN_SIGNUP = True
LOGIN_URL = "account_login"
LOGOUT_URL = "account_logout"
SIGNUP_REDIRECT_URL = "/"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
Expand Down
3 changes: 2 additions & 1 deletion account/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import functools

from django.contrib.auth import REDIRECT_FIELD_NAME
from django.utils.decorators import available_attrs

from account.utils import handle_redirect_to_login


def login_required(func=None, redirect_field_name="next", login_url=None):
def login_required(func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
"""
Decorator for views that checks that the user is logged in, redirecting
to the log in page if necessary.
Expand Down
39 changes: 39 additions & 0 deletions account/management/commands/user_password_expiry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.contrib.auth import get_user_model
from django.core.management.base import LabelCommand

from account.conf import settings
from account.models import PasswordExpiry


class Command(LabelCommand):

help = "Create user-specific password expiration period."
label = "username"

def add_arguments(self, parser):
super(Command, self).add_arguments(parser)
parser.add_argument(
"-e", "--expire",
type=int,
nargs="?",
default=settings.ACCOUNT_PASSWORD_EXPIRY,
help="number of seconds until password expires"
)

def handle_label(self, username, **options):
User = get_user_model()
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return "User \"{}\" not found".format(username)

expire = options["expire"]

# Modify existing PasswordExpiry or create new if needed.
if not hasattr(user, "password_expiry"):
PasswordExpiry.objects.create(user=user, expiry=expire)
else:
user.password_expiry.expiry = expire
user.password_expiry.save()

return "User \"{}\" password expiration set to {} seconds".format(username, expire)
45 changes: 45 additions & 0 deletions account/management/commands/user_password_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import datetime
import pytz

from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand

from account.models import PasswordHistory


class Command(BaseCommand):

help = "Create password history for all users without existing history."

def add_arguments(self, parser):
parser.add_argument(
"-d", "--days",
type=int,
nargs="?",
default=10,
help="age of current password (in days)"
)
parser.add_argument(
"-f", "--force",
action="store_true",
help="create new password history for all users, regardless of existing history"
)

def handle(self, *args, **options):
User = get_user_model()
users = User.objects.all()
if not options["force"]:
users = users.filter(password_history=None)

if not users:
return "No users found without password history"

days = options["days"]
timestamp = datetime.datetime.now(tz=pytz.UTC) - datetime.timedelta(days=days)

# Create new PasswordHistory on `timestamp`
PasswordHistory.objects.bulk_create(
[PasswordHistory(user=user, timestamp=timestamp) for user in users]
)

return "Password history set to {} for {} users".format(timestamp, len(users))
41 changes: 41 additions & 0 deletions account/middleware.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
from __future__ import unicode_literals

try:
from urllib.parse import urlparse, urlunparse
except ImportError: # python 2
from urlparse import urlparse, urlunparse

from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.core.urlresolvers import resolve, reverse
from django.http import HttpResponseRedirect, QueryDict
from django.utils import translation, timezone
from django.utils.cache import patch_vary_headers
from django.utils.translation import ugettext_lazy as _

from account import signals
from account.conf import settings
from account.models import Account
from account.utils import check_password_expired


class LocaleMiddleware(object):
Expand Down Expand Up @@ -51,3 +63,32 @@ def process_request(self, request):
if account:
tz = settings.TIME_ZONE if not account.timezone else account.timezone
timezone.activate(tz)


class ExpiredPasswordMiddleware(object):

def process_request(self, request):
if request.user.is_authenticated() and not request.user.is_staff:
next_url = resolve(request.path).url_name
# Authenticated users must be allowed to access
# "change password" page and "log out" page.
# even if password is expired.
if next_url not in [settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL,
settings.ACCOUNT_LOGOUT_URL,
]:
if check_password_expired(request.user):
signals.password_expired.send(sender=self, user=request.user)
messages.add_message(
request,
messages.WARNING,
_("Your password has expired. Please save a new password.")
)
redirect_field_name = REDIRECT_FIELD_NAME

change_password_url = reverse(settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL)
url_bits = list(urlparse(change_password_url))
querystring = QueryDict(url_bits[4], mutable=True)
querystring[redirect_field_name] = next_url
url_bits[4] = querystring.urlencode(safe="/")

return HttpResponseRedirect(urlunparse(url_bits))
5 changes: 3 additions & 2 deletions account/migrations/0003_passwordexpiry_passwordhistory.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-09-01 17:50
# Generated by Django 1.10.1 on 2016-09-13 08:55
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):
Expand All @@ -28,7 +29,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=255)),
('timestamp', models.DateTimeField(auto_now=True)),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='password_history', to=settings.AUTH_USER_MODEL)),
],
),
Expand Down
4 changes: 3 additions & 1 deletion account/mixins.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from __future__ import unicode_literals

from django.contrib.auth import REDIRECT_FIELD_NAME

from account.conf import settings
from account.utils import handle_redirect_to_login


class LoginRequiredMixin(object):

redirect_field_name = "next"
redirect_field_name = REDIRECT_FIELD_NAME
login_url = None

def dispatch(self, request, *args, **kwargs):
Expand Down
8 changes: 6 additions & 2 deletions account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def user_post_save(sender, **kwargs):
"""

# Disable post_save during manage.py loaddata
if kwargs.get('raw', False):
if kwargs.get("raw", False):
return False

user, created = kwargs["instance"], kwargs["created"]
Expand Down Expand Up @@ -396,9 +396,13 @@ class PasswordHistory(models.Model):
"""
Contains single password history for user.
"""
class Meta:
verbose_name = _("password history")
verbose_name_plural = _("password histories")

user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="password_history")
password = models.CharField(max_length=255) # encrypted password
timestamp = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(default=timezone.now) # password creation time


class PasswordExpiry(models.Model):
Expand Down
1 change: 1 addition & 0 deletions account/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
email_confirmed = django.dispatch.Signal(providing_args=["email_address"])
email_confirmation_sent = django.dispatch.Signal(providing_args=["confirmation"])
password_changed = django.dispatch.Signal(providing_args=["user"])
password_expired = django.dispatch.Signal(providing_args=["user"])
1 change: 1 addition & 0 deletions account/tests/templates/account/settings.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty for now
Loading

0 comments on commit c4a40be

Please sign in to comment.