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:
Ronald De Rose 2016-05-24 17:20:33 +00:00
parent be86bb1206
commit 5d90bfa61e
6 changed files with 154 additions and 2 deletions

View File

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

View File

@ -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,
]

View File

@ -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.

View File

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

View File

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

View File

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