Fix cross-tenant logs submission

* add 'delegate_roles' configuration option

Change-Id: If4952b84536ef058d91f6ee2332076dc448d97bd
This commit is contained in:
Witold Bedyk 2017-04-12 16:10:47 +02:00
parent 4b743bcc6c
commit 9bb918197e
14 changed files with 181 additions and 55 deletions

View File

@ -20,7 +20,9 @@ Create logs.
None. None.
#### Query Parameters #### Query Parameters
* tenant_id (string, optional, restricted) - Tenant ID to create log on behalf of. Usage of this query parameter requires the `monitoring-delegate` role. * tenant_id (string, optional, restricted) - Tenant ID (project ID) to create
log on behalf of. Usage of this query parameter requires the role specified
in the configuration option `delegate_roles` .
#### Request Body #### Request Body
JSON object which can have a maximum size of 5 MB. It consists of global JSON object which can have a maximum size of 5 MB. It consists of global
@ -241,9 +243,6 @@ Example:
#### Path Parameters #### Path Parameters
None. None.
#### Query Parameters
* tenant_id (string, optional, restricted) - Tenant ID to create log on behalf of. Usage of this query parameter requires the `monitoring-delegate` role.
#### Request Body #### Request Body
Consists of a single plain text message or a JSON object which can have a maximum length of 1048576 characters. Consists of a single plain text message or a JSON object which can have a maximum length of 1048576 characters.

View File

@ -53,3 +53,4 @@ kafka_topics = log
path = /v2.0/log,/v3.0/logs path = /v2.0/log,/v3.0/logs
default_roles = user,domainuser,domainadmin,monasca-user default_roles = user,domainuser,domainadmin,monasca-user
agent_roles = monasca-agent agent_roles = monasca-agent
delegate_roles = admin

View File

@ -21,8 +21,6 @@ from monasca_log_api.monitoring import metrics
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
MONITORING_DELEGATE_ROLE = 'monitoring-delegate'
class LogsApi(object): class LogsApi(object):
"""Logs API. """Logs API.

View File

@ -31,7 +31,11 @@ role_m_opts = [
default=None, default=None,
help=('List of roles, that if set, mean that request ' help=('List of roles, that if set, mean that request '
'comes from agent, thus is authorized in the same ' 'comes from agent, thus is authorized in the same '
'time')) 'time')),
cfg.ListOpt(name='delegate_roles',
default=['admin'],
help=('Roles that are allowed to POST logs on '
'behalf of another tenant (project)'))
] ]
role_m_group = cfg.OptGroup(name='roles_middleware', title='roles_middleware') role_m_group = cfg.OptGroup(name='roles_middleware', title='roles_middleware')
@ -81,12 +85,15 @@ class RoleMiddleware(om.ConfigurableMiddleware):
path = /v2.0/log path = /v2.0/log
default_roles = monasca-user default_roles = monasca-user
agent_roles = monasca-log-agent agent_roles = monasca-log-agent
delegate_roles = admin
Configuration explained: Configuration explained:
* path (list) - path (or list of paths) middleware should be applied * path (list) - path (or list of paths) middleware should be applied
* agent_roles (list) - list of roles that identifies tenant as an agent * agent_roles (list) - list of roles that identifies tenant as an agent
* default_roles (list) - list of roles that should be authorized * default_roles (list) - list of roles that should be authorized
* delegate_roles (list) - list of roles that are allowed to POST logs on
behalf of another tenant (project)
Note: Note:
Being an agent means that tenant is automatically authorized. Being an agent means that tenant is automatically authorized.

View File

@ -20,7 +20,6 @@ from oslo_log import log
import six import six
from monasca_log_api.api import exceptions from monasca_log_api.api import exceptions
from monasca_log_api.api import logs_api
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
@ -214,10 +213,12 @@ def validate_payload_size(req):
) )
def validate_is_delegate(role): def validate_is_delegate(roles):
if role: delegate_roles = CONF.roles_middleware.delegate_roles
role = role.split(',') if isinstance(role, six.string_types) else role if roles and delegate_roles:
return logs_api.MONITORING_DELEGATE_ROLE in role roles = roles.split(',') if isinstance(roles, six.string_types) \
else roles
return any(x in set(delegate_roles) for x in roles)
return False return False
@ -227,7 +228,7 @@ def validate_cross_tenant(tenant_id, cross_tenant_id, roles):
if cross_tenant_id: if cross_tenant_id:
raise falcon.HTTPForbidden( raise falcon.HTTPForbidden(
'Permission denied', 'Permission denied',
'Projects %s cannot POST cross tenant metrics' % tenant_id 'Projects %s cannot POST cross tenant logs' % tenant_id
) )

