fields: password field (#3201)
This commit is contained in:
parent
66a72ab979
commit
989d5f8a64
|
@ -11,6 +11,9 @@ from wcs import publisher
|
|||
from wcs.qommon.form import *
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
|
||||
import __builtin__
|
||||
__builtin__.__dict__['ngettext'] = lambda x, y, z: x
|
||||
|
||||
def setup_module(module):
|
||||
cleanup()
|
||||
|
||||
|
@ -27,7 +30,7 @@ def teardown_module(module):
|
|||
|
||||
class MockHtmlForm(object):
|
||||
def __init__(self, widget):
|
||||
widget = copy.copy(widget)
|
||||
widget = copy.deepcopy(widget)
|
||||
form = Form(method='post', use_tokens=False, enctype='application/x-www-form-urlencoded')
|
||||
form.widgets.append(widget)
|
||||
self.as_html = str(form.render())
|
||||
|
@ -49,7 +52,6 @@ def mock_form_submission(req, widget, html_vars={}, click=None):
|
|||
form.set_form_value(k, v)
|
||||
if click is not None:
|
||||
request = form.form.click(click)
|
||||
print 'data:', request.data
|
||||
req.form = parse_query(request.data, 'utf-8')
|
||||
else:
|
||||
req.form = form.get_parsed_query()
|
||||
|
@ -121,3 +123,62 @@ def test_table_list_rows_required():
|
|||
mock_form_submission(req, widget, click='test$add_element')
|
||||
widget = TableListRowsWidget('test', columns=['a', 'b', 'c'], required=True)
|
||||
assert not widget.has_error()
|
||||
|
||||
|
||||
def test_passwordentry_widget_success():
|
||||
widget = PasswordEntryWidget('test')
|
||||
form = MockHtmlForm(widget)
|
||||
assert 'name="test$pwd1"' in form.as_html
|
||||
req.form = {}
|
||||
assert widget.parse() is None
|
||||
|
||||
widget = PasswordEntryWidget('test', value={'plain': 'foo'}, formats=['plain'])
|
||||
req.form = {}
|
||||
assert widget.parse() == {'plain': 'foo'}
|
||||
|
||||
widget = PasswordEntryWidget('test', formats=['plain'])
|
||||
req.form = {}
|
||||
mock_form_submission(req, widget, {'test$pwd1': ''})
|
||||
assert widget.parse() is None
|
||||
|
||||
widget = PasswordEntryWidget('test', formats=['plain'])
|
||||
req.form = {}
|
||||
mock_form_submission(req, widget, {'test$pwd1': 'foo', 'test$pwd2': 'foo'})
|
||||
assert widget.parse() == {'plain': 'foo'}
|
||||
|
||||
|
||||
def test_passwordentry_widget_errors():
|
||||
# mismatch
|
||||
widget = PasswordEntryWidget('test', formats=['plain'])
|
||||
req.form = {}
|
||||
mock_form_submission(req, widget, {'test$pwd1': 'foo', 'test$pwd2': 'bar'})
|
||||
assert widget.parse() is None
|
||||
assert widget.has_error() is True
|
||||
|
||||
# too short
|
||||
widget = PasswordEntryWidget('test', formats=['plain'], min_length=4)
|
||||
req.form = {}
|
||||
mock_form_submission(req, widget, {'test$pwd1': 'foo', 'test$pwd2': 'foo'})
|
||||
assert widget.parse() is None
|
||||
assert widget.has_error() is True
|
||||
|
||||
# uppercases
|
||||
widget = PasswordEntryWidget('test', formats=['plain'], count_uppercase=1)
|
||||
req.form = {}
|
||||
mock_form_submission(req, widget, {'test$pwd1': 'foo', 'test$pwd2': 'foo'})
|
||||
assert widget.parse() is None
|
||||
assert widget.has_error() is True
|
||||
|
||||
# digits
|
||||
widget = PasswordEntryWidget('test', formats=['plain'], count_digit=1)
|
||||
req.form = {}
|
||||
mock_form_submission(req, widget, {'test$pwd1': 'foo', 'test$pwd2': 'foo'})
|
||||
assert widget.parse() is None
|
||||
assert widget.has_error() is True
|
||||
|
||||
# specials
|
||||
widget = PasswordEntryWidget('test', formats=['plain'], count_special=1)
|
||||
req.form = {}
|
||||
mock_form_submission(req, widget, {'test$pwd1': 'foo', 'test$pwd2': 'foo'})
|
||||
assert widget.parse() is None
|
||||
assert widget.has_error() is True
|
||||
|
|
|
@ -1422,6 +1422,68 @@ class RankedItemsField(WidgetField):
|
|||
register_field_class(RankedItemsField)
|
||||
|
||||
|
||||
class PasswordField(WidgetField):
|
||||
key = 'password'
|
||||
description = N_('Password')
|
||||
|
||||
min_length = 0
|
||||
max_length = 0
|
||||
count_uppercase = 0
|
||||
count_lowercase = 0
|
||||
count_digit = 0
|
||||
count_special = 0
|
||||
confirmation_title = None
|
||||
formats = ['sha1']
|
||||
extra_attributes = ['formats', 'min_length', 'max_length',
|
||||
'count_uppercase', 'count_lowercase', 'count_digit',
|
||||
'count_special', 'confirmation_title']
|
||||
|
||||
widget_class = PasswordEntryWidget
|
||||
|
||||
def get_admin_attributes(self):
|
||||
return WidgetField.get_admin_attributes(self) + self.extra_attributes
|
||||
|
||||
def fill_admin_form(self, form):
|
||||
WidgetField.fill_admin_form(self, form)
|
||||
formats = [('plain', _('Plain')),
|
||||
('md5', _('MD5')),
|
||||
('sha1', _('SHA1')),
|
||||
]
|
||||
form.add(CheckboxesWidget, 'formats', title=_('Storage formats'),
|
||||
value=self.formats, elements=formats, inline=True)
|
||||
form.add(IntWidget, 'min_length', title=_('Minimum length'),
|
||||
value=self.min_length)
|
||||
form.add(IntWidget, 'max_length', title=_('Maximum password length'),
|
||||
value=self.max_length,
|
||||
hint=_('0 for unlimited length'))
|
||||
form.add(IntWidget, 'count_uppercase',
|
||||
title=_('Minimum number of uppercase characters'),
|
||||
value=self.count_uppercase)
|
||||
form.add(IntWidget, 'count_lowercase',
|
||||
title=_('Minimum number of lowercase characters'),
|
||||
value=self.count_lowercase)
|
||||
form.add(IntWidget, 'count_digit',
|
||||
title=_('Minimum number of digits'),
|
||||
value=self.count_digit)
|
||||
form.add(IntWidget, 'count_special',
|
||||
title=_('Minimum number of special characters'),
|
||||
value=self.count_special)
|
||||
form.add(StringWidget, 'confirmation_title', size=50,
|
||||
title=_('Label for confirmation input'),
|
||||
value=self.confirmation_title)
|
||||
|
||||
def get_view_value(self, value):
|
||||
return '*'*8
|
||||
|
||||
def get_csv_value(self, value, hint=None):
|
||||
return [self.get_view_value(value)]
|
||||
|
||||
def get_rst_view_value(self, value, indent=''):
|
||||
return indent + self.get_view_value(value)
|
||||
|
||||
register_field_class(PasswordField)
|
||||
|
||||
|
||||
def get_field_class_by_type(type):
|
||||
for k in field_classes:
|
||||
if k.key == type:
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import cStringIO
|
||||
import mimetypes
|
||||
import os
|
||||
|
@ -24,6 +25,8 @@ import time
|
|||
import random
|
||||
import datetime
|
||||
import itertools
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from storage import atomic_write
|
||||
|
||||
|
@ -1729,3 +1732,144 @@ class ColourWidget(SingleSelectWidget):
|
|||
SingleSelectWidget._parse(self, request)
|
||||
if self.value is not None:
|
||||
self.value = self.value.upper()
|
||||
|
||||
|
||||
class PasswordEntryWidget(CompositeWidget):
|
||||
min_length = 0
|
||||
max_length = 0
|
||||
count_uppercase = 0
|
||||
count_lowercase = 0
|
||||
count_digit = 0
|
||||
count_special = 0
|
||||
|
||||
def __init__(self, name, value=None, **kwargs):
|
||||
# hint will be displayed with pwd1 widget
|
||||
hint = kwargs.pop('hint', None)
|
||||
CompositeWidget.__init__(self, name, value, **kwargs)
|
||||
self.min_length = kwargs.get('min_length', 0)
|
||||
self.max_length = kwargs.get('max_length', 0)
|
||||
self.count_uppercase = kwargs.get('count_uppercase', 0)
|
||||
self.count_lowercase = kwargs.get('count_lowercase', 0)
|
||||
self.count_digit = kwargs.get('count_digit', 0)
|
||||
self.count_special = kwargs.get('count_special', 0)
|
||||
confirmation_title = kwargs.get('confirmation_title') or _('Confirmation')
|
||||
|
||||
self.formats = kwargs.get('formats', ['sha1'])
|
||||
if not self.attrs.get('readonly'):
|
||||
self.add(PasswordWidget, name='pwd1', title='',
|
||||
value='',
|
||||
required=kwargs.get('required', False),
|
||||
autocomplete='off',
|
||||
hint=hint)
|
||||
self.add(PasswordWidget, name='pwd2', title=confirmation_title,
|
||||
required=kwargs.get('required', False),
|
||||
autocomplete='off')
|
||||
else:
|
||||
encoded_value = base64.encodestring(json.dumps(value))
|
||||
if value:
|
||||
fake_value = '*' * 8
|
||||
else:
|
||||
fake_value = ''
|
||||
self.add(HtmlWidget, 'hashed', title=htmltext(
|
||||
'<input value="%s" readonly="readonly">'\
|
||||
'<input type="hidden" name="%s$encoded" value="%s"></input>' % (
|
||||
fake_value, self.name, encoded_value)))
|
||||
|
||||
def render_content(self):
|
||||
if self.attrs.get('readonly'):
|
||||
return CompositeWidget.render_content(self)
|
||||
get_response().add_javascript(['jquery.js', 'jquery.passstrength.js'])
|
||||
r = TemplateIO(html=True)
|
||||
r += CompositeWidget.render_content(self)
|
||||
r += htmltext('''<script>
|
||||
$(function() {
|
||||
$('input[id="form_%(form_id)s"]').passStrengthify({
|
||||
levels: ["%(veryweak)s", "%(veryweak)s", "%(weak)s", "%(weak)s", "%(moderate)s", "%(good)s", "%(strong)s", "%(verystrong)s"],
|
||||
minimum: %(min_length)s,
|
||||
labels: {
|
||||
passwordStrength: "%(password_strength)s",
|
||||
tooShort: "%(tooshort)s"
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>''') % {'form_id': self.get_widget('pwd1').get_name(),
|
||||
'min_length': self.min_length,
|
||||
'veryweak': _('Very weak'),
|
||||
'weak': _('Weak'),
|
||||
'moderate': _('Moderate'),
|
||||
'good': _('Good'),
|
||||
'strong': _('Strong'),
|
||||
'verystrong': _('Very strong'),
|
||||
'password_strength': _('Password strength:'),
|
||||
'tooshort': _('Too short')}
|
||||
return r.getvalue()
|
||||
|
||||
def _parse(self, request):
|
||||
CompositeWidget._parse(self, request)
|
||||
if request.form.get('%s$encoded' % self.name):
|
||||
self.value = json.loads(base64.decodestring(
|
||||
request.form.get('%s$encoded' % self.name)))
|
||||
return
|
||||
pwd1 = self.get('pwd1') or ''
|
||||
pwd2 = self.get('pwd2') or ''
|
||||
|
||||
if not self.get_widget('pwd1'):
|
||||
# we are in read-only mode, stop here.
|
||||
return
|
||||
|
||||
set_errors = []
|
||||
min_len = self.min_length
|
||||
if len(pwd1) < min_len:
|
||||
set_errors.append(_('Password is too short. It must be at least %d characters.') % min_len)
|
||||
|
||||
max_len = self.max_length
|
||||
if max_len and len(pwd1) > max_len:
|
||||
set_errors.append(_('Password is too long. It must be at most %d characters.') % max_len)
|
||||
|
||||
count = self.count_uppercase
|
||||
if len(filter(lambda c: c.isupper(), pwd1)) < count:
|
||||
set_errors.append(
|
||||
ngettext('Password must contain an uppercase character.',
|
||||
'Password must contain at least %(count)d uppercase characters.',
|
||||
count) % {'count': count})
|
||||
|
||||
count = self.count_lowercase
|
||||
if len(filter(lambda c: c.islower(), pwd1)) < count:
|
||||
set_errors.append(
|
||||
ngettext('Password must contain a lowercase character.',
|
||||
'Password must contain at least %(count)d lowercase characters.',
|
||||
count) % {'count': count})
|
||||
|
||||
count = self.count_digit
|
||||
if len(filter(lambda c: c.isdigit(), pwd1)) < self.count_digit:
|
||||
set_errors.append(
|
||||
ngettext('Password must contain a digit.',
|
||||
'Password must contain at least %(count)d digits.',
|
||||
count) % {'count': count})
|
||||
|
||||
count = self.count_special
|
||||
if len(filter(lambda c: not c.isalnum(), pwd1)) < count:
|
||||
set_errors.append(
|
||||
ngettext('Password must contain a special character.',
|
||||
'Password must contain at least %(count)d special characters.',
|
||||
count) % {'count': count})
|
||||
|
||||
if pwd1 != pwd2:
|
||||
self.get_widget('pwd2').set_error(_('Passwords do not match'))
|
||||
pwd1 = None
|
||||
if set_errors:
|
||||
self.get_widget('pwd1').set_error(' '.join(set_errors))
|
||||
pwd1 = None
|
||||
|
||||
PASSWORD_FORMATS = {
|
||||
'plain': lambda x: x,
|
||||
'md5': lambda x: hashlib.md5(x).hexdigest(),
|
||||
'sha1': lambda x: hashlib.sha1(x).hexdigest(),
|
||||
}
|
||||
|
||||
if pwd1:
|
||||
self.value = {}
|
||||
for fmt in self.formats:
|
||||
self.value[fmt] = PASSWORD_FORMATS[fmt](pwd1)
|
||||
else:
|
||||
self.value = None
|
||||
|
|
|
@ -40,7 +40,9 @@ SQL_TYPE_MAPPING = {
|
|||
'table': 'text[][]',
|
||||
'table-select': 'text[][]',
|
||||
'tablerows': 'text[][]',
|
||||
# mapping of dicts
|
||||
'ranked-items': 'text[][]',
|
||||
'password': 'text[][]',
|
||||
}
|
||||
|
||||
def get_name_as_sql_identifier(name):
|
||||
|
@ -528,7 +530,7 @@ class SqlMixin:
|
|||
continue
|
||||
value = data.get(field.id)
|
||||
if value is not None:
|
||||
if field.key == 'ranked-items':
|
||||
if field.key in ('ranked-items', 'password'):
|
||||
# turn {'poire': 2, 'abricot': 1, 'pomme': 3} into an array
|
||||
value = [[x, str(y)] for x, y in value.items()]
|
||||
elif sql_type == 'varchar':
|
||||
|
@ -558,6 +560,11 @@ class SqlMixin:
|
|||
for data, rank in value:
|
||||
d[data] = int(rank)
|
||||
value = d
|
||||
elif field.key == 'password':
|
||||
d = {}
|
||||
for fmt, val in value:
|
||||
d[fmt] = val
|
||||
value = d
|
||||
if sql_type == 'date':
|
||||
value = value.timetuple()
|
||||
elif sql_type == 'bytea':
|
||||
|
|
Loading…
Reference in New Issue