Skip to content

Commit

Permalink
Merge d16099b into 1a5cab5
Browse files Browse the repository at this point in the history
  • Loading branch information
grahamu committed Sep 15, 2016
2 parents 1a5cab5 + d16099b commit fa64d7b
Show file tree
Hide file tree
Showing 22 changed files with 662 additions and 65 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
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
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
@@ -1 +1 @@
__version__ = "2.0.0.dev1"
__version__ = "2.0.0.dev2"
24 changes: 23 additions & 1 deletion account/admin.py
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
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
39 changes: 39 additions & 0 deletions account/management/commands/user_password_expiry.py
@@ -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
@@ -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))
40 changes: 40 additions & 0 deletions account/middleware.py
@@ -1,10 +1,21 @@
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.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 +62,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 = "next" # fragile!

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
@@ -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
8 changes: 6 additions & 2 deletions account/models.py
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
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
@@ -0,0 +1 @@
# empty for now

0 comments on commit fa64d7b

Please sign in to comment.