Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ckanext/security/plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ckanext/security/templates/user/snippets/login_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<div id="login-form">

<div id="login-fields">
{{ 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"]) }}
</div>
Expand Down
3 changes: 2 additions & 1 deletion ckanext/security/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
21 changes: 19 additions & 2 deletions ckanext/security/validators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# encoding: utf-8
import six
import string
import collections

from ckan import authz
from ckan.common import _
Expand All @@ -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]
Expand All @@ -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)))


Expand Down