Skip to content

Commit

Permalink
Merge d20a523 into 1a5cab5
Browse files Browse the repository at this point in the history
  • Loading branch information
grahamu committed Sep 13, 2016
2 parents 1a5cab5 + d20a523 commit 22466a3
Show file tree
Hide file tree
Showing 16 changed files with 566 additions and 62 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
21 changes: 20 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,19 @@ class EmailAddressAdmin(AccountAdmin):
search_fields = ["email", "user__username"]


class PasswordExpiryAdmin(admin.ModelAdmin):

raw_id_fields = ["user"]


class PasswordHistoryAdmin(admin.ModelAdmin):

raw_id_fields = ["user"]


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)
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))
16 changes: 16 additions & 0 deletions account/middleware.py
@@ -1,10 +1,13 @@
from __future__ import unicode_literals

from django.core.urlresolvers import resolve
from django.shortcuts import redirect
from django.utils import translation, timezone
from django.utils.cache import patch_vary_headers

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 +54,16 @@ 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:
url_name = resolve(request.path).url_name
# All users must be allowed to access "change password" url.
if url_name not in settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL:
if check_password_expired(request.user):
return redirect(
settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL
)
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
4 changes: 2 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 @@ -398,7 +398,7 @@ class PasswordHistory(models.Model):
"""
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/tests/templates/account/settings.html
@@ -0,0 +1 @@
# empty for now

0 comments on commit 22466a3

Please sign in to comment.