From 930728a57ed3fc21ddc2edb174b28df434ea3586 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Fri, 27 Jan 2017 12:14:17 -0800 Subject: [PATCH] Deprecate [security_compliance]\password_expires_ignore_user_ids Deprecate [security_compliance]\password_expires_ignore_user_ids in favor of using the new user-option 'ignore_password_expiry'. This allows setting the value for ignoring password expiration on individual users without needing to restart keystone for each change to the list. Partial-Bug: 1659995 Change-Id: Ib4b422ab07f91c312f3268ade926db1638052587 --- keystone/conf/security_compliance.py | 11 ++++++ .../identity/backends/resource_options.py | 4 +++ keystone/identity/backends/sql_model.py | 26 ++++++++++++-- .../tests/unit/identity/test_backend_sql.py | 35 +++++++++++++++++-- .../notes/bug-1659995-f3e716de743b7291.yaml | 14 ++++++++ 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/bug-1659995-f3e716de743b7291.yaml diff --git a/keystone/conf/security_compliance.py b/keystone/conf/security_compliance.py index 3390a61f9d..9946fcb9bf 100644 --- a/keystone/conf/security_compliance.py +++ b/keystone/conf/security_compliance.py @@ -11,6 +11,7 @@ # under the License. from oslo_config import cfg +from oslo_log import versionutils from keystone.conf import utils @@ -77,6 +78,16 @@ the `[identity] driver`. password_expires_ignore_user_ids = cfg.ListOpt( 'password_expires_ignore_user_ids', + deprecated_for_removal=True, + deprecated_reason=utils.fmt(""" +Functionality added as a per-user option "ignore_password_expiry" in Ocata. +Each user that should ignore password expiry should have the value set to +"true" in the user's `options` attribute (e.g. +`user['options']['ignore_password_expiry'] = True`) with an "update_user" call. +This avoids the need to restart keystone to adjust the users that ignore +password expiry. This option will be removed in the Pike release. +"""), + deprecated_since=versionutils.deprecated.OCATA, default=[], help=utils.fmt(""" Comma separated list of user IDs to be ignored when checking if a password diff --git a/keystone/identity/backends/resource_options.py b/keystone/identity/backends/resource_options.py index 7f2bba1106..de7b78684a 100644 --- a/keystone/identity/backends/resource_options.py +++ b/keystone/identity/backends/resource_options.py @@ -17,6 +17,9 @@ USER_OPTIONS_REGISTRY = resource_options.ResourceOptionRegistry('USER') IGNORE_CHANGE_PASSWORD_OPT = ( resource_options.ResourceOption('1000', 'ignore_change_password_upon_first_use')) +IGNORE_PASSWORD_EXPIRY_OPT = ( + resource_options.ResourceOption('1001', + 'ignore_password_expiry')) # NOTE(notmorgan): wrap this in a function for testing purposes. @@ -24,6 +27,7 @@ IGNORE_CHANGE_PASSWORD_OPT = ( def register_user_options(): for opt in [ IGNORE_CHANGE_PASSWORD_OPT, + IGNORE_PASSWORD_EXPIRY_OPT, ]: USER_OPTIONS_REGISTRY.register_option(opt) diff --git a/keystone/identity/backends/sql_model.py b/keystone/identity/backends/sql_model.py index f90fa9e0db..bb77dbc0da 100644 --- a/keystone/identity/backends/sql_model.py +++ b/keystone/identity/backends/sql_model.py @@ -14,6 +14,7 @@ import datetime +from oslo_log import versionutils import sqlalchemy from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy import orm @@ -149,10 +150,29 @@ class User(sql.ModelBase, sql.DictBase): def _get_password_expires_at(self, created_at): expires_days = CONF.security_compliance.password_expires_days + # NOTE(notmorgan): This option is deprecated and subject to removal + # in a future release. ignore_list = CONF.security_compliance.password_expires_ignore_user_ids - if expires_days and (self.id not in ignore_list): - expired_date = (created_at + datetime.timedelta(days=expires_days)) - return expired_date.replace(microsecond=0) + if ignore_list: + versionutils.deprecated( + what='[security_compliance]\password_expires_ignore_user_ids', + as_of=versionutils.deprecated.OCATA, + remove_in=+1, + in_favor_of=('Using the `ignore_password_expiry` value set to ' + '`True` in the `user["options"]` dictionary on ' + 'User creation or update (via API call).')) + # Get the IGNORE_PASSWORD_EXPIRY_OPT value from the user's + # option_mapper. + + ignore_pw_expiry = getattr( + self.get_resource_option(iro.IGNORE_PASSWORD_EXPIRY_OPT.option_id), + 'option_value', + False) + if (self.id not in ignore_list) and not ignore_pw_expiry: + if expires_days: + expired_date = (created_at + + datetime.timedelta(days=expires_days)) + return expired_date.replace(microsecond=0) return None @password.expression diff --git a/keystone/tests/unit/identity/test_backend_sql.py b/keystone/tests/unit/identity/test_backend_sql.py index f7468e43c1..9ceeca4098 100644 --- a/keystone/tests/unit/identity/test_backend_sql.py +++ b/keystone/tests/unit/identity/test_backend_sql.py @@ -695,6 +695,33 @@ class PasswordExpiresValidationTests(test_backend_sql.SqlTests): user_id=user['id'], password=self.password) + def test_authenticate_with_expired_password_for_ignore_user_option(self): + # set user to have the 'ignore_password_expiry' option set to False + self.user_dict.setdefault('options', {})[ + iro.IGNORE_PASSWORD_EXPIRY_OPT.option_name] = False + # set password created_at so that the password will expire + password_created_at = ( + datetime.datetime.utcnow() - + datetime.timedelta( + days=CONF.security_compliance.password_expires_days + 1) + ) + user = self._create_user(self.user_dict, password_created_at) + self.assertRaises(exception.PasswordExpired, + self.identity_api.authenticate, + self.make_request(), + user_id=user['id'], + password=self.password) + + # update user to explicitly have the expiry option to True + user['options'][ + iro.IGNORE_PASSWORD_EXPIRY_OPT.option_name] = True + user = self.identity_api.update_user(user['id'], + user) + # test password is not expired due to ignore option + self.identity_api.authenticate(self.make_request(), + user_id=user['id'], + password=self.password) + def _get_test_user_dict(self, password): test_user_dict = { 'id': uuid.uuid4().hex, @@ -706,13 +733,15 @@ class PasswordExpiresValidationTests(test_backend_sql.SqlTests): return test_user_dict def _create_user(self, user_dict, password_created_at): - user_dict = utils.hash_user_password(user_dict) + # Bypass business logic and go straight for the identity driver + # (SQL in this case) + driver = self.identity_api.driver + driver.create_user(user_dict['id'], user_dict) with sql.session_for_write() as session: - user_ref = model.User.from_dict(user_dict) + user_ref = session.query(model.User).get(user_dict['id']) user_ref.password_ref.created_at = password_created_at user_ref.password_ref.expires_at = ( user_ref._get_password_expires_at(password_created_at)) - session.add(user_ref) return base.filter_user(user_ref.to_dict()) diff --git a/releasenotes/notes/bug-1659995-f3e716de743b7291.yaml b/releasenotes/notes/bug-1659995-f3e716de743b7291.yaml new file mode 100644 index 0000000000..7e2d57c096 --- /dev/null +++ b/releasenotes/notes/bug-1659995-f3e716de743b7291.yaml @@ -0,0 +1,14 @@ +--- +fixes: + - | + [`bug 1659995 `_] + A new option has been made available via the user create and update API + (``POST/PATCH /v3/users) call, the option will allow an admin to + mark users as exempt from the PCI password expiry policy via an API. + This can be done like so: ``user['options']['ignore_password_expiry']``. +deprecations: + - | + [`bug 1659995 `_] + The config option ``[security_compliance] ignore_password_expires_user_ids`` + has been deprecated in favor of using the option value set, available via + the user create and update API call \ No newline at end of file