Allow blocking users from self-service password change

User option ``lock_password`` has been implemented. This
option when set to ``True`` will prevent the usage of the
self-service password change API. If the ``lock_password``
option is set to ``False`` or ``None`` (to remove the
option from the user-data structure) normal password
change operations are allowed

Closes-Bug: #1755874
Change-Id: Icf1776c5fe625c2e9292bfcf40a8a9f17a002656
This commit is contained in:
Morgan Fainberg 2018-04-06 15:15:35 -07:00
parent 78adf4b40f
commit f17fa57f6c
6 changed files with 122 additions and 0 deletions

View File

@ -224,6 +224,29 @@ old password.
Otherwise, users would not be able to change their passwords before they
expire.
Prevent Self-Service Password Changes
-------------------------------------
If there exists a user who should not be able to change her own password via
the keystone password change API, keystone supports setting that user's option
``lock_password`` to ``True`` via the user update API
(``PATCH /v3/users/{user_id}``):
.. code-block:: json
{
"user": {
"options": {
"lock_password": True
}
}
}
The ``lock_password`` user-option is typically used in the case where passwords
are managed externally to keystone. The ``lock_password`` option can be set to
``True``, ``False``, or ``None``; if the option is set to ``None``, it is
removed from the user's data structure.
.. _Security Hardening PCI-DSS: https://specs.openstack.org/openstack/keystone-specs/specs/keystone/newton/pci-dss.html
.. _v3: https://developer.openstack.org/api-ref/identity/v3/index.html#update-user

View File

@ -119,6 +119,13 @@ class PasswordAgeValidationError(PasswordValidationError):
"reset your password.")
class PasswordSelfServiceDisabled(PasswordValidationError):
message_format = _("You cannot change your password at this time due "
"to password policy disallowing password changes. "
"Please contact your administrator to reset your "
"password.")
class SchemaValidationError(ValidationError):
# NOTE(lbragstad): For whole OpenStack message consistency, this error
# message has been written in a format consistent with WSME.

View File

@ -76,6 +76,12 @@ IGNORE_LOCKOUT_ATTEMPT_OPT = (
option_name='ignore_lockout_failure_attempts',
validator=resource_options.boolean_validator,
json_schema_validation=parameter_types.boolean))
LOCK_PASSWORD_OPT = (
resource_options.ResourceOption(
option_id='1003',
option_name='lock_password',
validator=resource_options.boolean_validator,
json_schema_validation=parameter_types.boolean))
MFA_RULES_OPT = (
resource_options.ResourceOption(
option_id='MFAR',
@ -112,6 +118,7 @@ def register_user_options():
IGNORE_CHANGE_PASSWORD_OPT,
IGNORE_PASSWORD_EXPIRY_OPT,
IGNORE_LOCKOUT_ATTEMPT_OPT,
LOCK_PASSWORD_OPT,
MFA_RULES_OPT,
MFA_ENABLED_OPT,
]:

View File

@ -248,6 +248,10 @@ class Identity(base.IdentityDriverBase):
def change_password(self, user_id, new_password):
with sql.session_for_write() as session:
user_ref = session.query(model.User).get(user_id)
lock_pw_opt = user_ref.get_resource_option(
options.LOCK_PASSWORD_OPT.option_id)
if lock_pw_opt is not None and lock_pw_opt.option_value is True:
raise exception.PasswordSelfServiceDisabled()
if user_ref.password_ref and user_ref.password_ref.self_service:
self._validate_minimum_password_age(user_ref)
self._validate_password_history(new_password, user_ref)

View File

@ -459,6 +459,25 @@ class IdentityTestCase(test_v3.RestfulTestCase):
password=new_password)
self.v3_create_token(new_password_auth)
def test_admin_password_reset_with_password_lock(self):
# create user
user_ref = unit.create_user(PROVIDERS.identity_api,
domain_id=self.domain['id'])
lock_pw_opt = options.LOCK_PASSWORD_OPT.option_name
update_user_body = {'user': {'options': {lock_pw_opt: True}}}
self.patch('/users/%s' % user_ref['id'], body=update_user_body)
# administrative password reset
new_password = uuid.uuid4().hex
r = self.patch('/users/%s' % user_ref['id'],
body={'user': {'password': new_password}})
self.assertValidUserResponse(r, user_ref)
# authenticate with new password
new_password_auth = self.build_authentication_request(
user_id=user_ref['id'],
password=new_password)
self.v3_create_token(new_password_auth)
def test_update_user_domain_id(self):
"""Call ``PATCH /users/{user_id}`` with domain_id.
@ -769,6 +788,59 @@ class UserSelfServiceChangingPasswordsTestCase(ChangePasswordTestCase):
original_password=new_password,
expected_status=http_client.NO_CONTENT)
def test_changing_password_with_password_lock(self):
password = uuid.uuid4().hex
ref = unit.new_user_ref(domain_id=self.domain_id, password=password)
response = self.post('/users', body={'user': ref})
user_id = response.json_body['user']['id']
time = datetime.datetime.utcnow()
with freezegun.freeze_time(time) as frozen_datetime:
# Lock the user's password
lock_pw_opt = options.LOCK_PASSWORD_OPT.option_name
user_patch = {'user': {'options': {lock_pw_opt: True}}}
self.patch('/users/%s' % user_id, body=user_patch)
# Fail, password is locked
new_password = uuid.uuid4().hex
body = {
'user': {
'original_password': password,
'password': new_password
}
}
path = '/users/%s/password' % user_id
self.post(path, body=body, expected_status=http_client.BAD_REQUEST)
# Unlock the password, and change should work
user_patch['user']['options'][lock_pw_opt] = False
self.patch('/users/%s' % user_id, body=user_patch)
path = '/users/%s/password' % user_id
self.post(path, body=body, expected_status=http_client.NO_CONTENT)
frozen_datetime.tick(delta=datetime.timedelta(seconds=1))
auth_data = self.build_authentication_request(
user_id=user_id,
password=new_password
)
self.v3_create_token(
auth_data, expected_status=http_client.CREATED
)
path = '/users/%s' % user_id
user = self.get(path).json_body['user']
self.assertIn(lock_pw_opt, user['options'])
self.assertFalse(user['options'][lock_pw_opt])
# Completely unset the option from the user's reference
user_patch['user']['options'][lock_pw_opt] = None
self.patch('/users/%s' % user_id, body=user_patch)
path = '/users/%s' % user_id
user = self.get(path).json_body['user']
self.assertNotIn(lock_pw_opt, user['options'])
def test_changing_password_with_missing_original_password_fails(self):
r = self.change_password(password=uuid.uuid4().hex,
expected_status=http_client.BAD_REQUEST)

View File

@ -0,0 +1,9 @@
---
fixes:
- |
[`bug 1755874 <https://bugs.launchpad.net/keystone/+bug/1755874>`_]
Users now can have the resource option ``lock_password`` set which prevents
the user from utilizing the self-service password change API. Valid
values are ``True``, ``False``, or "None" (where ``None`` clears the
option).