View File

@ -65,8 +65,8 @@ class Logs(logs_api.LogsApi):
self._logs_size_gauge.send(name=None, self._logs_size_gauge.send(name=None,
value=int(req.content_length)) value=int(req.content_length))
tenant_id = (req.project_id if req.project_id tenant_id = (req.cross_project_id if req.cross_project_id
else req.cross_project_id) else req.project_id)
try: try:
self._processor.send_message( self._processor.send_message(

View File

@ -20,10 +20,11 @@ import unittest
from monasca_log_api.api import exceptions as log_api_exceptions from monasca_log_api.api import exceptions as log_api_exceptions
from monasca_log_api.api import headers from monasca_log_api.api import headers
from monasca_log_api.api import logs_api
from monasca_log_api.reference.v2 import logs from monasca_log_api.reference.v2 import logs
from monasca_log_api.tests import base from monasca_log_api.tests import base
ROLES = 'admin'
def _init_resource(test): def _init_resource(test):
resource = logs.Logs() resource = logs.Logs()
@ -121,7 +122,7 @@ class TestLogs(testing.TestBase):
'/log/single', '/log/single',
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: 'a:1', headers.X_DIMENSIONS.name: 'a:1',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': '0' 'Content-Length': '0'
@ -147,7 +148,7 @@ class TestLogs(testing.TestBase):
method='POST', method='POST',
query_string='tenant_id=1', query_string='tenant_id=1',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: 'a:1', headers.X_DIMENSIONS.name: 'a:1',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': '0' 'Content-Length': '0'
@ -169,7 +170,7 @@ class TestLogs(testing.TestBase):
'/log/single', '/log/single',
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '', headers.X_DIMENSIONS.name: '',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': '0' 'Content-Length': '0'
@ -187,7 +188,7 @@ class TestLogs(testing.TestBase):
'/log/single', '/log/single',
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '', headers.X_DIMENSIONS.name: '',
'Content-Type': 'video/3gpp', 'Content-Type': 'video/3gpp',
'Content-Length': '0' 'Content-Length': '0'
@ -208,7 +209,7 @@ class TestLogs(testing.TestBase):
'/log/single', '/log/single',
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '', headers.X_DIMENSIONS.name: '',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': str(content_length) 'Content-Length': str(content_length)
@ -229,7 +230,7 @@ class TestLogs(testing.TestBase):
'/log/single', '/log/single',
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '', headers.X_DIMENSIONS.name: '',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': str(content_length) 'Content-Length': str(content_length)
@ -250,7 +251,7 @@ class TestLogs(testing.TestBase):
'/log/single', '/log/single',
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '', headers.X_DIMENSIONS.name: '',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': str(content_length) 'Content-Length': str(content_length)
@ -267,7 +268,7 @@ class TestLogs(testing.TestBase):
'/log/single', '/log/single',
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '', headers.X_DIMENSIONS.name: '',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }

View File

@ -16,18 +16,19 @@ import random
import string import string
import unittest import unittest
import falcon
from falcon import testing from falcon import testing
import mock import mock
import ujson as json import ujson as json
from monasca_log_api.api import exceptions as log_api_exceptions from monasca_log_api.api import exceptions as log_api_exceptions
from monasca_log_api.api import headers from monasca_log_api.api import headers
from monasca_log_api.api import logs_api
from monasca_log_api.reference.v3 import logs from monasca_log_api.reference.v3 import logs
from monasca_log_api.tests import base from monasca_log_api.tests import base
ENDPOINT = '/logs' ENDPOINT = '/logs'
TENANT_ID = 'bob' TENANT_ID = 'bob'
ROLES = 'admin'
def _init_resource(test): def _init_resource(test):
@ -101,7 +102,7 @@ class TestLogsMonitoring(testing.TestBase):
ENDPOINT, ENDPOINT,
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_TENANT_ID.name: TENANT_ID, headers.X_TENANT_ID.name: TENANT_ID,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': str(content_length) 'Content-Length': str(content_length)
@ -137,7 +138,7 @@ class TestLogsMonitoring(testing.TestBase):
ENDPOINT, ENDPOINT,
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_TENANT_ID.name: TENANT_ID, headers.X_TENANT_ID.name: TENANT_ID,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': str(content_length) 'Content-Length': str(content_length)
@ -181,7 +182,7 @@ class TestLogsMonitoring(testing.TestBase):
ENDPOINT, ENDPOINT,
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: ROLES,
headers.X_TENANT_ID.name: TENANT_ID, headers.X_TENANT_ID.name: TENANT_ID,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': str(content_length) 'Content-Length': str(content_length)
@ -204,3 +205,98 @@ class TestLogsMonitoring(testing.TestBase):
self.assertEqual(1, size_gauge.call_count) self.assertEqual(1, size_gauge.call_count)
self.assertEqual(content_length, self.assertEqual(content_length,
size_gauge.mock_calls[0][2]['value']) size_gauge.mock_calls[0][2]['value'])
class TestLogs(testing.TestBase):
api_class = base.MockedAPI
def before(self):
self.conf = base.mock_config(self)
@mock.patch('monasca_log_api.reference.v3.common.bulk_processor.'
'BulkProcessor')
def test_should_pass_cross_tenant_id(self, bulk_processor):
logs_resource = _init_resource(self)
logs_resource._processor = bulk_processor
v3_body, v3_logs = _generate_v3_payload(1)
payload = json.dumps(v3_body)
content_length = len(payload)
self.simulate_request(
'/logs',
method='POST',
query_string='tenant_id=1',
headers={
headers.X_ROLES.name: ROLES,
'Content-Type': 'application/json',
'Content-Length': str(content_length)
},
body=payload
)
self.assertEqual(falcon.HTTP_204, self.srmock.status)
logs_resource._processor.send_message.assert_called_with(
logs=v3_logs,
global_dimensions=v3_body['dimensions'],
log_tenant_id='1')
@mock.patch('monasca_log_api.reference.v3.common.bulk_processor.'
'BulkProcessor')
def test_should_fail_not_delegate_ok_cross_tenant_id(self, _):
_init_resource(self)
self.simulate_request(
'/logs',
method='POST',
query_string='tenant_id=1',
headers={
'Content-Type': 'application/json',
'Content-Length': '0'
}
)
self.assertEqual(falcon.HTTP_403, self.srmock.status)
@mock.patch('monasca_log_api.reference.v3.common.bulk_processor.'
'BulkProcessor')
def test_should_pass_empty_cross_tenant_id_wrong_role(self,
bulk_processor):
logs_resource = _init_resource(self)
logs_resource._processor = bulk_processor
v3_body, _ = _generate_v3_payload(1)
payload = json.dumps(v3_body)
content_length = len(payload)
self.simulate_request(
'/logs',
method='POST',
headers={
headers.X_ROLES.name: 'some_role',
'Content-Type': 'application/json',
'Content-Length': str(content_length)
},
body=payload
)
self.assertEqual(falcon.HTTP_204, self.srmock.status)
self.assertEqual(1, bulk_processor.send_message.call_count)
@mock.patch('monasca_log_api.reference.v3.common.bulk_processor.'
'BulkProcessor')
def test_should_pass_empty_cross_tenant_id_ok_role(self,
bulk_processor):
logs_resource = _init_resource(self)
logs_resource._processor = bulk_processor
v3_body, _ = _generate_v3_payload(1)
payload = json.dumps(v3_body)
content_length = len(payload)
self.simulate_request(
'/logs',
method='POST',
headers={
headers.X_ROLES.name: ROLES,
'Content-Type': 'application/json',
'Content-Length': str(content_length)
},
body=payload
)
self.assertEqual(falcon.HTTP_204, self.srmock.status)
self.assertEqual(1, bulk_processor.send_message.call_count)

View File

@ -22,31 +22,31 @@ import mock
from oslotest import base as os_test from oslotest import base as os_test
from monasca_log_api.api import exceptions from monasca_log_api.api import exceptions
from monasca_log_api.api import logs_api
from monasca_log_api.reference.common import validation from monasca_log_api.reference.common import validation
from monasca_log_api.reference.v2.common import service as common_service from monasca_log_api.reference.v2.common import service as common_service
from monasca_log_api.tests import base from monasca_log_api.tests import base
class IsDelegate(os_test.BaseTestCase): class IsDelegate(os_test.BaseTestCase):
def __init__(self, *args, **kwargs):
super(IsDelegate, self).__init__(*args, **kwargs)
self._conf = None
self._roles = ['admin']
def setUp(self):
super(IsDelegate, self).setUp()
self._conf = base.mock_config(self)
def test_is_delegate_ok_role(self): def test_is_delegate_ok_role(self):
roles = logs_api.MONITORING_DELEGATE_ROLE self.assertTrue(validation.validate_is_delegate(self._roles))
self.assertTrue(validation.validate_is_delegate(roles))
def test_is_delegate_ok_role_in_roles(self): def test_is_delegate_ok_role_in_roles(self):
roles = logs_api.MONITORING_DELEGATE_ROLE + ',a_role,b_role' self._roles.extend(['a_role', 'b_role'])
self.assertTrue(validation.validate_is_delegate(roles)) self.assertTrue(validation.validate_is_delegate(self._roles))
def test_is_delegate_not_ok_role(self): def test_is_delegate_not_ok_role(self):
roles = 'a_role,b_role' roles = ['a_role', 'b_role']
self.assertFalse(validation.validate_is_delegate(roles))
def test_is_delegate_ok_role_as_list(self):
roles = {logs_api.MONITORING_DELEGATE_ROLE}
self.assertTrue(validation.validate_is_delegate(roles))
def test_is_delegate_not_ok_role_as_list(self):
roles = {'a_role', 'b_role'}
self.assertFalse(validation.validate_is_delegate(roles)) self.assertFalse(validation.validate_is_delegate(roles))

View File

@ -17,7 +17,6 @@ import mock
import ujson as json import ujson as json
from monasca_log_api.api import headers from monasca_log_api.api import headers
from monasca_log_api.api import logs_api
from monasca_log_api.reference.v2 import logs as v2_logs from monasca_log_api.reference.v2 import logs as v2_logs
from monasca_log_api.reference.v3 import logs as v3_logs from monasca_log_api.reference.v3 import logs as v3_logs
from monasca_log_api.tests import base from monasca_log_api.tests import base
@ -48,6 +47,7 @@ class SameV2V3Output(testing.TestBase):
service = 'laas' service = 'laas'
hostname = 'kornik' hostname = 'kornik'
tenant_id = 'ironMan' tenant_id = 'ironMan'
roles = 'admin'
v2_dimensions = 'hostname:%s,service:%s' % (hostname, service) v2_dimensions = 'hostname:%s,service:%s' % (hostname, service)
v3_dimensions = { v3_dimensions = {
@ -76,7 +76,7 @@ class SameV2V3Output(testing.TestBase):
'/v2.0', '/v2.0',
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: roles,
headers.X_DIMENSIONS.name: v2_dimensions, headers.X_DIMENSIONS.name: v2_dimensions,
headers.X_APPLICATION_TYPE.name: component, headers.X_APPLICATION_TYPE.name: component,
headers.X_TENANT_ID.name: tenant_id, headers.X_TENANT_ID.name: tenant_id,
@ -90,7 +90,7 @@ class SameV2V3Output(testing.TestBase):
'/v3.0', '/v3.0',
method='POST', method='POST',
headers={ headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE, headers.X_ROLES.name: roles,
headers.X_TENANT_ID.name: tenant_id, headers.X_TENANT_ID.name: tenant_id,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': '100' 'Content-Length': '100'

View File

@ -33,7 +33,8 @@ class LogApiV2Client(rest_client.RestClient):
def send_single_log(self, def send_single_log(self,
log, log,
headers=None): headers=None,
fields=None):
default_headers = { default_headers = {
'X-Tenant-Id': 'b4265b0a48ae4fd3bdcee0ad8c2b6012', 'X-Tenant-Id': 'b4265b0a48ae4fd3bdcee0ad8c2b6012',
'X-Roles': 'admin', 'X-Roles': 'admin',

View File

@ -13,6 +13,7 @@
# under the License. # under the License.
from oslo_serialization import jsonutils as json from oslo_serialization import jsonutils as json
from six.moves.urllib.parse import urlencode
from tempest.lib.common import rest_client from tempest.lib.common import rest_client
@ -31,15 +32,19 @@ class LogApiV3Client(rest_client.RestClient):
resp, response_body = self.send_request('GET', '/') resp, response_body = self.send_request('GET', '/')
return resp, response_body return resp, response_body
def send_single_log(self, log, headers=None): def send_single_log(self, log, headers=None, fields=None):
default_headers = { default_headers = {
'X-Tenant-Id': 'b4265b0a48ae4fd3bdcee0ad8c2b6012', 'X-Tenant-Id': 'b4265b0a48ae4fd3bdcee0ad8c2b6012',
'X-Roles': 'admin', 'X-Roles': 'admin',
} }
default_headers.update(headers) default_headers.update(headers)
msg = json.dumps(log) msg = json.dumps(log)
uri = LogApiV3Client._uri
resp, body = self.post(LogApiV3Client._uri, msg, default_headers) if fields:
uri += '?' + urlencode(fields)
resp, body = self.post(uri, msg, default_headers)
return resp, body return resp, body

View File

@ -110,7 +110,7 @@ class BaseLogsTestCase(test.BaseTestCase):
cls.__name__, cls.__name__,
identity_version=auth_version) identity_version=auth_version)
credentials = cred_provider.get_creds_by_roles( credentials = cred_provider.get_creds_by_roles(
['monasca-user']).credentials ['monasca-user', 'admin']).credentials
cls.os = clients.Manager(credentials=credentials) cls.os = clients.Manager(credentials=credentials)
cls.logs_clients = cls.os.log_api_clients cls.logs_clients = cls.os.log_api_clients

View File

@ -14,6 +14,7 @@
from tempest.lib.common.utils import test_utils from tempest.lib.common.utils import test_utils
from tempest.lib import decorators from tempest.lib import decorators
from testtools import matchers
from monasca_log_api_tempest.tests import base from monasca_log_api_tempest.tests import base
@ -23,23 +24,28 @@ _RETRY_WAIT = 2
class TestSingleLog(base.BaseLogsTestCase): class TestSingleLog(base.BaseLogsTestCase):
def _run_and_wait(self, key, data, version, def _run_and_wait(self, key, data, version,
content_type='application/json', headers=None): content_type='application/json',
headers=None, fields=None):
headers = base._get_headers(headers, content_type) headers = base._get_headers(headers, content_type)
def wait(): def wait():
return self.logs_search_client.count_search_messages(key, headers) > 0 return self.logs_search_client.count_search_messages(key,
headers) > 0
self.assertEqual(0, self.logs_search_client.count_search_messages(key, headers), self.assertEqual(0, self.logs_search_client.count_search_messages(key,
headers),
'Find log message in elasticsearch: {0}'.format(key)) 'Find log message in elasticsearch: {0}'.format(key))
headers = base._get_headers(headers, content_type) headers = base._get_headers(headers, content_type)
data = base._get_data(data, content_type, version=version) data = base._get_data(data, content_type, version=version)
response, _ = self.logs_clients[version].send_single_log(data, headers) client = self.logs_clients[version]
response, _ = client.send_single_log(data, headers, fields)
self.assertEqual(204, response.status) self.assertEqual(204, response.status)
test_utils.call_until_true(wait, _RETRY_COUNT * _RETRY_WAIT, _RETRY_WAIT) test_utils.call_until_true(wait, _RETRY_COUNT * _RETRY_WAIT,
_RETRY_WAIT)
response = self.logs_search_client.search_messages(key, headers) response = self.logs_search_client.search_messages(key, headers)
self.assertEqual(1, len(response)) self.assertEqual(1, len(response))
@ -91,12 +97,23 @@ class TestSingleLog(base.BaseLogsTestCase):
def test_send_header_dimensions(self): def test_send_header_dimensions(self):
sid, message = base.generate_unique_message() sid, message = base.generate_unique_message()
headers = {'X-Dimensions': headers = {'X-Dimensions':
'server:WebServer01,environment:production'} 'server:WebServer01,environment:production'}
response = self._run_and_wait(sid, message, headers=headers, response = self._run_and_wait(sid, message, headers=headers,
version="v2") version="v2")
self.assertEqual('production', response[0]['_source']['environment']) self.assertEqual('production', response[0]['_source']['environment'])
self.assertEqual('WebServer01', response[0]['_source']['server']) self.assertEqual('WebServer01', response[0]['_source']['server'])
@decorators.attr(type="gate")
def test_send_cross_tenant(self):
sid, message = base.generate_small_message()
headers = {'X-Roles': 'admin, monitoring-delegate'}
cross_tennant_id = '2106b2c8da0eecdb3df4ea84a0b5624b'
fields = {'tenant_id': cross_tennant_id}
response = self._run_and_wait(sid, message, version="v3",
headers=headers, fields=fields)
self.assertThat(response[0]['_source']['tenant'],
matchers.StartsWith(cross_tennant_id))
# TODO(trebski) following test not passing - failed to retrieve # TODO(trebski) following test not passing - failed to retrieve
# big message from elasticsearch # big message from elasticsearch