keystone/keystone/tests/test_notifications.py

619 lines
25 KiB
Python

# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import uuid
import mock
from oslo.config import cfg
from oslotest import mockpatch
import testtools
from keystone.common import dependency
from keystone import notifications
from keystone.tests import test_v3
CONF = cfg.CONF
EXP_RESOURCE_TYPE = uuid.uuid4().hex
CREATED_OPERATION = 'created'
UPDATED_OPERATION = 'updated'
DELETED_OPERATION = 'deleted'
class ArbitraryException(Exception):
pass
def register_callback(operation, resource_type=EXP_RESOURCE_TYPE):
"""Helper for creating and registering a mock callback.
"""
callback = mock.Mock(__name__='callback',
im_class=mock.Mock(__name__='class'))
notifications.register_event_callback(operation, resource_type, callback)
return callback
class NotificationsWrapperTestCase(testtools.TestCase):
def create_fake_ref(self):
resource_id = uuid.uuid4().hex
return resource_id, {
'id': resource_id,
'key': uuid.uuid4().hex
}
@notifications.created(EXP_RESOURCE_TYPE)
def create_resource(self, resource_id, data):
return data
def test_resource_created_notification(self):
exp_resource_id, data = self.create_fake_ref()
callback = register_callback(CREATED_OPERATION)
self.create_resource(exp_resource_id, data)
callback.assert_called_with('identity', EXP_RESOURCE_TYPE,
CREATED_OPERATION,
{'resource_info': exp_resource_id})
@notifications.updated(EXP_RESOURCE_TYPE)
def update_resource(self, resource_id, data):
return data
def test_resource_updated_notification(self):
exp_resource_id, data = self.create_fake_ref()
callback = register_callback(UPDATED_OPERATION)
self.update_resource(exp_resource_id, data)
callback.assert_called_with('identity', EXP_RESOURCE_TYPE,
UPDATED_OPERATION,
{'resource_info': exp_resource_id})
@notifications.deleted(EXP_RESOURCE_TYPE)
def delete_resource(self, resource_id):
pass
def test_resource_deleted_notification(self):
exp_resource_id = uuid.uuid4().hex
callback = register_callback(DELETED_OPERATION)
self.delete_resource(exp_resource_id)
callback.assert_called_with('identity', EXP_RESOURCE_TYPE,
DELETED_OPERATION,
{'resource_info': exp_resource_id})
@notifications.created(EXP_RESOURCE_TYPE)
def create_exception(self, resource_id):
raise ArbitraryException()
def test_create_exception_without_notification(self):
callback = register_callback(CREATED_OPERATION)
self.assertRaises(
ArbitraryException, self.create_exception, uuid.uuid4().hex)
self.assertFalse(callback.called)
@notifications.created(EXP_RESOURCE_TYPE)
def update_exception(self, resource_id):
raise ArbitraryException()
def test_update_exception_without_notification(self):
callback = register_callback(UPDATED_OPERATION)
self.assertRaises(
ArbitraryException, self.update_exception, uuid.uuid4().hex)
self.assertFalse(callback.called)
@notifications.deleted(EXP_RESOURCE_TYPE)
def delete_exception(self, resource_id):
raise ArbitraryException()
def test_delete_exception_without_notification(self):
callback = register_callback(DELETED_OPERATION)
self.assertRaises(
ArbitraryException, self.delete_exception, uuid.uuid4().hex)
self.assertFalse(callback.called)
class NotificationsTestCase(testtools.TestCase):
def setUp(self):
super(NotificationsTestCase, self).setUp()
# these should use self.config_fixture.config(), but they haven't
# been registered yet
CONF.rpc_backend = 'fake'
CONF.notification_driver = ['fake']
def test_send_notification(self):
"""Test the private method _send_notification to ensure event_type,
payload, and context are built and passed properly.
"""
resource = uuid.uuid4().hex
resource_type = EXP_RESOURCE_TYPE
operation = CREATED_OPERATION
# NOTE(ldbragst): Even though notifications._send_notification doesn't
# contain logic that creates cases, this is supposed to test that
# context is always empty and that we ensure the resource ID of the
# resource in the notification is contained in the payload. It was
# agreed that context should be empty in Keystone's case, which is
# also noted in the /keystone/notifications.py module. This test
# ensures and maintains these conditions.
expected_args = [
{}, # empty context
'identity.%s.created' % resource_type, # event_type
{'resource_info': resource}, # payload
'INFO', # priority is always INFO...
]
with mock.patch.object(notifications._get_notifier(),
'_notify') as mocked:
notifications._send_notification(operation, resource_type,
resource)
mocked.assert_called_once_with(*expected_args)
class NotificationsForEntities(test_v3.RestfulTestCase):
def setUp(self):
super(NotificationsForEntities, self).setUp()
self._notifications = []
def fake_notify(operation, resource_type, resource_id,
public=True):
note = {
'resource_id': resource_id,
'operation': operation,
'resource_type': resource_type,
'send_notification_called': True,
'public': public}
self._notifications.append(note)
self.useFixture(mockpatch.PatchObject(
notifications, '_send_notification', fake_notify))
def _assertNotifySeen(self, resource_id, operation, resource_type):
self.assertIn(operation, self.exp_operations)
self.assertIn(resource_id, self.exp_resource_ids)
self.assertIn(resource_type, self.exp_resource_types)
self.assertTrue(self.send_notification_called)
def _assertLastNotify(self, resource_id, operation, resource_type):
self.assertTrue(len(self._notifications) > 0)
note = self._notifications[-1]
self.assertEqual(note['operation'], operation)
self.assertEqual(note['resource_id'], resource_id)
self.assertEqual(note['resource_type'], resource_type)
self.assertTrue(note['send_notification_called'])
def _assertNotifyNotSent(self, resource_id, operation, resource_type,
public=True):
unexpected = {
'resource_id': resource_id,
'operation': operation,
'resource_type': resource_type,
'send_notification_called': True,
'public': public}
for note in self._notifications:
self.assertNotEqual(unexpected, note)
def _assertNotifySent(self, resource_id, operation, resource_type, public):
expected = {
'resource_id': resource_id,
'operation': operation,
'resource_type': resource_type,
'send_notification_called': True,
'public': public}
for note in self._notifications:
if expected == note:
break
else:
self.fail("Notification not sent.")
def test_create_group(self):
group_ref = self.new_group_ref(domain_id=self.domain_id)
group_ref = self.identity_api.create_group(group_ref)
self._assertLastNotify(group_ref['id'], CREATED_OPERATION, 'group')
def test_create_project(self):
project_ref = self.new_project_ref(domain_id=self.domain_id)
self.assignment_api.create_project(project_ref['id'], project_ref)
self._assertLastNotify(
project_ref['id'], CREATED_OPERATION, 'project')
def test_create_role(self):
role_ref = self.new_role_ref()
self.assignment_api.create_role(role_ref['id'], role_ref)
self._assertLastNotify(role_ref['id'], CREATED_OPERATION, 'role')
def test_create_user(self):
user_ref = self.new_user_ref(domain_id=self.domain_id)
user_ref = self.identity_api.create_user(user_ref)
self._assertLastNotify(user_ref['id'], CREATED_OPERATION, 'user')
def test_create_trust(self):
trustor = self.new_user_ref(domain_id=self.domain_id)
trustor = self.identity_api.create_user(trustor)
trustee = self.new_user_ref(domain_id=self.domain_id)
trustee = self.identity_api.create_user(trustee)
role_ref = self.new_role_ref()
self.assignment_api.create_role(role_ref['id'], role_ref)
trust_ref = self.new_trust_ref(trustor['id'],
trustee['id'])
self.trust_api.create_trust(trust_ref['id'],
trust_ref,
[role_ref])
self._assertLastNotify(
trust_ref['id'], CREATED_OPERATION, 'OS-TRUST:trust')
def test_delete_group(self):
group_ref = self.new_group_ref(domain_id=self.domain_id)
group_ref = self.identity_api.create_group(group_ref)
self.identity_api.delete_group(group_ref['id'])
self._assertLastNotify(group_ref['id'], DELETED_OPERATION, 'group')
def test_delete_project(self):
project_ref = self.new_project_ref(domain_id=self.domain_id)
self.assignment_api.create_project(project_ref['id'], project_ref)
self.assignment_api.delete_project(project_ref['id'])
self._assertLastNotify(
project_ref['id'], DELETED_OPERATION, 'project')
def test_delete_role(self):
role_ref = self.new_role_ref()
self.assignment_api.create_role(role_ref['id'], role_ref)
self.assignment_api.delete_role(role_ref['id'])
self._assertLastNotify(role_ref['id'], DELETED_OPERATION, 'role')
def test_delete_user(self):
user_ref = self.new_user_ref(domain_id=self.domain_id)
user_ref = self.identity_api.create_user(user_ref)
self.identity_api.delete_user(user_ref['id'])
self._assertLastNotify(user_ref['id'], DELETED_OPERATION, 'user')
def test_update_domain(self):
domain_ref = self.new_domain_ref()
self.assignment_api.create_domain(domain_ref['id'], domain_ref)
domain_ref['description'] = uuid.uuid4().hex
self.assignment_api.update_domain(domain_ref['id'], domain_ref)
self._assertLastNotify(domain_ref['id'], UPDATED_OPERATION, 'domain')
def test_delete_trust(self):
trustor = self.new_user_ref(domain_id=self.domain_id)
trustor = self.identity_api.create_user(trustor)
trustee = self.new_user_ref(domain_id=self.domain_id)
trustee = self.identity_api.create_user(trustee)
role_ref = self.new_role_ref()
trust_ref = self.new_trust_ref(trustor['id'], trustee['id'])
self.trust_api.create_trust(trust_ref['id'],
trust_ref,
[role_ref])
self.trust_api.delete_trust(trust_ref['id'])
self._assertLastNotify(
trust_ref['id'], DELETED_OPERATION, 'OS-TRUST:trust')
def test_delete_domain(self):
domain_ref = self.new_domain_ref()
self.assignment_api.create_domain(domain_ref['id'], domain_ref)
domain_ref['enabled'] = False
self.assignment_api.update_domain(domain_ref['id'], domain_ref)
self.assignment_api.delete_domain(domain_ref['id'])
self._assertLastNotify(domain_ref['id'], DELETED_OPERATION, 'domain')
def test_delete_endpoint(self):
endpoint_ref = self.new_endpoint_ref(service_id=self.service_id)
self.catalog_api.create_endpoint(endpoint_ref['id'], endpoint_ref)
self.catalog_api.delete_endpoint(endpoint_ref['id'])
self._assertNotifySent(endpoint_ref['id'], DELETED_OPERATION,
'endpoint', public=False)
def test_disable_domain(self):
domain_ref = self.new_domain_ref()
self.assignment_api.create_domain(domain_ref['id'], domain_ref)
domain_ref['enabled'] = False
self.assignment_api.update_domain(domain_ref['id'], domain_ref)
self._assertNotifySent(domain_ref['id'], 'disabled', 'domain',
public=False)
def test_disable_of_disabled_domain_does_not_notify(self):
domain_ref = self.new_domain_ref()
domain_ref['enabled'] = False
self.assignment_api.create_domain(domain_ref['id'], domain_ref)
# The domain_ref above is not changed during the create process. We
# can use the same ref to perform the update.
self.assignment_api.update_domain(domain_ref['id'], domain_ref)
self._assertNotifyNotSent(domain_ref['id'], 'disabled', 'domain',
public=False)
def test_update_group(self):
group_ref = self.new_group_ref(domain_id=self.domain_id)
group_ref = self.identity_api.create_group(group_ref)
self.identity_api.update_group(group_ref['id'], group_ref)
self._assertLastNotify(group_ref['id'], UPDATED_OPERATION, 'group')
def test_update_project(self):
project_ref = self.new_project_ref(domain_id=self.domain_id)
self.assignment_api.create_project(project_ref['id'], project_ref)
self.assignment_api.update_project(project_ref['id'], project_ref)
self._assertNotifySent(
project_ref['id'], UPDATED_OPERATION, 'project', public=True)
def test_disable_project(self):
project_ref = self.new_project_ref(domain_id=self.domain_id)
self.assignment_api.create_project(project_ref['id'], project_ref)
project_ref['enabled'] = False
self.assignment_api.update_project(project_ref['id'], project_ref)
self._assertNotifySent(project_ref['id'], 'disabled', 'project',
public=False)
def test_disable_of_disabled_project_does_not_notify(self):
project_ref = self.new_project_ref(domain_id=self.domain_id)
project_ref['enabled'] = False
self.assignment_api.create_project(project_ref['id'], project_ref)
# The project_ref above is not changed during the create process. We
# can use the same ref to perform the update.
self.assignment_api.update_project(project_ref['id'], project_ref)
self._assertNotifyNotSent(project_ref['id'], 'disabled', 'project',
public=False)
def test_update_project_does_not_send_disable(self):
project_ref = self.new_project_ref(domain_id=self.domain_id)
self.assignment_api.create_project(project_ref['id'], project_ref)
project_ref['enabled'] = True
self.assignment_api.update_project(project_ref['id'], project_ref)
self._assertLastNotify(
project_ref['id'], UPDATED_OPERATION, 'project')
self._assertNotifyNotSent(project_ref['id'], 'disabled', 'project')
def test_update_role(self):
role_ref = self.new_role_ref()
self.assignment_api.create_role(role_ref['id'], role_ref)
self.assignment_api.update_role(role_ref['id'], role_ref)
self._assertLastNotify(role_ref['id'], UPDATED_OPERATION, 'role')
def test_update_user(self):
user_ref = self.new_user_ref(domain_id=self.domain_id)
user_ref = self.identity_api.create_user(user_ref)
self.identity_api.update_user(user_ref['id'], user_ref)
self._assertLastNotify(user_ref['id'], UPDATED_OPERATION, 'user')
class TestEventCallbacks(test_v3.RestfulTestCase):
def setUp(self):
super(TestEventCallbacks, self).setUp()
self.has_been_called = False
def _project_deleted_callback(self, service, resource_type, operation,
payload):
self.has_been_called = True
def _project_created_callback(self, service, resource_type, operation,
payload):
self.has_been_called = True
def test_notification_received(self):
callback = register_callback(CREATED_OPERATION, 'project')
project_ref = self.new_project_ref(domain_id=self.domain_id)
self.assignment_api.create_project(project_ref['id'], project_ref)
self.assertTrue(callback.called)
def test_notification_method_not_callable(self):
fake_method = None
self.assertRaises(TypeError,
notifications.register_event_callback,
UPDATED_OPERATION,
'project',
[fake_method])
def test_notification_event_not_valid(self):
self.assertRaises(ValueError,
notifications.register_event_callback,
uuid.uuid4().hex,
'project',
self._project_deleted_callback)
def test_event_registration_for_unknown_resource_type(self):
# Registration for unknown resource types should succeed. If no event
# is issued for that resource type, the callback wont be triggered.
notifications.register_event_callback(DELETED_OPERATION,
uuid.uuid4().hex,
self._project_deleted_callback)
resource_type = uuid.uuid4().hex
notifications.register_event_callback(DELETED_OPERATION,
resource_type,
self._project_deleted_callback)
def test_provider_event_callbacks_subscription(self):
callback_called = []
@dependency.provider('foo_api')
class Foo:
def __init__(self):
self.event_callbacks = {
CREATED_OPERATION: {'project': [self.foo_callback]}}
def foo_callback(self, service, resource_type, operation,
payload):
# uses callback_called from the closure
callback_called.append(True)
Foo()
project_ref = self.new_project_ref(domain_id=self.domain_id)
self.assignment_api.create_project(project_ref['id'], project_ref)
self.assertEqual([True], callback_called)
def test_invalid_event_callbacks(self):
@dependency.provider('foo_api')
class Foo:
def __init__(self):
self.event_callbacks = 'bogus'
self.assertRaises(ValueError, Foo)
def test_invalid_event_callbacks_event(self):
@dependency.provider('foo_api')
class Foo:
def __init__(self):
self.event_callbacks = {CREATED_OPERATION: 'bogus'}
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)
self.useFixture(mockpatch.PatchObject(
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)
class TestCallbackRegistration(testtools.TestCase):
def setUp(self):
super(TestCallbackRegistration, self).setUp()
self.mock_log = mock.Mock()
# Force the callback logging to occur
self.mock_log.logger.getEffectiveLevel.return_value = 1
def verify_log_message(self, data):
"""Tests that use this are a little brittle because adding more
logging can break them.
TODO(dstanek): remove the need for this in a future refactoring
"""
self.assertEqual(len(data), self.mock_log.info.call_count)
for i, data in enumerate(data):
self.mock_log.info.assert_any_call(mock.ANY, data)
def test_a_function_callback(self):
def callback(*args, **kwargs):
pass
resource_type = 'thing'
with mock.patch('keystone.notifications.LOG', self.mock_log):
notifications.register_event_callback(
CREATED_OPERATION, resource_type, callback)
expected_log_data = {
'callback': 'keystone.tests.test_notifications.callback',
'event': 'identity.%s.created' % resource_type
}
self.verify_log_message([expected_log_data])
def test_a_method_callback(self):
class C(object):
def callback(self, *args, **kwargs):
pass
with mock.patch('keystone.notifications.LOG', self.mock_log):
notifications.register_event_callback(
CREATED_OPERATION, 'thing', C.callback)
expected_log_data = {
'callback': 'keystone.tests.test_notifications.C.callback',
'event': 'identity.thing.created'
}
self.verify_log_message([expected_log_data])
def test_a_list_of_callbacks(self):
def callback(*args, **kwargs):
pass
class C(object):
def callback(self, *args, **kwargs):
pass
with mock.patch('keystone.notifications.LOG', self.mock_log):
notifications.register_event_callback(
CREATED_OPERATION, 'thing', [callback, C.callback])
expected_log_data = [
{
'callback': 'keystone.tests.test_notifications.callback',
'event': 'identity.thing.created'
},
{
'callback': 'keystone.tests.test_notifications.C.callback',
'event': 'identity.thing.created'
},
]
self.verify_log_message(expected_log_data)
def test_an_invalid_callback(self):
self.assertRaises(TypeError,
notifications.register_event_callback,
(CREATED_OPERATION, 'thing', object()))
def test_an_invalid_event(self):
def callback(*args, **kwargs):
pass
self.assertRaises(ValueError,
notifications.register_event_callback,
uuid.uuid4().hex,
'thing',
callback)