Create user option `ignore_lockout_failure_attempts`

Rather than an option list in keystone.conf, leverage the
per-user options available via REST APIs, to define whether
a user is exempt from being disabled upon too many failed
password attempts.

Closes-Bug: 1659995

Co-Authored-By: "Steve Martinelli <s.martinelli@gmail.com>"
Change-Id: I6999343314a9e11a82664cd0db6e56047084fa76
This commit is contained in:
Morgan Fainberg 2017-01-23 08:26:34 -08:00
parent 47cd72911c
commit 9844fa1e26
6 changed files with 75 additions and 17 deletions

View File

@ -54,17 +54,6 @@ non-zero value. This feature depends on the `sql` backend for the `[identity]
driver`.
"""))
lockout_ignored_user_ids = cfg.ListOpt(
'lockout_ignored_user_ids',
default=[],
help=utils.fmt("""
Comma separated list of user IDs to be ignored when checking if a user should
be locked out based on failed authentication attempts. Thus, users in this list
can fail to authenticate for an unlimited amount of times and will never be
locked out. This feature will only be enabled if `[security_compliance]
lockout_failure_attempts` is set.
"""))
password_expires_days = cfg.IntOpt(
'password_expires_days',
min=1,
@ -160,7 +149,6 @@ ALL_OPTS = [
disable_user_account_days_inactive,
lockout_failure_attempts,
lockout_duration,
lockout_ignored_user_ids,
password_expires_days,
password_expires_ignore_user_ids,
unique_last_password_count,

View File

@ -27,6 +27,12 @@ IGNORE_PASSWORD_EXPIRY_OPT = (
option_name='ignore_password_expiry',
validator=resource_options.boolean_validator,
json_schema_validation=parameter_types.boolean))
IGNORE_LOCKOUT_ATTEMPT_OPT = (
resource_options.ResourceOption(
option_id='1002',
option_name='ignore_lockout_failure_attempts',
validator=resource_options.boolean_validator,
json_schema_validation=parameter_types.boolean))
# NOTE(notmorgan): wrap this in a function for testing purposes.
@ -35,6 +41,7 @@ def register_user_options():
for opt in [
IGNORE_CHANGE_PASSWORD_OPT,
IGNORE_PASSWORD_EXPIRY_OPT,
IGNORE_LOCKOUT_ATTEMPT_OPT
]:
USER_OPTIONS_REGISTRY.register_option(opt)

View File

@ -87,8 +87,11 @@ class Identity(base.IdentityDriverBase):
:returns Boolean: True if the account is locked; False otherwise
"""
if user_id in CONF.security_compliance.lockout_ignored_user_ids:
ignore_option = user_ref.get_resource_option(
options.IGNORE_LOCKOUT_ATTEMPT_OPT.option_id)
if ignore_option and ignore_option.option_value is True:
return False
attempts = user_ref.local_user.failed_auth_count or 0
max_attempts = CONF.security_compliance.lockout_failure_attempts
lockout_duration = CONF.security_compliance.lockout_duration

View File

@ -533,10 +533,11 @@ class LockingOutUserTests(test_backend_sql.SqlTests):
password=uuid.uuid4().hex)
def test_lock_out_for_ignored_user(self):
# add the user id to the ignore list
self.config_fixture.config(
group='security_compliance',
lockout_ignored_user_ids=[self.user['id']])
# mark the user as exempt from failed password attempts
# ignore user and reset password, password not expired
self.user['options'][iro.IGNORE_LOCKOUT_ATTEMPT_OPT.option_name] = True
self.identity_api.update_user(self.user['id'], self.user)
# fail authentication repeatedly the max number of times
self._fail_auth_repeatedly(self.user['id'])
# authenticate with wrong password, account should not be locked

View File

@ -1037,6 +1037,29 @@ class UserSelfServiceChangingPasswordsTestCase(ChangePasswordTestCase):
self.token = self.get_request_token(reset_password,
http_client.CREATED)
def test_lockout_exempt(self):
self.config_fixture.config(group='security_compliance',
lockout_failure_attempts=1)
# create user
self.user_ref = unit.create_user(self.identity_api,
domain_id=self.domain['id'])
# update the user, mark her as exempt from lockout
ignore_opt_name = options.IGNORE_LOCKOUT_ATTEMPT_OPT.option_name
self.user_ref['options'][ignore_opt_name] = True
self.identity_api.update_user(self.user_ref['id'], self.user_ref)
# fail to auth, this should lockout the user, since we're allowed
# one failure, but we're exempt from lockout!
bad_password = uuid.uuid4().hex
self.token = self.get_request_token(bad_password,
http_client.UNAUTHORIZED)
# attempt to authenticate with correct password
self.get_request_token(self.user_ref['password'],
expected_status=http_client.CREATED)
class PasswordValidationTestCase(ChangePasswordTestCase):

View File

@ -1839,6 +1839,42 @@ class UserValidationTestCase(unit.BaseTestCase):
}
self.update_user_validator.validate(request_to_validate)
def test_user_create_with_options_lockout_password(self):
request_to_validate = {
'name': self.user_name,
'options': {
ro.IGNORE_LOCKOUT_ATTEMPT_OPT.option_name: True
}
}
self.create_user_validator.validate(request_to_validate)
def test_user_update_with_options_lockout_password(self):
request_to_validate = {
'options': {
ro.IGNORE_LOCKOUT_ATTEMPT_OPT.option_name: False
}
}
self.update_user_validator.validate(request_to_validate)
def test_user_update_with_two_options(self):
request_to_validate = {
'options': {
ro.IGNORE_CHANGE_PASSWORD_OPT.option_name: True,
ro.IGNORE_LOCKOUT_ATTEMPT_OPT.option_name: True
}
}
self.update_user_validator.validate(request_to_validate)
def test_user_create_with_two_options(self):
request_to_validate = {
'name': self.user_name,
'options': {
ro.IGNORE_CHANGE_PASSWORD_OPT.option_name: False,
ro.IGNORE_LOCKOUT_ATTEMPT_OPT.option_name: True
}
}
self.create_user_validator.validate(request_to_validate)
class GroupValidationTestCase(unit.BaseTestCase):
"""Test for V3 Group API validation."""