Add reason to notifications for PCI-DSS
This adds a reason to the CADF event notifications that are emitted for the following events related to PCI-DSS: - Change user passwords/passphrases at least once every X days - Limit repeated access attempts by locking out the user ID after not more than X attempts - Do not allow an individual to submit a new password/phrase that is the same as any of the last X passwords/phrases he or she has used - Passwords/phrases must meet the specificed regex - User attempting to change password early Implements: bp pci-dss-notifications Co-Authored-By: Tin Lam <tinlam@gmail.com> Change-Id: Ia678d25bdfa151c95483f5fcb77853184fbecfd1
This commit is contained in:
parent
e274a474e8
commit
7fe14c8da0
|
@ -37,9 +37,8 @@ def validate_password(password):
|
|||
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)
|
||||
raise exception.PasswordRequirementsValidationError(
|
||||
detail=pattern_desc)
|
||||
except re.error:
|
||||
msg = _LE("Unable to validate password due to invalid regular "
|
||||
"expression - password_regex: ")
|
||||
|
|
|
@ -97,6 +97,18 @@ class PasswordValidationError(ValidationError):
|
|||
message_format = _("Password validation error: %(detail)s")
|
||||
|
||||
|
||||
class PasswordRequirementsValidationError(PasswordValidationError):
|
||||
message_format = _("The password does not match the requirements:"
|
||||
" %(detail)s")
|
||||
|
||||
|
||||
class PasswordHistoryValidationError(PasswordValidationError):
|
||||
message_format = _("The new password cannot be identical to a "
|
||||
"previous password. The number of previous "
|
||||
"passwords that must be unique is "
|
||||
"%(unique_count)s")
|
||||
|
||||
|
||||
class PasswordAgeValidationError(PasswordValidationError):
|
||||
message_format = _("You cannot change your password at this time due "
|
||||
"to the minimum password age. Once you change your "
|
||||
|
|
|
@ -180,11 +180,8 @@ class Identity(base.IdentityDriverBase):
|
|||
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)d') % {'unique_cnt': unique_cnt}
|
||||
raise exception.PasswordValidationError(detail=detail)
|
||||
raise exception.PasswordHistoryValidationError(
|
||||
unique_count=unique_cnt)
|
||||
|
||||
def change_password(self, user_id, new_password):
|
||||
with sql.session_for_write() as session:
|
||||
|
|
|
@ -289,7 +289,8 @@ class UserV3(controller.V3Controller):
|
|||
attribute='password')
|
||||
try:
|
||||
self.identity_api.change_password(
|
||||
request, user_id, original_password, password)
|
||||
request, user_id, original_password,
|
||||
password, initiator=request.audit_initiator)
|
||||
except AssertionError as e:
|
||||
raise exception.Unauthorized(_(
|
||||
'Error when changing user password: %s') % e)
|
||||
|
|
|
@ -22,6 +22,7 @@ import uuid
|
|||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_log import versionutils
|
||||
from pycadf import reason
|
||||
|
||||
from keystone import assignment # TODO(lbragstad): Decouple this dependency
|
||||
from keystone.common import cache
|
||||
|
@ -1282,17 +1283,23 @@ class Manager(manager.Manager):
|
|||
|
||||
@domains_configured
|
||||
def change_password(self, request, user_id, original_password,
|
||||
new_password):
|
||||
new_password, initiator=None):
|
||||
|
||||
# authenticate() will raise an AssertionError if authentication fails
|
||||
self.authenticate(request, user_id, original_password)
|
||||
|
||||
validators.validate_password(new_password)
|
||||
|
||||
domain_id, driver, entity_id = (
|
||||
self._get_domain_driver_and_entity_id(user_id))
|
||||
driver.change_password(entity_id, new_password)
|
||||
notifications.Audit.updated(self._USER, user_id)
|
||||
try:
|
||||
validators.validate_password(new_password)
|
||||
driver.change_password(entity_id, new_password)
|
||||
except exception.PasswordValidationError as ex:
|
||||
audit_reason = reason.Reason(str(ex), str(ex.code))
|
||||
notifications.Audit.updated(self._USER, user_id,
|
||||
initiator, reason=audit_reason)
|
||||
raise
|
||||
|
||||
notifications.Audit.updated(self._USER, user_id, initiator)
|
||||
self.emit_invalidate_user_token_persistence(user_id)
|
||||
|
||||
@MEMOIZE
|
||||
|
|
|
@ -27,8 +27,10 @@ from pycadf import cadftaxonomy as taxonomy
|
|||
from pycadf import cadftype
|
||||
from pycadf import credential
|
||||
from pycadf import eventfactory
|
||||
from pycadf import reason
|
||||
from pycadf import resource
|
||||
|
||||
from keystone import exception
|
||||
from keystone.i18n import _, _LE
|
||||
from keystone.common import dependency
|
||||
from keystone.common import utils
|
||||
|
@ -87,7 +89,7 @@ class Audit(object):
|
|||
|
||||
@classmethod
|
||||
def _emit(cls, operation, resource_type, resource_id, initiator, public,
|
||||
actor_dict=None):
|
||||
actor_dict=None, reason=None):
|
||||
"""Directly send an event notification.
|
||||
|
||||
:param operation: one of the values from ACTIONS
|
||||
|
@ -100,6 +102,8 @@ class Audit(object):
|
|||
notify_event_callbacks to in process listeners
|
||||
:param actor_dict: dictionary of actor information in the event of
|
||||
assignment notification
|
||||
:param reason: pycadf object containing the response code and
|
||||
message description
|
||||
"""
|
||||
# NOTE(stevemar): the _send_notification function is
|
||||
# overloaded, it's used to register callbacks and to actually
|
||||
|
@ -116,52 +120,52 @@ class Audit(object):
|
|||
if CONF.notification_format == 'cadf' and public:
|
||||
outcome = taxonomy.OUTCOME_SUCCESS
|
||||
_create_cadf_payload(operation, resource_type, resource_id,
|
||||
outcome, initiator)
|
||||
outcome, initiator, reason)
|
||||
|
||||
@classmethod
|
||||
def created(cls, resource_type, resource_id, initiator=None,
|
||||
public=True):
|
||||
public=True, reason=None):
|
||||
cls._emit(ACTIONS.created, resource_type, resource_id, initiator,
|
||||
public)
|
||||
public, reason=reason)
|
||||
|
||||
@classmethod
|
||||
def updated(cls, resource_type, resource_id, initiator=None,
|
||||
public=True):
|
||||
public=True, reason=None):
|
||||
cls._emit(ACTIONS.updated, resource_type, resource_id, initiator,
|
||||
public)
|
||||
public, reason=reason)
|
||||
|
||||
@classmethod
|
||||
def disabled(cls, resource_type, resource_id, initiator=None,
|
||||
public=True):
|
||||
public=True, reason=None):
|
||||
cls._emit(ACTIONS.disabled, resource_type, resource_id, initiator,
|
||||
public)
|
||||
public, reason=reason)
|
||||
|
||||
@classmethod
|
||||
def deleted(cls, resource_type, resource_id, initiator=None,
|
||||
public=True):
|
||||
public=True, reason=None):
|
||||
cls._emit(ACTIONS.deleted, resource_type, resource_id, initiator,
|
||||
public)
|
||||
public, reason=reason)
|
||||
|
||||
@classmethod
|
||||
def added_to(cls, target_type, target_id, actor_type, actor_id,
|
||||
initiator=None, public=True):
|
||||
initiator=None, public=True, reason=None):
|
||||
actor_dict = {'id': actor_id,
|
||||
'type': actor_type,
|
||||
'actor_operation': 'added'}
|
||||
cls._emit(ACTIONS.updated, target_type, target_id, initiator, public,
|
||||
actor_dict=actor_dict)
|
||||
actor_dict=actor_dict, reason=reason)
|
||||
|
||||
@classmethod
|
||||
def removed_from(cls, target_type, target_id, actor_type, actor_id,
|
||||
initiator=None, public=True):
|
||||
initiator=None, public=True, reason=None):
|
||||
actor_dict = {'id': actor_id,
|
||||
'type': actor_type,
|
||||
'actor_operation': 'removed'}
|
||||
cls._emit(ACTIONS.updated, target_type, target_id, initiator, public,
|
||||
actor_dict=actor_dict)
|
||||
actor_dict=actor_dict, reason=reason)
|
||||
|
||||
@classmethod
|
||||
def internal(cls, resource_type, resource_id):
|
||||
def internal(cls, resource_type, resource_id, reason=None):
|
||||
# NOTE(lbragstad): Internal notifications are never public and have
|
||||
# never used the initiator variable, but the _emit() method expects
|
||||
# them. Let's set them here but not expose them through the method
|
||||
|
@ -170,7 +174,7 @@ class Audit(object):
|
|||
initiator = None
|
||||
public = False
|
||||
cls._emit(ACTIONS.internal, resource_type, resource_id, initiator,
|
||||
public)
|
||||
public, reason)
|
||||
|
||||
|
||||
def _get_callback_info(callback):
|
||||
|
@ -336,7 +340,7 @@ def reset_notifier():
|
|||
|
||||
|
||||
def _create_cadf_payload(operation, resource_type, resource_id,
|
||||
outcome, initiator):
|
||||
outcome, initiator, reason=None):
|
||||
"""Prepare data for CADF audit notifier.
|
||||
|
||||
Transform the arguments into content to be consumed by the function that
|
||||
|
@ -357,6 +361,8 @@ def _create_cadf_payload(operation, resource_type, resource_id,
|
|||
:param resource_id: ID of resource being operated on
|
||||
:param outcome: outcomes of the operation (SUCCESS, FAILURE, etc)
|
||||
:param initiator: CADF representation of the user that created the request
|
||||
:param reason: pycadf object containing the response code and
|
||||
message description
|
||||
"""
|
||||
if resource_type not in CADF_TYPE_MAP:
|
||||
target_uri = taxonomy.UNKNOWN
|
||||
|
@ -370,7 +376,7 @@ def _create_cadf_payload(operation, resource_type, resource_id,
|
|||
event_type = '%s.%s.%s' % (SERVICE, resource_type, operation)
|
||||
|
||||
_send_audit_notification(cadf_action, initiator, outcome,
|
||||
target, event_type, **audit_kwargs)
|
||||
target, event_type, reason=reason, **audit_kwargs)
|
||||
|
||||
|
||||
def _send_notification(operation, resource_type, resource_id, actor_dict=None,
|
||||
|
@ -481,12 +487,25 @@ class CadfNotificationWrapper(object):
|
|||
def __call__(self, f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(wrapped_self, request, user_id, *args, **kwargs):
|
||||
"""Alway send a notification."""
|
||||
"""Will always send a notification."""
|
||||
target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER)
|
||||
try:
|
||||
result = f(wrapped_self, request, user_id, *args, **kwargs)
|
||||
except (exception.AccountLocked,
|
||||
exception.PasswordExpired,
|
||||
exception.PasswordRequirementsValidationError,
|
||||
exception.PasswordHistoryValidationError,
|
||||
exception.PasswordAgeValidationError) as ex:
|
||||
# Send a CADF event with a reason for PCI-DSS related
|
||||
# authentication failures
|
||||
audit_reason = reason.Reason(str(ex), str(ex.code))
|
||||
_send_audit_notification(self.action, request.audit_initiator,
|
||||
taxonomy.OUTCOME_FAILURE,
|
||||
target, self.event_type,
|
||||
reason=audit_reason)
|
||||
raise
|
||||
except Exception:
|
||||
# For authentication failure send a cadf event as well
|
||||
# For authentication failure send a CADF event as well
|
||||
_send_audit_notification(self.action, request.audit_initiator,
|
||||
taxonomy.OUTCOME_FAILURE,
|
||||
target, self.event_type)
|
||||
|
@ -644,7 +663,7 @@ class _CatalogHelperObj(object):
|
|||
|
||||
|
||||
def _send_audit_notification(action, initiator, outcome, target,
|
||||
event_type, **kwargs):
|
||||
event_type, reason=None, **kwargs):
|
||||
"""Send CADF notification to inform observers about the affected resource.
|
||||
|
||||
This method logs an exception when sending the notification fails.
|
||||
|
@ -658,7 +677,8 @@ def _send_audit_notification(action, initiator, outcome, target,
|
|||
Ceilometer uses to poll events.
|
||||
:param kwargs: Any additional arguments passed in will be added as
|
||||
key-value pairs to the CADF event.
|
||||
|
||||
:param reason: Reason for the notification which contains the response
|
||||
code and message description
|
||||
"""
|
||||
if _check_notification_opt_out(event_type, outcome):
|
||||
return
|
||||
|
@ -680,6 +700,7 @@ def _send_audit_notification(action, initiator, outcome, target,
|
|||
action=action,
|
||||
initiator=initiator,
|
||||
target=target,
|
||||
reason=reason,
|
||||
observer=resource.Resource(typeURI=taxonomy.SERVICE_SECURITY))
|
||||
|
||||
if service_id is not None:
|
||||
|
|
|
@ -12,9 +12,11 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
import fixtures
|
||||
import freezegun
|
||||
import mock
|
||||
from oslo_config import fixture as config_fixture
|
||||
from oslo_log import log
|
||||
|
@ -24,6 +26,7 @@ from pycadf import eventfactory
|
|||
from pycadf import resource as cadfresource
|
||||
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
from keystone import notifications
|
||||
from keystone.tests import unit
|
||||
from keystone.tests.unit import test_v3
|
||||
|
@ -76,13 +79,14 @@ class AuditNotificationsTestCase(unit.BaseTestCase):
|
|||
'keystone.notifications._create_cadf_payload') as cadf_notify:
|
||||
notify_function(EXP_RESOURCE_TYPE, exp_resource_id)
|
||||
initiator = None
|
||||
reason = None
|
||||
cadf_notify.assert_called_once_with(
|
||||
operation, EXP_RESOURCE_TYPE, exp_resource_id,
|
||||
notifications.taxonomy.OUTCOME_SUCCESS, initiator)
|
||||
notifications.taxonomy.OUTCOME_SUCCESS, initiator, reason)
|
||||
notify_function(EXP_RESOURCE_TYPE, exp_resource_id, public=False)
|
||||
cadf_notify.assert_called_once_with(
|
||||
operation, EXP_RESOURCE_TYPE, exp_resource_id,
|
||||
notifications.taxonomy.OUTCOME_SUCCESS, initiator)
|
||||
notifications.taxonomy.OUTCOME_SUCCESS, initiator, reason)
|
||||
|
||||
def test_resource_created_notification(self):
|
||||
self._test_notification_operation_with_basic_format(
|
||||
|
@ -248,7 +252,7 @@ class BaseNotificationTest(test_v3.RestfulTestCase):
|
|||
notifications, '_send_notification', fake_notify))
|
||||
|
||||
def fake_audit(action, initiator, outcome, target,
|
||||
event_type, **kwargs):
|
||||
event_type, reason=None, **kwargs):
|
||||
service_security = cadftaxonomy.SERVICE_SECURITY
|
||||
|
||||
event = eventfactory.EventFactory().new_event(
|
||||
|
@ -257,13 +261,16 @@ class BaseNotificationTest(test_v3.RestfulTestCase):
|
|||
action=action,
|
||||
initiator=initiator,
|
||||
target=target,
|
||||
reason=reason,
|
||||
observer=cadfresource.Resource(typeURI=service_security))
|
||||
|
||||
for key, value in kwargs.items():
|
||||
setattr(event, key, value)
|
||||
|
||||
payload = event.as_dict()
|
||||
|
||||
audit = {
|
||||
'payload': event.as_dict(),
|
||||
'payload': payload,
|
||||
'event_type': event_type,
|
||||
'send_notification_called': True}
|
||||
self._audits.append(audit)
|
||||
|
@ -290,7 +297,7 @@ class BaseNotificationTest(test_v3.RestfulTestCase):
|
|||
self.assertEqual(actor_operation, note['actor_operation'])
|
||||
|
||||
def _assert_last_audit(self, resource_id, operation, resource_type,
|
||||
target_uri):
|
||||
target_uri, reason=None):
|
||||
# NOTE(stevemar): If 'cadf' format is not used, then simply
|
||||
# return since this assertion is not valid.
|
||||
if CONF.notification_format != 'cadf':
|
||||
|
@ -298,13 +305,22 @@ class BaseNotificationTest(test_v3.RestfulTestCase):
|
|||
self.assertGreater(len(self._audits), 0)
|
||||
audit = self._audits[-1]
|
||||
payload = audit['payload']
|
||||
self.assertEqual(resource_id, payload['resource_info'])
|
||||
action = '%s.%s' % (operation, resource_type)
|
||||
if 'resource_info' in payload:
|
||||
self.assertEqual(resource_id, payload['resource_info'])
|
||||
action = '.'.join(filter(None, [operation, resource_type]))
|
||||
self.assertEqual(action, payload['action'])
|
||||
self.assertEqual(target_uri, payload['target']['typeURI'])
|
||||
self.assertEqual(resource_id, payload['target']['id'])
|
||||
event_type = '%s.%s.%s' % ('identity', resource_type, operation)
|
||||
if resource_id:
|
||||
self.assertEqual(resource_id, payload['target']['id'])
|
||||
event_type = '.'.join(filter(None, ['identity',
|
||||
resource_type,
|
||||
operation]))
|
||||
self.assertEqual(event_type, audit['event_type'])
|
||||
if reason:
|
||||
self.assertEqual(reason['reasonCode'],
|
||||
payload['reason']['reasonCode'])
|
||||
self.assertEqual(reason['reasonType'],
|
||||
payload['reason']['reasonType'])
|
||||
self.assertTrue(audit['send_notification_called'])
|
||||
|
||||
def _assert_initiator_data_is_set(self, operation, resource_type, typeURI):
|
||||
|
@ -686,6 +702,171 @@ class NotificationsForEntities(BaseNotificationTest):
|
|||
actor_operation='removed')
|
||||
|
||||
|
||||
class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
|
||||
|
||||
def setUp(self):
|
||||
super(CADFNotificationsForPCIDSSEvents, self).setUp()
|
||||
conf = self.useFixture(config_fixture.Config(CONF))
|
||||
conf.config(notification_format='cadf')
|
||||
conf.config(group='security_compliance',
|
||||
password_expires_days=2)
|
||||
conf.config(group='security_compliance',
|
||||
lockout_failure_attempts=3)
|
||||
conf.config(group='security_compliance',
|
||||
unique_last_password_count=2)
|
||||
conf.config(group='security_compliance',
|
||||
minimum_password_age=2)
|
||||
conf.config(group='security_compliance',
|
||||
password_regex='^(?=.*\d)(?=.*[a-zA-Z]).{7,}$')
|
||||
conf.config(group='security_compliance',
|
||||
password_regex_description='1 letter, 1 digit, 7 chars')
|
||||
|
||||
def test_password_expired_sends_notification(self):
|
||||
password = uuid.uuid4().hex
|
||||
password_creation_time = (
|
||||
datetime.datetime.utcnow() -
|
||||
datetime.timedelta(
|
||||
days=CONF.security_compliance.password_expires_days + 1)
|
||||
)
|
||||
freezer = freezegun.freeze_time(password_creation_time)
|
||||
|
||||
# NOTE(gagehugo): This part below uses freezegun to spoof
|
||||
# the time as being three days in the past from right now. We will
|
||||
# create a user and have that user successfully authenticate,
|
||||
# then stop the time machine and return to the present time,
|
||||
# where the user's password is now expired.
|
||||
freezer.start()
|
||||
user_ref = unit.new_user_ref(domain_id=self.domain_id,
|
||||
password=password)
|
||||
user_ref = self.identity_api.create_user(user_ref)
|
||||
self.identity_api.authenticate(self.make_request(),
|
||||
user_ref['id'], password)
|
||||
freezer.stop()
|
||||
|
||||
reason_type = (exception.PasswordExpired.message_format %
|
||||
{'user_id': user_ref['id']})
|
||||
expected_reason = {'reasonCode': '401',
|
||||
'reasonType': reason_type}
|
||||
self.assertRaises(exception.PasswordExpired,
|
||||
self.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
password=password)
|
||||
self._assert_last_audit(None, 'authenticate', None,
|
||||
cadftaxonomy.ACCOUNT_USER,
|
||||
reason=expected_reason)
|
||||
|
||||
def test_locked_out_user_sends_notification(self):
|
||||
password = uuid.uuid4().hex
|
||||
new_password = uuid.uuid4().hex
|
||||
expected_responses = [AssertionError, AssertionError, AssertionError,
|
||||
exception.AccountLocked]
|
||||
user_ref = unit.new_user_ref(domain_id=self.domain_id,
|
||||
password=password)
|
||||
user_ref = self.identity_api.create_user(user_ref)
|
||||
reason_type = (exception.AccountLocked.message_format %
|
||||
{'user_id': user_ref['id']})
|
||||
expected_reason = {'reasonCode': '401',
|
||||
'reasonType': reason_type}
|
||||
for ex in expected_responses:
|
||||
self.assertRaises(ex,
|
||||
self.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
original_password=new_password,
|
||||
new_password=new_password)
|
||||
|
||||
self._assert_last_audit(None, 'authenticate', None,
|
||||
cadftaxonomy.ACCOUNT_USER,
|
||||
reason=expected_reason)
|
||||
|
||||
def test_repeated_password_sends_notification(self):
|
||||
conf = self.useFixture(config_fixture.Config(CONF))
|
||||
conf.config(group='security_compliance',
|
||||
minimum_password_age=0)
|
||||
password = uuid.uuid4().hex
|
||||
new_password = uuid.uuid4().hex
|
||||
count = CONF.security_compliance.unique_last_password_count
|
||||
reason_type = (exception.PasswordHistoryValidationError.message_format
|
||||
% {'unique_count': count})
|
||||
expected_reason = {'reasonCode': '400',
|
||||
'reasonType': reason_type}
|
||||
user_ref = unit.new_user_ref(domain_id=self.domain_id,
|
||||
password=password)
|
||||
user_ref = self.identity_api.create_user(user_ref)
|
||||
self.identity_api.change_password(self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
original_password=password,
|
||||
new_password=new_password)
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
self.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
original_password=new_password,
|
||||
new_password=password)
|
||||
|
||||
self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user',
|
||||
cadftaxonomy.SECURITY_ACCOUNT_USER,
|
||||
reason=expected_reason)
|
||||
|
||||
def test_invalid_password_sends_notification(self):
|
||||
password = uuid.uuid4().hex
|
||||
invalid_password = '1'
|
||||
regex = CONF.security_compliance.password_regex_description
|
||||
reason_type = (exception.PasswordRequirementsValidationError
|
||||
.message_format %
|
||||
{'detail': regex})
|
||||
expected_reason = {'reasonCode': '400',
|
||||
'reasonType': reason_type}
|
||||
user_ref = unit.new_user_ref(domain_id=self.domain_id,
|
||||
password=password)
|
||||
user_ref = self.identity_api.create_user(user_ref)
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
self.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
original_password=password,
|
||||
new_password=invalid_password)
|
||||
|
||||
self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user',
|
||||
cadftaxonomy.SECURITY_ACCOUNT_USER,
|
||||
reason=expected_reason)
|
||||
|
||||
def test_changing_password_too_early_sends_notification(self):
|
||||
password = uuid.uuid4().hex
|
||||
new_password = uuid.uuid4().hex
|
||||
next_password = uuid.uuid4().hex
|
||||
|
||||
user_ref = unit.new_user_ref(domain_id=self.domain_id,
|
||||
password=password,
|
||||
password_created_at=(
|
||||
datetime.datetime.utcnow()))
|
||||
user_ref = self.identity_api.create_user(user_ref)
|
||||
|
||||
min_days = CONF.security_compliance.minimum_password_age
|
||||
min_age = (user_ref['password_created_at'] +
|
||||
datetime.timedelta(days=min_days))
|
||||
days_left = (min_age - datetime.datetime.utcnow()).days
|
||||
reason_type = (exception.PasswordAgeValidationError.message_format %
|
||||
{'min_age_days': min_days, 'days_left': days_left})
|
||||
expected_reason = {'reasonCode': '400',
|
||||
'reasonType': reason_type}
|
||||
self.identity_api.change_password(self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
original_password=password,
|
||||
new_password=new_password)
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
self.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
original_password=new_password,
|
||||
new_password=next_password)
|
||||
|
||||
self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user',
|
||||
cadftaxonomy.SECURITY_ACCOUNT_USER,
|
||||
reason=expected_reason)
|
||||
|
||||
|
||||
class CADFNotificationsForEntities(NotificationsForEntities):
|
||||
|
||||
def setUp(self):
|
||||
|
@ -990,7 +1171,7 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase):
|
|||
self._notifications = []
|
||||
|
||||
def fake_notify(action, initiator, outcome, target,
|
||||
event_type, **kwargs):
|
||||
event_type, reason=None, **kwargs):
|
||||
service_security = cadftaxonomy.SERVICE_SECURITY
|
||||
|
||||
event = eventfactory.EventFactory().new_event(
|
||||
|
@ -999,6 +1180,7 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase):
|
|||
action=action,
|
||||
initiator=initiator,
|
||||
target=target,
|
||||
reason=reason,
|
||||
observer=cadfresource.Resource(typeURI=service_security))
|
||||
|
||||
for key, value in kwargs.items():
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
features:
|
||||
- >
|
||||
[`blueprint pci-dss-notifications <https://blueprints.launchpad.net/keystone/+spec/pci-dss-notifications>`_]
|
||||
CADF notifications now extend to PCI-DSS events. A ``reason`` object
|
||||
is added to the notification. A ``reason`` object has both a ``reasonType``
|
||||
(a short description of the reason) and ``reasonCode`` (the HTTP return code).
|
||||
The following events will be impacted:
|
||||
|
||||
* If a user does not change their passwords at least once every X days.
|
||||
See ``[security_compliance] password_expires_days``.
|
||||
* If a user is locked out after many failed authentication attempts.
|
||||
See ``[security_compliance] lockout_failure_attempts``.
|
||||
* If a user submits a new password that was recently used. See
|
||||
``[security_compliance] unique_last_password_count``.
|
||||
* If a password does not meet the specified criteria. See
|
||||
``[security_compliance] password_regex``.
|
||||
* If a user attempts to change their password too often. See
|
||||
``[security_compliance] minimum_password_age``.
|
||||
|
||||
See http://docs.openstack.org/developer/keystone/event_notifications.html for
|
||||
additional details.
|
Loading…
Reference in New Issue