diff --git a/ckanext/security/plugin/__init__.py b/ckanext/security/plugin/__init__.py index f027db3..56c8b4a 100644 --- a/ckanext/security/plugin/__init__.py +++ b/ckanext/security/plugin/__init__.py @@ -9,6 +9,7 @@ validate_upload ) from ckanext.security.logic import auth, action +from ckanext.security import validators from ckanext.security.helpers import security_enable_totp from ckanext.security.plugin.flask_plugin import MixinPlugin @@ -22,6 +23,7 @@ class CkanSecurityPlugin(MixinPlugin, p.SingletonPlugin): p.implements(p.IActions) p.implements(p.IAuthFunctions) p.implements(p.ITemplateHelpers) + p.implements(p.IValidators) # BEGIN Hooks for IConfigurer @@ -46,6 +48,14 @@ def update_config(self, config): # END Hooks for IConfigurer + # BEGIN hooks for IValidators + def get_validators(self): + return { + 'user_password_validator': validators.user_password_validator, + 'old_username_validator': validators.old_username_validator, + } + # END hooks for IValidators + # BEGIN Hooks for IResourceController # CKAN < 2.10 diff --git a/ckanext/security/templates/user/snippets/login_form.html b/ckanext/security/templates/user/snippets/login_form.html index 6fd0c1e..32b0e16 100644 --- a/ckanext/security/templates/user/snippets/login_form.html +++ b/ckanext/security/templates/user/snippets/login_form.html @@ -25,7 +25,7 @@
- {{ form.input('login', label=_("Username"), id='field-login', value="", error=username_error, classes=["control-medium"]) }} + {{ form.input('login', label=_("Username or Email"), id='field-login', value="", error=username_error, classes=["control-medium"]) }} {{ form.input('password', label=_("Password"), id='field-password', type="password", value="", error=password_error, classes=["control-medium"]) }}
diff --git a/ckanext/security/utils.py b/ckanext/security/utils.py index d07f36e..0b4300c 100644 --- a/ckanext/security/utils.py +++ b/ckanext/security/utils.py @@ -125,6 +125,8 @@ def login(): user_name = identity['login'] user = model.User.by_name(user_name) + if not user: + user = model.User.by_email(user_name) login_throttle_key = get_login_throttle_key(request, user_name) if login_throttle_key is None: @@ -180,7 +182,6 @@ def login(): log.info('User %s supplied invalid 2fa code', user_name) response_status = 403 throttle.increment() - return (response_status, json.dumps(res)) except Exception as err: diff --git a/ckanext/security/validators.py b/ckanext/security/validators.py index 77ba480..00024f1 100644 --- a/ckanext/security/validators.py +++ b/ckanext/security/validators.py @@ -1,6 +1,7 @@ # encoding: utf-8 import six import string +import collections from ckan import authz from ckan.common import _ @@ -10,9 +11,24 @@ MIN_LEN_ERROR = ( 'Your password must be {} characters or longer, and consist of at least ' 'three of the following character sets: uppercase characters, lowercase ' - 'characters, digits, punctuation & special characters.' + 'characters, digits, punctuation & special characters, and not contain ' + 'too many repeated characters.' ) +def _too_many_repeated_characters(value): + """ does the password contain too many repeated characters + + Returns True if the most frequent character is >= 1/3 of the characters. + e.g. "password" is false: ct(s)==2 < 8/3 + "aaaaword" is true: ct(a)==4 > 8/3 + + :param s: proposed password + :returns: boolean, True if password is ok by this criteria + """ + char_counts = collections.Counter(value) + # note, will fail on empty password, but caller checks MIN_PASSWORD_LENGTH + return (char_counts.most_common(1)[0][1] >= (len(value)/3)) + def user_password_validator(key, data, errors, context): value = data[key] @@ -31,7 +47,8 @@ def user_password_validator(key, data, errors, context): any(x.isdigit() for x in value), any(x in string.punctuation for x in value) ] - if len(value) < MIN_PASSWORD_LENGTH or sum(rules) < 3: + if len(value) < MIN_PASSWORD_LENGTH or sum(rules) < 3 \ + or _too_many_repeated_characters(value): raise Invalid(_(MIN_LEN_ERROR.format(MIN_PASSWORD_LENGTH)))