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)))