PCI-DSS Password history requirements

This patch satisfies the following PCI-DSS password history
requirements:

* PCI-DSS 8.2.5: Do not allow an individual to submit a new
password/phrase that is the same as any of the last four
passwords/phrases he or she has used.

This feature is configurable; thus, not allowing users to change their
password if it matches a configurable number of previous passwords.

Partially-implements: blueprint pci-dss
Change-Id: I3765cd71463de7f0c7b2c1066ca8848f194c94f1
This commit is contained in:
Ronald De Rose 2016-06-10 15:12:50 +00:00
parent 96af93d361
commit 6bc3a74909
3 changed files with 152 additions and 4 deletions

View File

@ -65,13 +65,14 @@ feature. This feature depends on the `sql` backend for the `[identity] driver`.
unique_last_password_count = cfg.IntOpt(
'unique_last_password_count',
default=0,
min=0,
default=1,
min=1,
help=utils.fmt("""
This controls the number of previous user password iterations to keep in
history, in order to enforce that newly created passwords are unique. Setting
the value to zero (the default) disables this feature. This feature depends on
the `sql` backend for the `[identity] driver`.
the value to one (the default) disables this feature. Thus, to enable this
feature, values must be greater than 1. This feature depends on the `sql`
backend for the `[identity] driver`.
"""))
password_change_limit_per_day = cfg.IntOpt(

View File

@ -19,12 +19,16 @@ import sqlalchemy
from keystone.common import driver_hints
from keystone.common import sql
from keystone.common import utils
import keystone.conf
from keystone import exception
from keystone.i18n import _
from keystone.identity.backends import base
from keystone.identity.backends import sql_model as model
CONF = keystone.conf.CONF
class Identity(base.IdentityDriverV8):
# NOTE(henry-nash): Override the __init__() method so as to take a
# config parameter to enable sql to be used as a domain-specific driver.
@ -105,6 +109,8 @@ class Identity(base.IdentityDriverV8):
def update_user(self, user_id, user):
with sql.session_for_write() as session:
user_ref = self._get_user(session, user_id)
if 'password' in user:
self._validate_password_history(user['password'], user_ref)
old_user_dict = user_ref.to_dict()
user = utils.hash_user_password(user)
for k in user:
@ -117,6 +123,21 @@ class Identity(base.IdentityDriverV8):
return base.filter_user(
user_ref.to_dict(include_extra_dict=True))
def _validate_password_history(self, password, user_ref):
unique_cnt = CONF.security_compliance.unique_last_password_count
# Slice off all of the extra passwords.
user_ref.local_user.passwords = (
user_ref.local_user.passwords[-unique_cnt:])
# Validate the new password against the remaining passwords.
if unique_cnt > 1:
for password_ref in user_ref.local_user.passwords:
if utils.check_password(password, password_ref.password):
detail = _('The new password cannot be identical to a '
'previous password. The number of previous '
'passwords that must be unique is: '
'%(unique_cnt)s') % {'unique_cnt': unique_cnt}
raise exception.PasswordValidationError(detail=detail)
def add_user_to_group(self, user_id, group_id):
with sql.session_for_write() as session:
self.get_group(group_id)

View File

@ -150,3 +150,129 @@ class DisableInactiveUserTests(test_backend_sql.SqlTests):
user_ref = session.query(model.User).get(user_id)
user_ref.last_active_at = last_active_at
return user_ref
class PasswordHistoryValidationTests(test_backend_sql.SqlTests):
def setUp(self):
super(PasswordHistoryValidationTests, self).setUp()
self.passwords = [uuid.uuid4().hex,
uuid.uuid4().hex,
uuid.uuid4().hex,
uuid.uuid4().hex]
self.max_cnt = 3
self.config_fixture.config(group='security_compliance',
unique_last_password_count=self.max_cnt)
def test_validate_password_history_with_invalid_password(self):
user = self._create_user(self.passwords[0])
self.assertValidPasswordUpdate(user, self.passwords[1])
# Attempt to update with the initial password
user['password'] = self.passwords[0]
self.assertRaises(exception.PasswordValidationError,
self.identity_api.update_user,
user['id'],
user)
def test_validate_password_history_with_valid_password(self):
user = self._create_user(self.passwords[0])
self.assertValidPasswordUpdate(user, self.passwords[1])
self.assertValidPasswordUpdate(user, self.passwords[2])
self.assertValidPasswordUpdate(user, self.passwords[3])
# Now you should be able to change the password to match the initial
# password because the password history only contains password elements
# 1, 2, 3
self.assertValidPasswordUpdate(user, self.passwords[0])
def test_validate_password_history_but_start_with_password_none(self):
# Create user and confirm password is None
user = self._create_user(None)
user_ref = self._get_user_ref(user['id'])
self.assertIsNone(user_ref.password)
# Update the password
self.assertValidPasswordUpdate(user, self.passwords[0])
self.assertValidPasswordUpdate(user, self.passwords[1])
# Attempt to update with a previous password
user['password'] = self.passwords[0]
self.assertRaises(exception.PasswordValidationError,
self.identity_api.update_user,
user['id'],
user)
def test_validate_password_history_disabled_and_repeat_same_password(self):
self.config_fixture.config(group='security_compliance',
unique_last_password_count=1)
user = self._create_user(self.passwords[0])
# Repeatedly update the password with the same password
self.assertValidPasswordUpdate(user, self.passwords[0])
self.assertValidPasswordUpdate(user, self.passwords[0])
def test_truncate_passwords(self):
user = self._create_user(self.passwords[0])
self._add_passwords_to_history(user, n=4)
user_ref = self._get_user_ref(user['id'])
self.assertEqual(
len(user_ref.local_user.passwords), (self.max_cnt + 1))
def test_truncate_passwords_when_max_is_default(self):
self.max_cnt = 1
expected_length = self.max_cnt + 1
self.config_fixture.config(group='security_compliance',
unique_last_password_count=self.max_cnt)
user = self._create_user(self.passwords[0])
self._add_passwords_to_history(user, n=4)
user_ref = self._get_user_ref(user['id'])
self.assertEqual(len(user_ref.local_user.passwords), expected_length)
# Start with multiple passwords and then change max_cnt to one
self.max_cnt = 4
self.config_fixture.config(group='security_compliance',
unique_last_password_count=self.max_cnt)
self._add_passwords_to_history(user, n=self.max_cnt)
user_ref = self._get_user_ref(user['id'])
self.assertEqual(
len(user_ref.local_user.passwords), (self.max_cnt + 1))
self.max_cnt = 1
self.config_fixture.config(group='security_compliance',
unique_last_password_count=self.max_cnt)
self._add_passwords_to_history(user, n=1)
user_ref = self._get_user_ref(user['id'])
self.assertEqual(len(user_ref.local_user.passwords), expected_length)
def test_truncate_passwords_when_max_is_default_and_no_password(self):
expected_length = 1
self.max_cnt = 1
self.config_fixture.config(group='security_compliance',
unique_last_password_count=self.max_cnt)
user = {
'name': uuid.uuid4().hex,
'domain_id': 'default',
'enabled': True,
}
user = self.identity_api.create_user(user)
self._add_passwords_to_history(user, n=1)
user_ref = self._get_user_ref(user['id'])
self.assertEqual(len(user_ref.local_user.passwords), expected_length)
def _create_user(self, password):
user = {
'name': uuid.uuid4().hex,
'domain_id': 'default',
'enabled': True,
'password': password
}
return self.identity_api.create_user(user)
def assertValidPasswordUpdate(self, user, new_password):
user['password'] = new_password
self.identity_api.update_user(user['id'], user)
self.identity_api.authenticate(self.make_request(),
user_id=user['id'],
password=new_password)
def _add_passwords_to_history(self, user, n):
for _ in range(n):
user['password'] = uuid.uuid4().hex
self.identity_api.update_user(user['id'], user)
def _get_user_ref(self, user_id):
with sql.session_for_read() as session:
return self.identity_api._get_user(session, user_id)