From 15628c698fbb9e96aa876f4bc35c8277afdd72e7 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz
Date: Wed, 8 Feb 2017 18:02:03 +0000
Subject: [PATCH] Tweaked the IPR Details page to show the possible a), b), and
c) choices under section 'V' when licensing declaration to be provided later
has been chosen. - Legacy-Id: 12791
---
ietf/bower.json | 3 +-
ietf/ietfauth/forms.py | 32 ++++-
ietf/ietfauth/urls.py | 27 ++--
ietf/ietfauth/views.py | 64 +++++++--
ietf/ipr/views.py | 3 +
ietf/settings.py | 11 +-
ietf/static/ietf/js/password_strength.js | 130 ++++++++++++++++++
ietf/templates/base/menu_user.html | 6 +-
ietf/templates/ipr/details_view.html | 12 +-
.../registration/change_password.html | 54 ++++++--
ietf/templates/registration/create.html | 30 ++--
ietf/templates/registration/edit_profile.html | 7 +
ietf/templates/registration/login.html | 39 +++---
requirements.txt | 3 +
14 files changed, 341 insertions(+), 80 deletions(-)
create mode 100644 ietf/static/ietf/js/password_strength.js
diff --git a/ietf/bower.json b/ietf/bower.json
index 710d76ec7..03d1ecb9b 100644
--- a/ietf/bower.json
+++ b/ietf/bower.json
@@ -15,7 +15,8 @@
"respond": "~1",
"select2": "~3",
"select2-bootstrap-css": "~1",
- "spin.js": "~2"
+ "spin.js": "~2",
+ "zxcvbn": "~4"
},
"devDependencies": {},
"overrides": {
diff --git a/ietf/ietfauth/forms.py b/ietf/ietfauth/forms.py
index 08b0ee9d4..f30f8c103 100644
--- a/ietf/ietfauth/forms.py
+++ b/ietf/ietfauth/forms.py
@@ -1,4 +1,5 @@
import re
+from unidecode import unidecode
from django import forms
from django.conf import settings
@@ -8,7 +9,7 @@ from django.contrib.auth.models import User
from django.utils.html import mark_safe
from django.core.urlresolvers import reverse as urlreverse
-from unidecode import unidecode
+from django_password_strength.widgets import PasswordStrengthInput, PasswordConfirmationInput
import debug # pyflakes:ignore
@@ -31,8 +32,8 @@ class RegistrationForm(forms.Form):
class PasswordForm(forms.Form):
- password = forms.CharField(widget=forms.PasswordInput)
- password_confirmation = forms.CharField(widget=forms.PasswordInput,
+ password = forms.CharField(widget=PasswordStrengthInput)
+ password_confirmation = forms.CharField(widget=PasswordConfirmationInput,
help_text="Enter the same password as above, for verification.")
def clean_password_confirmation(self):
@@ -166,3 +167,28 @@ class WhitelistForm(forms.ModelForm):
exclude = ['by', 'time' ]
+from django import forms
+
+
+class ChangePasswordForm(forms.Form):
+ current_password = forms.CharField(widget=forms.PasswordInput)
+
+
+ new_password = forms.CharField(widget=PasswordStrengthInput)
+ new_password_confirmation = forms.CharField(widget=PasswordConfirmationInput)
+
+ def __init__(self, user, data=None):
+ self.user = user
+ super(ChangePasswordForm, self).__init__(data)
+
+ def clean_current_password(self):
+ password = self.cleaned_data.get('current_password', None)
+ if not self.user.check_password(password):
+ raise ValidationError('Invalid password')
+
+ def clean(self):
+ new_password = self.cleaned_data.get('new_password', None)
+ conf_password = self.cleaned_data.get('new_password_confirmation', None)
+ if not new_password == conf_password:
+ raise ValidationError("The password confirmation is different than the new password")
+
diff --git a/ietf/ietfauth/urls.py b/ietf/ietfauth/urls.py
index 1bc247554..ee278a4d4 100644
--- a/ietf/ietfauth/urls.py
+++ b/ietf/ietfauth/urls.py
@@ -3,23 +3,20 @@
from django.conf.urls import url
from django.contrib.auth.views import login, logout
-from ietf.ietfauth.views import add_account_whitelist
+from ietf.ietfauth import views
urlpatterns = [
- url(r'^$', 'ietf.ietfauth.views.index'),
-# url(r'^login/$', 'ietf.ietfauth.views.ietf_login'),
+ url(r'^$', views.index),
+ url(r'^confirmnewemail/(?P[^/]+)/$', views.confirm_new_email),
+ url(r'^create/$', views.create_account),
+ url(r'^create/confirm/(?P[^/]+)/$', views.confirm_account),
url(r'^login/$', login),
url(r'^logout/$', logout),
-# url(r'^loggedin/$', 'ietf.ietfauth.views.ietf_loggedin'),
-# url(r'^loggedout/$', 'ietf.ietfauth.views.logged_out'),
- url(r'^profile/$', 'ietf.ietfauth.views.profile'),
-# (r'^login/(?P[a-z0-9.@]+)/(?P.+)$', 'ietf.ietfauth.views.url_login'),
- url(r'^testemail/$', 'ietf.ietfauth.views.test_email'),
- url(r'^create/$', 'ietf.ietfauth.views.create_account'),
- url(r'^create/confirm/(?P[^/]+)/$', 'ietf.ietfauth.views.confirm_account'),
- url(r'^reset/$', 'ietf.ietfauth.views.password_reset'),
- url(r'^reset/confirm/(?P[^/]+)/$', 'ietf.ietfauth.views.confirm_password_reset'),
- url(r'^confirmnewemail/(?P[^/]+)/$', 'ietf.ietfauth.views.confirm_new_email'),
- url(r'whitelist/add/?$', add_account_whitelist),
- url(r'^review/$', 'ietf.ietfauth.views.review_overview'),
+ url(r'^password/$', views.change_password),
+ url(r'^profile/$', views.profile),
+ url(r'^reset/$', views.password_reset),
+ url(r'^reset/confirm/(?P[^/]+)/$', views.confirm_password_reset),
+ url(r'^review/$', views.review_overview),
+ url(r'^testemail/$', views.test_email),
+ url(r'whitelist/add/?$', views.add_account_whitelist),
]
diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py
index 8b3c18ff1..56bb4f4a1 100644
--- a/ietf/ietfauth/views.py
+++ b/ietf/ietfauth/views.py
@@ -32,24 +32,27 @@
# Copyright The IETF Trust 2007, All Rights Reserved
+import importlib
+
from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date
from collections import defaultdict
-from django.conf import settings
-from django.http import Http404 #, HttpResponse, HttpResponseRedirect
-from django.shortcuts import render, redirect, get_object_or_404
-#from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
-from django.contrib.auth.decorators import login_required
-#from django.utils.http import urlquote
import django.core.signing
-from django.contrib.sites.models import Site
-from django.contrib.auth.models import User
from django import forms
+from django.contrib import messages
+from django.conf import settings
+from django.contrib.auth import update_session_auth_hash
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+from django.core.urlresolvers import reverse as urlreverse
+from django.http import Http404, HttpResponseRedirect #, HttpResponse,
+from django.shortcuts import render, redirect, get_object_or_404
import debug # pyflakes:ignore
from ietf.group.models import Role, Group
-from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm
+from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm, ChangePasswordForm
from ietf.ietfauth.forms import get_person_form, RoleEmailForm, NewEmailForm
from ietf.ietfauth.htpasswd import update_htpasswd_file
from ietf.ietfauth.utils import role_required
@@ -465,3 +468,46 @@ def review_overview(request):
'review_wishes': review_wishes,
'review_wish_form': review_wish_form,
})
+
+@login_required
+def change_password(request):
+ success = False
+ person = None
+
+ try:
+ person = request.user.person
+ except Person.DoesNotExist:
+ return render(request, 'registration/missing_person.html')
+
+ emails = Email.objects.filter(person=person, active=True).order_by('-primary','-time').first
+
+ if request.method == 'POST':
+ user = request.user
+ form = ChangePasswordForm(user, request.POST)
+ if form.is_valid():
+ new_password = form.cleaned_data["new_password"]
+
+ user.set_password(new_password)
+ user.save()
+ # password is also stored in htpasswd file
+ update_htpasswd_file(user.username, new_password)
+ # keep the session
+ update_session_auth_hash(request, user)
+
+ messages.success(request, "Your password was successfully changed")
+ return HttpResponseRedirect(urlreverse('ietf.ietfauth.views.profile'))
+
+ else:
+ form = ChangePasswordForm(request.user)
+
+ hlibname, hashername = settings.PASSWORD_HASHERS[0].rsplit('.',1)
+
+ hlib = importlib.import_module(hlibname)
+ hasher = getattr(hlib, hashername)
+ return render(request, 'registration/change_password.html', {
+ 'form': form,
+ 'success': success,
+ 'hasher': hasher,
+ })
+
+
diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py
index 3ec7a0a3d..cf59220de 100644
--- a/ietf/ipr/views.py
+++ b/ietf/ipr/views.py
@@ -32,6 +32,7 @@ from ietf.ipr.utils import (get_genitive, get_ipr_summary,
iprs_from_docs, related_docs)
from ietf.message.models import Message
from ietf.message.utils import infer_message
+from ietf.name.models import IprLicenseTypeName
from ietf.person.models import Person
from ietf.secr.utils.document import get_rfc_num, is_draft
from ietf.utils.draft_search import normalize_draftname
@@ -703,6 +704,7 @@ def get_details_tabs(ipr, selected):
('History', urlreverse('ipr_history', kwargs={ 'id': ipr.pk }))
]]
+@debug.trace
def show(request, id):
"""View of individual declaration"""
ipr = get_object_or_404(IprDisclosureBase, id=id).get_child()
@@ -717,6 +719,7 @@ def show(request, id):
return render(request, "ipr/details_view.html", {
'ipr': ipr,
'tabs': get_details_tabs(ipr, 'Disclosure'),
+ 'choices_abc': [ i.desc for i in IprLicenseTypeName.objects.filter(slug__in=['no-license', 'royalty-free', 'reasonable', ]) ],
'updates_iprs': ipr.relatedipr_source_set.all(),
'updated_by_iprs': ipr.relatedipr_target_set.filter(source__state="posted")
})
diff --git a/ietf/settings.py b/ietf/settings.py
index 7171f3ca1..216f5b6bc 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -54,6 +54,13 @@ ADMINS = (
('Ryan Cross', 'rcross@amsl.com'),
)
+PASSWORD_HASHERS = [
+ 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+ 'django.contrib.auth.hashers.SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.CryptPasswordHasher',
+]
+
ALLOWED_HOSTS = [".ietf.org", ".ietf.org.", "209.208.19.216", "4.31.198.44", ]
@@ -296,11 +303,12 @@ INSTALLED_APPS = (
'django.contrib.staticfiles',
# External apps
'bootstrap3',
+ 'django_markup',
+ 'django_password_strength',
'djangobwr',
'form_utils',
'tastypie',
'widget_tweaks',
- 'django_markup',
# IETF apps
'ietf.api',
'ietf.community',
@@ -782,7 +790,6 @@ SILENCED_SYSTEM_CHECKS = [
"fields.W342", # Setting unique=True on a ForeignKey has the same effect as using a OneToOneField.
]
-
# Put the production SECRET_KEY in settings_local.py, and also any other
# sensitive or site-specific changes. DO NOT commit settings_local.py to svn.
from settings_local import * # pyflakes:ignore pylint: disable=wildcard-import
diff --git a/ietf/static/ietf/js/password_strength.js b/ietf/static/ietf/js/password_strength.js
new file mode 100644
index 000000000..a1fd393f5
--- /dev/null
+++ b/ietf/static/ietf/js/password_strength.js
@@ -0,0 +1,130 @@
+// Taken from django-password-strength, with changes to use the bower-managed zxcvbn.js The
+// bower-managed zxcvbn.js is kept up-to-date to a larger extent than the copy packaged with
+// the django-password-strength component.
+(function($, window, document, undefined){
+ window.djangoPasswordStrength = {
+ config: {
+ passwordClass: 'password_strength',
+ confirmationClass: 'password_confirmation'
+ },
+
+ init: function (config) {
+ var self = this;
+ // Setup configuration
+ if ($.isPlainObject(config)) {
+ $.extend(self.config, config);
+ }
+
+ self.initListeners();
+ },
+
+ initListeners: function() {
+ var self = this;
+ var body = $('body');
+
+ $('.' + self.config.passwordClass).on('keyup', function() {
+ var password_strength_bar = $(this).parent().find('.password_strength_bar');
+ var password_strength_info = $(this).parent().find('.password_strength_info');
+
+ if( $(this).val() ) {
+ var result = zxcvbn( $(this).val() );
+
+ if( result.score < 3 ) {
+ password_strength_bar.removeClass('progress-bar-success').addClass('progress-bar-warning');
+ password_strength_info.find('.label').removeClass('hidden');
+ } else {
+ password_strength_bar.removeClass('progress-bar-warning').addClass('progress-bar-success');
+ password_strength_info.find('.label').addClass('hidden');
+ }
+
+ password_strength_bar.width( ((result.score+1)/5)*100 + '%' ).attr('aria-valuenow', result.score + 1);
+ // henrik@levkowetz.com -- this is the only changed line:
+ password_strength_info.find('.password_strength_time').html(result.crack_times_display.online_no_throttling_10_per_second);
+ password_strength_info.removeClass('hidden');
+ } else {
+ password_strength_bar.removeClass('progress-bar-success').addClass('progress-bar-warning');
+ password_strength_bar.width( '0%' ).attr('aria-valuenow', 0);
+ password_strength_info.addClass('hidden');
+ }
+ self.match_passwords($(this));
+ });
+
+ var timer = null;
+ $('.' + self.config.confirmationClass).on('keyup', function() {
+ var password_field;
+ var confirm_with = $(this).data('confirm-with');
+
+ if( confirm_with ) {
+ password_field = $('#' + confirm_with);
+ } else {
+ password_field = $('.' + self.config.passwordClass);
+ }
+
+ if (timer !== null) clearTimeout(timer);
+
+ timer = setTimeout(function(){
+ self.match_passwords(password_field);
+ }, 400);
+ });
+ },
+
+ display_time: function(seconds) {
+ var minute = 60;
+ var hour = minute * 60;
+ var day = hour * 24;
+ var month = day * 31;
+ var year = month * 12;
+ var century = year * 100;
+
+ // Provide fake gettext for when it is not available
+ if( typeof gettext !== 'function' ) { gettext = function(text) { return text; }; };
+
+ if( seconds < minute ) return gettext('only an instant');
+ if( seconds < hour) return (1 + Math.ceil(seconds / minute)) + ' ' + gettext('minutes');
+ if( seconds < day) return (1 + Math.ceil(seconds / hour)) + ' ' + gettext('hours');
+ if( seconds < month) return (1 + Math.ceil(seconds / day)) + ' ' + gettext('days');
+ if( seconds < year) return (1 + Math.ceil(seconds / month)) + ' ' + gettext('months');
+ if( seconds < century) return (1 + Math.ceil(seconds / year)) + ' ' + gettext('years');
+
+ return gettext('centuries');
+ },
+
+ match_passwords: function(password_field, confirmation_fields) {
+ var self = this;
+ // Optional parameter: if no specific confirmation field is given, check all
+ if( confirmation_fields === undefined ) { confirmation_fields = $('.' + self.config.confirmationClass) }
+ if( confirmation_fields === undefined ) { return; }
+
+ var password = password_field.val();
+
+ confirmation_fields.each(function(index, confirm_field) {
+ var confirm_value = $(confirm_field).val();
+ var confirm_with = $(confirm_field).data('confirm-with');
+
+ if( confirm_with && confirm_with == password_field.attr('id')) {
+ if( confirm_value && password ) {
+ if (confirm_value === password) {
+ $(confirm_field).parent().find('.password_strength_info').addClass('hidden');
+ } else {
+ $(confirm_field).parent().find('.password_strength_info').removeClass('hidden');
+ }
+ } else {
+ $(confirm_field).parent().find('.password_strength_info').addClass('hidden');
+ }
+ }
+ });
+
+ // If a password field other than our own has been used, add the listener here
+ if( !password_field.hasClass(self.config.passwordClass) && !password_field.data('password-listener') ) {
+ password_field.on('keyup', function() {
+ self.match_passwords($(this));
+ });
+ password_field.data('password-listener', true);
+ }
+ }
+ };
+
+ // Call the init for backwards compatibility
+ djangoPasswordStrength.init();
+
+})(jQuery, window, document);
diff --git a/ietf/templates/base/menu_user.html b/ietf/templates/base/menu_user.html
index 79f1a72c5..697a145b8 100644
--- a/ietf/templates/base/menu_user.html
+++ b/ietf/templates/base/menu_user.html
@@ -16,14 +16,16 @@
{% else %}
{% if user.is_authenticated %}
Sign out
- Edit profile
+ Account info
{% else %}
Sign in
Password reset
{% endif %}
{% endif %}
- {% if request.user.is_authenticated %}Manage account{% else %}New account{% endif %}
+ {% if not request.user.is_authenticated %}
+ New account
+ {% endif %}
Preferences
{% if user|has_role:"Reviewer" %}
diff --git a/ietf/templates/ipr/details_view.html b/ietf/templates/ipr/details_view.html
index bbac6ef63..3dca63f7f 100644
--- a/ietf/templates/ipr/details_view.html
+++ b/ietf/templates/ipr/details_view.html
@@ -200,9 +200,19 @@
specification, is as follows(select one licensing declaration option only):
+ {% if ipr.licensing.slug == "provided-later" %}
+
+ Possible licencing choices a), b), and c) when Licencing Declaration to be Provided Later:
+
+ {% for desc in choices_abc %}
+ {{ desc}}
+ {% endfor %}
+
+
+ {% endif %}
Licensing
- {% if ipr.licensing.slug == "later" %}{{ ipr.licensing.desc|slice:"2:"|slice:":43" }}{% else %}{{ ipr.licensing.desc|slice:"2:" }}{% endif %}
+ {% if ipr.licensing.slug == "provided-later" %}{{ ipr.licensing.desc|slice:"2:"|slice:":117" }}){% else %}{{ ipr.licensing.desc|slice:"2:" }}{% endif %}
Licensing information, comments, notes, or URL for further information
{{ ipr.licensing_comments|default:"(No information submitted)"|linebreaks }}
diff --git a/ietf/templates/registration/change_password.html b/ietf/templates/registration/change_password.html
index 26321558a..34518b917 100644
--- a/ietf/templates/registration/change_password.html
+++ b/ietf/templates/registration/change_password.html
@@ -3,30 +3,56 @@
{% load origin %}
{% load bootstrap3 %}
+{% load staticfiles %}
-{% block title %}Change password{% endblock %}
+{% block title %}Account creation{% endblock %}
+
+{% block js %}
+ {{ block.super }}
+
+
+{% endblock %}
{% block content %}
{% origin %}
{% if success %}
- Password change successful
-
- Your password has been updated.
- Sign in
+ Your password was successfully changed.
{% else %}
- Change password
+
+
+
+
Change password
-
You can change the password below for your user {{ username }} below.
-
+ {% buttons %}
+
Change password
+ {% endbuttons %}
+
+
+
+ This password change form uses the
+
zxcvbn
+ password strength estimator to give an indication of password strength.
+ The crack times given assume online attack without rate limiting,
+ at a rate of 10 attempts per second.
+
+
+
+ The datatracker currently uses a {{ hasher.algorithm }} -based
+ password hasher with
+ {% if hasher.iterations %}{{ hasher.iterations }} iterations{% else %}{{ hasher.rounds }} rounds{% endif %} .
+ Calculating offline attack time if password hashes wouldleak is left
+ as an excercise for the reader.
+
+
+
+
+
{% endif %}
{% endblock %}
diff --git a/ietf/templates/registration/create.html b/ietf/templates/registration/create.html
index 2c12c9a1e..7bb32242c 100644
--- a/ietf/templates/registration/create.html
+++ b/ietf/templates/registration/create.html
@@ -18,22 +18,9 @@
{% else %}
-
+
+
Account creation
-
Please enter your email address in order to create a new datatracker account.
-
-
-
-
-
Other options
-
If you already have an account and want to use a new email address,
please go to your account profile page and
@@ -49,7 +36,20 @@
Reset your password
+
+
+
+
Please enter your email address in order to create your datatracker account.
+
+
{% endif %}
{% endblock %}
diff --git a/ietf/templates/registration/edit_profile.html b/ietf/templates/registration/edit_profile.html
index 9c4bccad5..44c89d04f 100644
--- a/ietf/templates/registration/edit_profile.html
+++ b/ietf/templates/registration/edit_profile.html
@@ -22,6 +22,13 @@
+
+