From a02a47a65f2be3d80d8e05685d6001c91aaeef25 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Thu, 25 Oct 2018 17:41:13 -0700 Subject: [PATCH] Emit CADF notifications on authentication for invalid users Emit CADF notifications on authentication when the user_name or the user_id is invalid (UserNotFound raised). This closes a minor security gap in notifications. Change-Id: If8b49b5dc49a4b0670fb81a493f50c77df7b4362 closes-bug: #1537963 --- keystone/auth/plugins/core.py | 31 ++++++++++++++ .../tests/unit/common/test_notifications.py | 42 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/keystone/auth/plugins/core.py b/keystone/auth/plugins/core.py index e8529a3997..f8dd87f502 100644 --- a/keystone/auth/plugins/core.py +++ b/keystone/auth/plugins/core.py @@ -15,17 +15,24 @@ import sys from oslo_log import log +from pycadf import cadftaxonomy as taxonomy +from pycadf import reason +from pycadf import resource import six from keystone.common import driver_hints from keystone.common import provider_api import keystone.conf from keystone import exception +from keystone import notifications CONF = keystone.conf.CONF LOG = log.getLogger(__name__) PROVIDERS = provider_api.ProviderAPIs +_NOTIFY_OP = 'authenticate' +_NOTIFY_EVENT = '{service}.{event}'.format(service=notifications.SERVICE, + event=_NOTIFY_OP) def construct_method_map_from_config(): @@ -153,6 +160,7 @@ class BaseUserInfo(provider_api.ProviderAPIMixin, object): user_info = auth_payload['user'] user_id = user_info.get('id') user_name = user_info.get('name') + domain_ref = {} if not user_id and not user_name: raise exception.ValidationError(attribute='id or name', target='user') @@ -171,6 +179,29 @@ class BaseUserInfo(provider_api.ProviderAPIMixin, object): self._assert_domain_is_enabled(domain_ref) except exception.UserNotFound as e: LOG.warning(six.text_type(e)) + + # We need to special case USER NOT FOUND here for CADF + # notifications as the normal path for notification(s) come from + # `identity_api.authenticate` and we are a bit before dropping into + # that method. + audit_reason = reason.Reason(str(e), str(e.code)) + audit_initiator = notifications.build_audit_initiator() + # build an appropriate audit initiator with relevant information + # for the failed request. This will catch invalid user_name and + # invalid user_id. + if user_name: + audit_initiator.user_name = user_name + else: + audit_initiator.user_id = user_id + audit_initiator.domain_id = domain_ref.get('id') + audit_initiator.domain_name = domain_ref.get('name') + notifications._send_audit_notification( + action=_NOTIFY_OP, + initiator=audit_initiator, + outcome=taxonomy.OUTCOME_FAILURE, + target=resource.Resource(typeURI=taxonomy.ACCOUNT_USER), + event_type=_NOTIFY_EVENT, + reason=audit_reason) raise exception.Unauthorized(e) self._assert_user_is_enabled(user_ref) self.user_ref = user_ref diff --git a/keystone/tests/unit/common/test_notifications.py b/keystone/tests/unit/common/test_notifications.py index 1b57db3249..5acc7440d7 100644 --- a/keystone/tests/unit/common/test_notifications.py +++ b/keystone/tests/unit/common/test_notifications.py @@ -25,6 +25,7 @@ from pycadf import cadftaxonomy from pycadf import cadftype from pycadf import eventfactory from pycadf import resource as cadfresource +from six.moves import http_client from keystone.common import provider_api import keystone.conf @@ -1068,6 +1069,10 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): self.useFixture(fixtures.MockPatchObject( notifications, '_send_audit_notification', fake_notify)) + def _get_last_note(self): + self.assertTrue(self._notifications) + return self._notifications[-1] + def _assert_last_note(self, action, user_id, event_type=None): self.assertTrue(self._notifications) note = self._notifications[-1] @@ -1158,6 +1163,43 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): self.post('/auth/tokens', body=data) self._assert_last_note(self.ACTION, user_id) + def test_v3_authenticate_with_invalid_user_id_sends_notification(self): + user_id = uuid.uuid4().hex + password = self.user['password'] + data = self.build_authentication_request(user_id=user_id, + password=password) + self.post('/auth/tokens', body=data, + expected_status=http_client.UNAUTHORIZED) + note = self._get_last_note() + initiator = note['initiator'] + + # Confirm user-name specific event was emitted. + self.assertEqual(self.ACTION, note['action']) + self.assertEqual(user_id, initiator.user_id) + self.assertTrue(note['send_notification_called']) + self.assertEqual(cadftaxonomy.OUTCOME_FAILURE, note['event'].outcome) + self.assertEqual(self.LOCAL_HOST, initiator.host.address) + + def test_v3_authenticate_with_invalid_user_name_sends_notification(self): + user_name = uuid.uuid4().hex + password = self.user['password'] + domain_id = self.domain_id + data = self.build_authentication_request(username=user_name, + user_domain_id=domain_id, + password=password) + self.post('/auth/tokens', body=data, + expected_status=http_client.UNAUTHORIZED) + note = self._get_last_note() + initiator = note['initiator'] + + # Confirm user-name specific event was emitted. + self.assertEqual(self.ACTION, note['action']) + self.assertEqual(user_name, initiator.user_name) + self.assertEqual(domain_id, initiator.domain_id) + self.assertTrue(note['send_notification_called']) + self.assertEqual(cadftaxonomy.OUTCOME_FAILURE, note['event'].outcome) + self.assertEqual(self.LOCAL_HOST, initiator.host.address) + def test_v3_authenticate_user_name_and_domain_name(self): user_id = self.user_id user_name = self.user['name']