PCI-DSS Password strength requirements
This patch implements PCI-DSS password strength requirements: * PCI-DSS 8.2.3: Passwords/phrases must meet the following: 1) Require a minimum length of at least seven characters. 2) Contain both numeric and alphabetic characters. Alternatively, the passwords/phrases must have complexity and strength at least equivalent to the parameters specified above. Partially-implements: blueprint pci-dss Change-Id: I32ef8deb0a7c48511229f95f7cc973ab0d183372
This commit is contained in:
parent
be86bb1206
commit
5d90bfa61e
|
@ -11,10 +11,42 @@
|
|||
# under the License.
|
||||
"""Internal implementation of request body validating middleware."""
|
||||
|
||||
import re
|
||||
|
||||
import jsonschema
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
import six
|
||||
|
||||
from keystone import exception
|
||||
from keystone.i18n import _
|
||||
from keystone.i18n import _, _LE
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO(rderose): extend schema validation and add this check there
|
||||
def validate_password(password):
|
||||
pattern = CONF.security_compliance.password_regex
|
||||
if pattern:
|
||||
if not isinstance(password, six.string_types):
|
||||
detail = _("Password must be a string type")
|
||||
raise exception.PasswordValidationError(detail=detail)
|
||||
try:
|
||||
if not re.match(pattern, password):
|
||||
pattern_desc = (
|
||||
CONF.security_compliance.password_regex_description)
|
||||
detail = _("The password does not meet the requirements: "
|
||||
"%(message)s") % {'message': pattern_desc}
|
||||
raise exception.PasswordValidationError(detail=detail)
|
||||
except re.error:
|
||||
msg = _LE("Unable to validate password due to invalid regular "
|
||||
"expression - password_regex: ")
|
||||
LOG.error(msg, pattern)
|
||||
detail = _("Unable to validate password due to invalid "
|
||||
"configuration")
|
||||
raise exception.PasswordValidationError(detail=detail)
|
||||
|
||||
|
||||
class SchemaValidator(object):
|
||||
|
|
|
@ -86,7 +86,7 @@ depends on the `sql` backend for the `[identity] driver`.
|
|||
|
||||
password_regex = cfg.StrOpt(
|
||||
'password_regex',
|
||||
default='^$',
|
||||
default=None,
|
||||
help=utils.fmt("""
|
||||
The regular expression used to validate password strength requirements. By
|
||||
default, the regular expression will match any password. The following is an
|
||||
|
@ -95,6 +95,16 @@ minimum length of 7 characters: ^(?=.*\d)(?=.*[a-zA-Z]).{7,}$ This feature
|
|||
depends on the `sql` backend for the `[identity] driver`.
|
||||
"""))
|
||||
|
||||
password_regex_description = cfg.StrOpt(
|
||||
'password_regex_description',
|
||||
default=None,
|
||||
help=utils.fmt("""
|
||||
Describe your password regular expression here in language for humans. If a
|
||||
password fails to match the regular expression, the contents of this
|
||||
configuration variable will be returned to users to explain why their
|
||||
requested password was insufficient.
|
||||
"""))
|
||||
|
||||
|
||||
GROUP_NAME = __name__.split('.')[-1]
|
||||
ALL_OPTS = [
|
||||
|
@ -105,6 +115,7 @@ ALL_OPTS = [
|
|||
unique_last_password_count,
|
||||
password_change_limit_per_day,
|
||||
password_regex,
|
||||
password_regex_description,
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -93,6 +93,10 @@ class URLValidationError(ValidationError):
|
|||
" %(url)s")
|
||||
|
||||
|
||||
class PasswordValidationError(ValidationError):
|
||||
message_format = _("Invalid password: %(detail)s")
|
||||
|
||||
|
||||
class SchemaValidationError(ValidationError):
|
||||
# NOTE(lbragstad): For whole OpenStack message consistency, this error
|
||||
# message has been written in a format consistent with WSME.
|
||||
|
|
|
@ -29,6 +29,7 @@ from keystone.common import clean
|
|||
from keystone.common import dependency
|
||||
from keystone.common import driver_hints
|
||||
from keystone.common import manager
|
||||
from keystone.common.validation import validators
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
from keystone.i18n import _, _LW
|
||||
|
@ -850,6 +851,8 @@ class Manager(manager.Manager):
|
|||
@exception_translated('user')
|
||||
def create_user(self, user_ref, initiator=None):
|
||||
user = user_ref.copy()
|
||||
if 'password' in user:
|
||||
validators.validate_password(user['password'])
|
||||
user['name'] = clean.user_name(user['name'])
|
||||
user.setdefault('enabled', True)
|
||||
user['enabled'] = clean.user_enabled(user['enabled'])
|
||||
|
@ -933,6 +936,8 @@ class Manager(manager.Manager):
|
|||
def update_user(self, user_id, user_ref, initiator=None):
|
||||
old_user_ref = self.get_user(user_id)
|
||||
user = user_ref.copy()
|
||||
if 'password' in user:
|
||||
validators.validate_password(user['password'])
|
||||
if 'name' in user:
|
||||
user['name'] = clean.user_name(user['name'])
|
||||
if 'enabled' in user:
|
||||
|
@ -1225,6 +1230,8 @@ class Manager(manager.Manager):
|
|||
# authenticate() will raise an AssertionError if authentication fails
|
||||
self.authenticate(context, user_id, original_password)
|
||||
|
||||
validators.validate_password(new_password)
|
||||
|
||||
update_dict = {'password': new_password}
|
||||
self.update_user(user_id, update_dict)
|
||||
|
||||
|
|
|
@ -808,3 +808,58 @@ class UserSelfServiceChangingPasswordsTestCase(test_v3.RestfulTestCase):
|
|||
|
||||
self.assertNotIn(self.user_ref['password'], log_fix.output)
|
||||
self.assertNotIn(new_password, log_fix.output)
|
||||
|
||||
|
||||
class PasswordValidationTestCase(UserSelfServiceChangingPasswordsTestCase):
|
||||
"""Test password validation."""
|
||||
|
||||
def setUp(self):
|
||||
super(PasswordValidationTestCase, self).setUp()
|
||||
# passwords requires: 1 letter, 1 digit, 7 chars
|
||||
self.config_fixture.config(group='security_compliance',
|
||||
password_regex=(
|
||||
'^(?=.*\d)(?=.*[a-zA-Z]).{7,}$'))
|
||||
|
||||
def test_create_user_with_invalid_password(self):
|
||||
user = unit.new_user_ref(domain_id=self.domain_id)
|
||||
user['password'] = 'simple'
|
||||
self.post('/users', body={'user': user}, token=self.get_admin_token(),
|
||||
expected_status=http_client.BAD_REQUEST)
|
||||
|
||||
def test_update_user_with_invalid_password(self):
|
||||
user = unit.create_user(self.identity_api,
|
||||
domain_id=self.domain['id'])
|
||||
user['password'] = 'simple'
|
||||
self.patch('/users/%(user_id)s' % {
|
||||
'user_id': user['id']},
|
||||
body={'user': user},
|
||||
expected_status=http_client.BAD_REQUEST)
|
||||
|
||||
def test_changing_password_with_simple_password_strength(self):
|
||||
# password requires: any non-whitespace character
|
||||
self.config_fixture.config(group='security_compliance',
|
||||
password_regex='[\S]+')
|
||||
self.change_password(password='simple',
|
||||
original_password=self.user_ref['password'],
|
||||
expected_status=http_client.NO_CONTENT)
|
||||
|
||||
def test_changing_password_with_strong_password_strength(self):
|
||||
self.change_password(password='mypassword2',
|
||||
original_password=self.user_ref['password'],
|
||||
expected_status=http_client.NO_CONTENT)
|
||||
|
||||
def test_changing_password_with_strong_password_strength_fails(self):
|
||||
# no digit
|
||||
self.change_password(password='mypassword',
|
||||
original_password=self.user_ref['password'],
|
||||
expected_status=http_client.BAD_REQUEST)
|
||||
|
||||
# no letter
|
||||
self.change_password(password='12345678',
|
||||
original_password=self.user_ref['password'],
|
||||
expected_status=http_client.BAD_REQUEST)
|
||||
|
||||
# less than 7 chars
|
||||
self.change_password(password='mypas2',
|
||||
original_password=self.user_ref['password'],
|
||||
expected_status=http_client.BAD_REQUEST)
|
||||
|
|
|
@ -2083,3 +2083,46 @@ class OAuth1ValidationTestCase(unit.BaseTestCase):
|
|||
request_to_validate = {'description': None}
|
||||
self.create_consumer_validator.validate(request_to_validate)
|
||||
self.update_consumer_validator.validate(request_to_validate)
|
||||
|
||||
|
||||
class PasswordValidationTestCase(unit.TestCase):
|
||||
def setUp(self):
|
||||
super(PasswordValidationTestCase, self).setUp()
|
||||
# passwords requires: 1 letter, 1 digit, 7 chars
|
||||
self.config_fixture.config(group='security_compliance',
|
||||
password_regex=(
|
||||
'^(?=.*\d)(?=.*[a-zA-Z]).{7,}$'))
|
||||
|
||||
def test_password_validate_with_valid_strong_password(self):
|
||||
password = 'mypassword2'
|
||||
validators.validate_password(password)
|
||||
|
||||
def test_password_validate_with_invalid_strong_password(self):
|
||||
# negative test: None
|
||||
password = None
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
validators.validate_password,
|
||||
password)
|
||||
# negative test: numeric
|
||||
password = 1234
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
validators.validate_password,
|
||||
password)
|
||||
# negative test: boolean
|
||||
password = True
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
validators.validate_password,
|
||||
password)
|
||||
|
||||
def test_password_validate_with_invalid_password_regex(self):
|
||||
# invalid regular expression, missing beginning '['
|
||||
self.config_fixture.config(group='security_compliance',
|
||||
password_regex='\S]+')
|
||||
password = 'mypassword2'
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
validators.validate_password,
|
||||
password)
|
||||
# fix regular expression and validate
|
||||
self.config_fixture.config(group='security_compliance',
|
||||
password_regex='[\S]+')
|
||||
validators.validate_password(password)
|
||||
|
|
Loading…
Reference in New Issue