Only allow safe context fields in notifications
Publishing a fully hydrated context object in a notification would give
someone with access to that notification the ability to impersonate the
original actor through inclusion of sensitive fields.
Now, instead, we pare down the context object to the bare minimum before
passing it for serialization in notification workflows.
Closes-bug: 2030976
Change-Id: Ic94323658c89df1c1ff32f511ca23502317d0f00
(cherry picked from commit 1b315615e7
)
This commit is contained in:
parent
ae7d6d28aa
commit
44d112eb9d
|
@ -171,6 +171,47 @@ def get_notification_transport(conf, url=None, allowed_remote_exmods=None):
|
|||
transport_cls=msg_transport.NotificationTransport)
|
||||
|
||||
|
||||
def _sanitize_context(ctxt):
|
||||
# NOTE(JayF): The below values are in the same order they are in
|
||||
# oslo_context.context.RequestContext.__init__()
|
||||
safe_keys = (
|
||||
'user_id',
|
||||
'project_id',
|
||||
'domain_id',
|
||||
'user_domain_id',
|
||||
'project_domain_id',
|
||||
'request_id',
|
||||
'roles',
|
||||
'user_name',
|
||||
'project_name',
|
||||
'domain_name',
|
||||
'user_domain_name',
|
||||
'project_domain_name',
|
||||
'service_user_id',
|
||||
'service_user_domain_id',
|
||||
'service_user_domain_name',
|
||||
'service_project_id',
|
||||
'service_project_name',
|
||||
'service_project_domain_id',
|
||||
'service_project_domain_name',
|
||||
'service_roles',
|
||||
'global_request_id',
|
||||
'system_scope',
|
||||
# NOTE(JayF) These have been renamed but may show up in notifications
|
||||
'user',
|
||||
'domain',
|
||||
'user_domain',
|
||||
'project_domain',
|
||||
)
|
||||
ctxt_dict = ctxt if isinstance(ctxt, dict) else ctxt.to_dict()
|
||||
safe_dict = {k: v for k, v in ctxt_dict.items()
|
||||
if k in safe_keys}
|
||||
if ctxt_dict is ctxt:
|
||||
return safe_dict
|
||||
else:
|
||||
return ctxt.__class__.from_dict(safe_dict)
|
||||
|
||||
|
||||
class Notifier(object):
|
||||
|
||||
"""Send notification messages.
|
||||
|
@ -296,7 +337,12 @@ class Notifier(object):
|
|||
def _notify(self, ctxt, event_type, payload, priority, publisher_id=None,
|
||||
retry=None):
|
||||
payload = self._serializer.serialize_entity(ctxt, payload)
|
||||
ctxt = self._serializer.serialize_context(ctxt)
|
||||
|
||||
# NOTE(JayF): We must remove secure information from notification
|
||||
# payloads, otherwise we risk sending sensitive creds
|
||||
# to a notification bus.
|
||||
safe_ctxt = _sanitize_context(ctxt)
|
||||
ctxt = self._serializer.serialize_context(safe_ctxt)
|
||||
|
||||
msg = dict(message_id=str(uuid.uuid4()),
|
||||
publisher_id=publisher_id or self.publisher_id,
|
||||
|
|
|
@ -286,18 +286,18 @@ class TestNotifyListener(test_utils.BaseTestCase, ListenerSetupMixin):
|
|||
listener_thread = self._setup_listener(transport, [endpoint],
|
||||
targets=targets)
|
||||
notifier = self._setup_notifier(transport, topics=['topic1'])
|
||||
notifier.info({'ctxt': '1'}, 'an_event.start1', 'test')
|
||||
notifier.info({'user_name': 'bob'}, 'an_event.start1', 'test')
|
||||
notifier = self._setup_notifier(transport, topics=['topic2'])
|
||||
notifier.info({'ctxt': '2'}, 'an_event.start2', 'test')
|
||||
notifier.info({'user_name': 'bob2'}, 'an_event.start2', 'test')
|
||||
|
||||
self.wait_for_messages(2)
|
||||
self.assertFalse(listener_thread.stop())
|
||||
|
||||
endpoint.info.assert_has_calls([
|
||||
mock.call({'ctxt': '1'}, 'testpublisher',
|
||||
mock.call({'user_name': 'bob'}, 'testpublisher',
|
||||
'an_event.start1', 'test',
|
||||
{'timestamp': mock.ANY, 'message_id': mock.ANY}),
|
||||
mock.call({'ctxt': '2'}, 'testpublisher',
|
||||
mock.call({'user_name': 'bob2'}, 'testpublisher',
|
||||
'an_event.start2', 'test',
|
||||
{'timestamp': mock.ANY, 'message_id': mock.ANY})],
|
||||
any_order=True)
|
||||
|
@ -326,23 +326,23 @@ class TestNotifyListener(test_utils.BaseTestCase, ListenerSetupMixin):
|
|||
transport._send_notification = mock.MagicMock(
|
||||
side_effect=side_effect)
|
||||
|
||||
notifier.info({'ctxt': '0'},
|
||||
notifier.info({'user_name': 'bob0'},
|
||||
'an_event.start', 'test message default exchange')
|
||||
mock_notifier_exchange('exchange1')
|
||||
notifier.info({'ctxt': '1'},
|
||||
notifier.info({'user_name': 'bob1'},
|
||||
'an_event.start', 'test message exchange1')
|
||||
mock_notifier_exchange('exchange2')
|
||||
notifier.info({'ctxt': '2'},
|
||||
notifier.info({'user_name': 'bob2'},
|
||||
'an_event.start', 'test message exchange2')
|
||||
|
||||
self.wait_for_messages(2)
|
||||
self.assertFalse(listener_thread.stop())
|
||||
|
||||
endpoint.info.assert_has_calls([
|
||||
mock.call({'ctxt': '1'}, 'testpublisher', 'an_event.start',
|
||||
mock.call({'user_name': 'bob1'}, 'testpublisher', 'an_event.start',
|
||||
'test message exchange1',
|
||||
{'timestamp': mock.ANY, 'message_id': mock.ANY}),
|
||||
mock.call({'ctxt': '2'}, 'testpublisher', 'an_event.start',
|
||||
mock.call({'user_name': 'bob2'}, 'testpublisher', 'an_event.start',
|
||||
'test message exchange2',
|
||||
{'timestamp': mock.ANY, 'message_id': mock.ANY})],
|
||||
any_order=True)
|
||||
|
@ -414,8 +414,8 @@ class TestNotifyListener(test_utils.BaseTestCase, ListenerSetupMixin):
|
|||
targets=targets, pool="pool2")
|
||||
|
||||
notifier = self._setup_notifier(transport, topics=["topic"])
|
||||
notifier.info({'ctxt': '0'}, 'an_event.start', 'test message0')
|
||||
notifier.info({'ctxt': '1'}, 'an_event.start', 'test message1')
|
||||
notifier.info({'user_name': 'bob0'}, 'an_event.start', 'test message0')
|
||||
notifier.info({'user_name': 'bob1'}, 'an_event.start', 'test message1')
|
||||
|
||||
self.wait_for_messages(2, "pool1")
|
||||
self.wait_for_messages(2, "pool2")
|
||||
|
@ -423,7 +423,7 @@ class TestNotifyListener(test_utils.BaseTestCase, ListenerSetupMixin):
|
|||
self.assertFalse(listener1_thread.stop())
|
||||
|
||||
def mocked_endpoint_call(i):
|
||||
return mock.call({'ctxt': '%d' % i}, 'testpublisher',
|
||||
return mock.call({'user_name': 'bob%d' % i}, 'testpublisher',
|
||||
'an_event.start', 'test message%d' % i,
|
||||
{'timestamp': mock.ANY, 'message_id': mock.ANY})
|
||||
|
||||
|
@ -452,14 +452,14 @@ class TestNotifyListener(test_utils.BaseTestCase, ListenerSetupMixin):
|
|||
targets=targets, pool="pool2")
|
||||
|
||||
def mocked_endpoint_call(i):
|
||||
return mock.call({'ctxt': '%d' % i}, 'testpublisher',
|
||||
return mock.call({'user_name': 'bob%d' % i}, 'testpublisher',
|
||||
'an_event.start', 'test message%d' % i,
|
||||
{'timestamp': mock.ANY, 'message_id': mock.ANY})
|
||||
|
||||
notifier = self._setup_notifier(transport, topics=["topic"])
|
||||
mocked_endpoint1_calls = []
|
||||
for i in range(0, 25):
|
||||
notifier.info({'ctxt': '%d' % i}, 'an_event.start',
|
||||
notifier.info({'user_name': 'bob%d' % i}, 'an_event.start',
|
||||
'test message%d' % i)
|
||||
mocked_endpoint1_calls.append(mocked_endpoint_call(i))
|
||||
|
||||
|
@ -467,7 +467,7 @@ class TestNotifyListener(test_utils.BaseTestCase, ListenerSetupMixin):
|
|||
listener2_thread.stop()
|
||||
|
||||
for i in range(0, 25):
|
||||
notifier.info({'ctxt': '%d' % i}, 'an_event.start',
|
||||
notifier.info({'user_name': 'bob%d' % i}, 'an_event.start',
|
||||
'test message%d' % i)
|
||||
mocked_endpoint1_calls.append(mocked_endpoint_call(i))
|
||||
|
||||
|
@ -476,7 +476,7 @@ class TestNotifyListener(test_utils.BaseTestCase, ListenerSetupMixin):
|
|||
listener3_thread.stop()
|
||||
|
||||
for i in range(0, 25):
|
||||
notifier.info({'ctxt': '%d' % i}, 'an_event.start',
|
||||
notifier.info({'user_name': 'bob%d' % i}, 'an_event.start',
|
||||
'test message%d' % i)
|
||||
mocked_endpoint1_calls.append(mocked_endpoint_call(i))
|
||||
|
||||
|
@ -484,7 +484,7 @@ class TestNotifyListener(test_utils.BaseTestCase, ListenerSetupMixin):
|
|||
listener3_thread.start()
|
||||
|
||||
for i in range(0, 25):
|
||||
notifier.info({'ctxt': '%d' % i}, 'an_event.start',
|
||||
notifier.info({'user_name': 'bob%d' % i}, 'an_event.start',
|
||||
'test message%d' % i)
|
||||
mocked_endpoint1_calls.append(mocked_endpoint_call(i))
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@ class TestMessagingNotifier(test_utils.BaseTestCase):
|
|||
]
|
||||
|
||||
_context = [
|
||||
('ctxt', dict(ctxt={'user': 'bob'})),
|
||||
('ctxt', dict(ctxt={'user_name': 'bob'})),
|
||||
]
|
||||
|
||||
_retry = [
|
||||
|
@ -229,6 +229,157 @@ class TestMessagingNotifier(test_utils.BaseTestCase):
|
|||
TestMessagingNotifier.generate_scenarios()
|
||||
|
||||
|
||||
class TestMessagingNotifierContextFiltering(test_utils.BaseTestCase):
|
||||
|
||||
_v1 = [
|
||||
('v1', dict(v1=True)),
|
||||
('not_v1', dict(v1=False)),
|
||||
]
|
||||
|
||||
_v2 = [
|
||||
('v2', dict(v2=True)),
|
||||
('not_v2', dict(v2=False)),
|
||||
]
|
||||
|
||||
_publisher_id = [
|
||||
('ctor_pub_id', dict(ctor_pub_id='test',
|
||||
expected_pub_id='test')),
|
||||
('prep_pub_id', dict(prep_pub_id='test.localhost',
|
||||
expected_pub_id='test.localhost')),
|
||||
('override', dict(ctor_pub_id='test',
|
||||
prep_pub_id='test.localhost',
|
||||
expected_pub_id='test.localhost')),
|
||||
]
|
||||
|
||||
_topics = [
|
||||
('no_topics', dict(topics=[])),
|
||||
('single_topic', dict(topics=['notifications'])),
|
||||
('multiple_topic2', dict(topics=['foo', 'bar'])),
|
||||
]
|
||||
|
||||
_priority = [
|
||||
('audit', dict(priority='audit')),
|
||||
('debug', dict(priority='debug')),
|
||||
('info', dict(priority='info')),
|
||||
('warn', dict(priority='warn')),
|
||||
('error', dict(priority='error')),
|
||||
('sample', dict(priority='sample')),
|
||||
('critical', dict(priority='critical')),
|
||||
]
|
||||
|
||||
_payload = [
|
||||
('payload', dict(payload={'foo': 'bar'})),
|
||||
]
|
||||
|
||||
_context = [
|
||||
('ctxt', dict(ctxt={'user_name': 'bob'})),
|
||||
]
|
||||
|
||||
_retry = [
|
||||
('unconfigured', dict()),
|
||||
('None', dict(retry=None)),
|
||||
('0', dict(retry=0)),
|
||||
('5', dict(retry=5)),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def generate_scenarios(cls):
|
||||
cls.scenarios = testscenarios.multiply_scenarios(cls._v1,
|
||||
cls._v2,
|
||||
cls._publisher_id,
|
||||
cls._topics,
|
||||
cls._priority,
|
||||
cls._payload,
|
||||
cls._retry)
|
||||
|
||||
def setUp(self):
|
||||
super(TestMessagingNotifierContextFiltering, self).setUp()
|
||||
|
||||
self.logger = self.useFixture(_ReRaiseLoggedExceptionsFixture()).logger
|
||||
self.useFixture(fixtures.MockPatchObject(
|
||||
messaging, 'LOG', self.logger))
|
||||
self.useFixture(fixtures.MockPatchObject(
|
||||
msg_notifier, '_LOG', self.logger))
|
||||
|
||||
@mock.patch('oslo_utils.timeutils.utcnow')
|
||||
def test_notifier(self, mock_utcnow):
|
||||
ctxt = {'user_name': 'bob', 'secret_data': 'redact_me'}
|
||||
safe_ctxt = {'user_name': 'bob'}
|
||||
drivers = []
|
||||
if self.v1:
|
||||
drivers.append('messaging')
|
||||
if self.v2:
|
||||
drivers.append('messagingv2')
|
||||
|
||||
self.config(driver=drivers,
|
||||
topics=self.topics,
|
||||
group='oslo_messaging_notifications')
|
||||
|
||||
transport = oslo_messaging.get_notification_transport(self.conf,
|
||||
url='fake:')
|
||||
|
||||
if hasattr(self, 'ctor_pub_id'):
|
||||
notifier = oslo_messaging.Notifier(transport,
|
||||
publisher_id=self.ctor_pub_id)
|
||||
else:
|
||||
notifier = oslo_messaging.Notifier(transport)
|
||||
|
||||
prepare_kwds = {}
|
||||
if hasattr(self, 'retry'):
|
||||
prepare_kwds['retry'] = self.retry
|
||||
if hasattr(self, 'prep_pub_id'):
|
||||
prepare_kwds['publisher_id'] = self.prep_pub_id
|
||||
if prepare_kwds:
|
||||
notifier = notifier.prepare(**prepare_kwds)
|
||||
|
||||
transport._send_notification = mock.Mock()
|
||||
|
||||
message_id = uuid.uuid4()
|
||||
uuid.uuid4 = mock.Mock(return_value=message_id)
|
||||
|
||||
mock_utcnow.return_value = datetime.datetime.utcnow()
|
||||
|
||||
message = {
|
||||
'message_id': str(message_id),
|
||||
'publisher_id': self.expected_pub_id,
|
||||
'event_type': 'test.notify',
|
||||
'priority': self.priority.upper(),
|
||||
'payload': self.payload,
|
||||
'timestamp': str(timeutils.utcnow()),
|
||||
}
|
||||
|
||||
sends = []
|
||||
if self.v1:
|
||||
sends.append(dict(version=1.0))
|
||||
if self.v2:
|
||||
sends.append(dict(version=2.0))
|
||||
|
||||
calls = []
|
||||
for send_kwargs in sends:
|
||||
for topic in self.topics:
|
||||
if hasattr(self, 'retry'):
|
||||
send_kwargs['retry'] = self.retry
|
||||
else:
|
||||
send_kwargs['retry'] = -1
|
||||
target = oslo_messaging.Target(topic='%s.%s' % (topic,
|
||||
self.priority))
|
||||
calls.append(mock.call(target,
|
||||
safe_ctxt,
|
||||
message,
|
||||
**send_kwargs))
|
||||
|
||||
method = getattr(notifier, self.priority)
|
||||
method(ctxt, 'test.notify', self.payload)
|
||||
|
||||
uuid.uuid4.assert_called_once_with()
|
||||
transport._send_notification.assert_has_calls(calls, any_order=True)
|
||||
|
||||
self.assertTrue(notifier.is_enabled())
|
||||
|
||||
|
||||
TestMessagingNotifierContextFiltering.generate_scenarios()
|
||||
|
||||
|
||||
class TestMessagingNotifierRetry(test_utils.BaseTestCase):
|
||||
|
||||
class TestingException(BaseException):
|
||||
|
@ -328,12 +479,12 @@ class TestSerializer(test_utils.BaseTestCase):
|
|||
mock_utcnow.return_value = datetime.datetime.utcnow()
|
||||
|
||||
serializer.serialize_context = mock.Mock()
|
||||
serializer.serialize_context.return_value = dict(user='alice')
|
||||
serializer.serialize_context.return_value = dict(user_name='alice')
|
||||
|
||||
serializer.serialize_entity = mock.Mock()
|
||||
serializer.serialize_entity.return_value = 'sbar'
|
||||
|
||||
notifier.info(dict(user='bob'), 'test.notify', 'bar')
|
||||
notifier.info(dict(user_name='bob'), 'test.notify', 'bar')
|
||||
|
||||
message = {
|
||||
'message_id': str(message_id),
|
||||
|
@ -344,13 +495,14 @@ class TestSerializer(test_utils.BaseTestCase):
|
|||
'timestamp': str(timeutils.utcnow()),
|
||||
}
|
||||
|
||||
self.assertEqual([(dict(user='alice'), message, 'INFO', -1)],
|
||||
self.assertEqual([(dict(user_name='alice'), message, 'INFO', -1)],
|
||||
_impl_test.NOTIFICATIONS)
|
||||
|
||||
uuid.uuid4.assert_called_once_with()
|
||||
serializer.serialize_context.assert_called_once_with(dict(user='bob'))
|
||||
serializer.serialize_entity.assert_called_once_with(dict(user='bob'),
|
||||
'bar')
|
||||
serializer.serialize_context.assert_called_once_with(
|
||||
dict(user_name='bob'))
|
||||
serializer.serialize_entity.assert_called_once_with(
|
||||
dict(user_name='bob'), 'bar')
|
||||
|
||||
|
||||
class TestNotifierTopics(test_utils.BaseTestCase):
|
||||
|
|
Loading…
Reference in New Issue