diff --git a/keystone/auth/plugins/password.py b/keystone/auth/plugins/password.py index 2b15e1550..42a1fbb63 100644 --- a/keystone/auth/plugins/password.py +++ b/keystone/auth/plugins/password.py @@ -113,6 +113,7 @@ class Password(auth.AuthMethodHandler): # all we care is password matches try: self.identity_api.authenticate( + context, user_id=user_info.user_id, password=user_info.password, domain_scope=user_info.domain_id) diff --git a/keystone/contrib/user_crud/core.py b/keystone/contrib/user_crud/core.py index 460be911e..c550cf3e6 100644 --- a/keystone/contrib/user_crud/core.py +++ b/keystone/contrib/user_crud/core.py @@ -60,6 +60,7 @@ class UserController(identity.controllers.User): try: user_ref = self.identity_api.authenticate( + context, user_id=user_id_from_token, password=original_password) if not user_ref.get('enabled', True): diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 11e8e5903..b61c34fba 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -350,8 +350,8 @@ class UserV3(controller.V3Controller): domain_scope = self._get_domain_id_for_request(context) try: - self.identity_api.change_password(user_id, original_password, - password, domain_scope) + self.identity_api.change_password( + context, user_id, original_password, password, domain_scope) except AssertionError: raise exception.Unauthorized() diff --git a/keystone/identity/core.py b/keystone/identity/core.py index 6d20a479a..5c467046e 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -270,8 +270,9 @@ class Manager(manager.Manager): # - select the right driver for this domain # - clear/set domain_ids for drivers that do not support domains + @notifications.emit_event('authenticate') @domains_configured - def authenticate(self, user_id, password, domain_scope=None): + def authenticate(self, context, user_id, password, domain_scope=None): domain_id, driver = self._get_domain_id_and_driver(domain_scope) ref = driver.authenticate(user_id, password) if not driver.is_domain_aware(): @@ -462,11 +463,11 @@ class Manager(manager.Manager): return driver.check_user_in_group(user_id, group_id) @domains_configured - def change_password(self, user_id, original_password, new_password, - domain_scope): + def change_password(self, context, user_id, original_password, + new_password, domain_scope): # authenticate() will raise an AssertionError if authentication fails - self.authenticate(user_id, original_password, + self.authenticate(context, user_id, original_password, domain_scope=domain_scope) update_dict = {'password': new_password} diff --git a/keystone/notifications.py b/keystone/notifications.py index eec7cfd54..84d37d0be 100644 --- a/keystone/notifications.py +++ b/keystone/notifications.py @@ -19,6 +19,11 @@ import socket from oslo.config import cfg from oslo import messaging +import pycadf +from pycadf import cadftaxonomy as taxonomy +from pycadf import cadftype +from pycadf import eventfactory +from pycadf import resource from keystone.openstack.common import log @@ -213,3 +218,90 @@ def _send_notification(operation, resource_type, resource_id, public=True): LOG.exception(_( 'Failed to send %(res_id)s %(event_type)s notification'), {'res_id': resource_id, 'event_type': event_type}) + + +class CadfNotificationWrapper(object): + """Send CADF event notifications for various methods. + + Sends CADF notifications for events such as whether an authentication was + successful or not. + + """ + + def __init__(self, action): + self.action = action + + def __call__(self, f): + def wrapper(wrapped_self, context, user_id, *args, **kwargs): + """Always send a notification.""" + + remote_addr = None + http_user_agent = None + environment = context.get('environment') + + if environment: + remote_addr = environment.get('REMOTE_ADDR') + http_user_agent = environment.get('HTTP_USER_AGENT') + + host = pycadf.host.Host(address=remote_addr, agent=http_user_agent) + initiator = resource.Resource(typeURI=taxonomy.ACCOUNT_USER, + name=user_id, host=host) + + _send_audit_notification(self.action, initiator, + taxonomy.OUTCOME_PENDING) + try: + result = f(wrapped_self, context, user_id, *args, **kwargs) + except Exception: + # For authentication failure send a cadf event as well + _send_audit_notification(self.action, initiator, + taxonomy.OUTCOME_FAILURE) + raise + else: + _send_audit_notification(self.action, initiator, + taxonomy.OUTCOME_SUCCESS) + return result + + return wrapper + + +def _send_audit_notification(action, initiator, outcome): + """Send CADF notification to inform observers about the affected resource. + + This method logs an exception when sending the notification fails. + + :param action: CADF action being audited (e.g., 'authenticate') + :param initiator: CADF resource representing the initiator + :param outcome: The CADF outcome (taxonomy.OUTCOME_PENDING, + taxonomy.OUTCOME_SUCCESS, taxonomy.OUTCOME_FAILURE) + + """ + + event = eventfactory.EventFactory().new_event( + eventType=cadftype.EVENTTYPE_ACTIVITY, + outcome=outcome, + action=action, + initiator=initiator, + target=resource.Resource(typeURI=taxonomy.ACCOUNT_USER), + observer=resource.Resource(typeURI='service/security')) + + context = {} + payload = event.as_dict() + LOG.debug(_('CADF Event: %s'), payload) + service = 'identity' + event_type = '%(service)s.%(action)s' % {'service': service, + 'action': action} + + notifier = _get_notifier() + + if notifier: + try: + notifier.info(context, event_type, payload) + except Exception: + # diaper defense: any exception that occurs while emitting the + # notification should not interfere with the API request + LOG.exception(_( + 'Failed to send %(action)s %(event_type)s notification'), + {'action': action, 'event_type': event_type}) + + +emit_event = CadfNotificationWrapper diff --git a/keystone/tests/test_backend.py b/keystone/tests/test_backend.py index b4ae32a07..94a431972 100644 --- a/keystone/tests/test_backend.py +++ b/keystone/tests/test_backend.py @@ -79,17 +79,20 @@ class IdentityTests(object): def test_authenticate_bad_user(self): self.assertRaises(AssertionError, self.identity_api.authenticate, + context={}, user_id=uuid.uuid4().hex, password=self.user_foo['password']) def test_authenticate_bad_password(self): self.assertRaises(AssertionError, self.identity_api.authenticate, + context={}, user_id=self.user_foo['id'], password=uuid.uuid4().hex) def test_authenticate(self): user_ref = self.identity_api.authenticate( + context={}, user_id=self.user_sna['id'], password=self.user_sna['password']) # NOTE(termie): the password field is left in user_sna to make @@ -110,6 +113,7 @@ class IdentityTests(object): self.assignment_api.add_user_to_project(self.tenant_baz['id'], user['id']) user_ref = self.identity_api.authenticate( + context={}, user_id=user['id'], password=user['password']) self.assertNotIn('password', user_ref) @@ -134,6 +138,7 @@ class IdentityTests(object): self.assertRaises(AssertionError, self.identity_api.authenticate, + context={}, user_id=id_, password='password') @@ -1819,10 +1824,12 @@ class IdentityTests(object): # with a password that is empty string or None self.assertRaises(AssertionError, self.identity_api.authenticate, + context={}, user_id='fake1', password='') self.assertRaises(AssertionError, self.identity_api.authenticate, + context={}, user_id='fake1', password=None) @@ -1835,10 +1842,12 @@ class IdentityTests(object): # with a password that is empty string or None self.assertRaises(AssertionError, self.identity_api.authenticate, + context={}, user_id='fake1', password='') self.assertRaises(AssertionError, self.identity_api.authenticate, + context={}, user_id='fake1', password=None) diff --git a/keystone/tests/test_backend_ldap.py b/keystone/tests/test_backend_ldap.py index 485d77d4b..2a822fbc1 100644 --- a/keystone/tests/test_backend_ldap.py +++ b/keystone/tests/test_backend_ldap.py @@ -447,6 +447,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.assertRaises(AssertionError, self.identity_api.authenticate, + context={}, user_id=user['id'], password=None, domain_scope=user['domain_id']) diff --git a/keystone/tests/test_notifications.py b/keystone/tests/test_notifications.py index 1ec916cd1..da8da5e9d 100644 --- a/keystone/tests/test_notifications.py +++ b/keystone/tests/test_notifications.py @@ -440,3 +440,68 @@ class TestEventCallbacks(test_v3.RestfulTestCase): notifications.SUBSCRIBERS = {} self.assertRaises(ValueError, Foo) + + +class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): + + LOCAL_HOST = 'localhost' + ACTION = 'authenticate' + + def setUp(self): + super(CadfNotificationsWrapperTestCase, self).setUp() + self._notifications = [] + + def fake_notify(action, initiator, outcome): + note = { + 'action': action, + 'initiator': initiator, + # NOTE(stevemar): outcome has 2 stages, pending and success + # so we are ignoring it for now. + #'outcome': outcome, + 'send_notification_called': True} + self._notifications.append(note) + + # TODO(stevemar): Look into using mock instead of mox + fixture = self.useFixture(moxstubout.MoxStubout()) + self.stubs = fixture.stubs + self.stubs.Set(notifications, '_send_audit_notification', + fake_notify) + + def _assertLastNotify(self, action, user_id): + self.assertTrue(self._notifications) + note = self._notifications[-1] + self.assertEqual(note['action'], action) + initiator = note['initiator'] + self.assertEqual(initiator.name, user_id) + self.assertEqual(initiator.host.address, self.LOCAL_HOST) + self.assertTrue(note['send_notification_called']) + + def test_v3_authenticate_user_name_and_domain_id(self): + user_id = self.user_id + user_name = self.user['name'] + 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) + self._assertLastNotify(self.ACTION, user_id) + + def test_v3_authenticate_user_id(self): + user_id = self.user_id + password = self.user['password'] + data = self.build_authentication_request(user_id=user_id, + password=password) + self.post('/auth/tokens', body=data) + self._assertLastNotify(self.ACTION, user_id) + + def test_v3_authenticate_user_name_and_domain_name(self): + user_id = self.user_id + user_name = self.user['name'] + password = self.user['password'] + domain_name = self.domain['name'] + data = self.build_authentication_request(username=user_name, + user_domain_name=domain_name, + password=password) + self.post('/auth/tokens', body=data) + self._assertLastNotify(self.ACTION, user_id) diff --git a/keystone/token/controllers.py b/keystone/token/controllers.py index 98698adef..7ba965c45 100644 --- a/keystone/token/controllers.py +++ b/keystone/token/controllers.py @@ -263,6 +263,7 @@ class Auth(controller.V2Controller): try: user_ref = self.identity_api.authenticate( + context, user_id=user_id, password=password) except AssertionError as e: diff --git a/requirements.txt b/requirements.txt index 4758552ca..217a243b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ Babel>=1.3 oauthlib>=0.6 dogpile.cache>=0.5.0 jsonschema>=2.0.0,<3.0.0 +pycadf>=0.1.9 # KDS exclusive dependencies