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.
#### 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
JSON object which can have a maximum size of 5 MB. It consists of global
@ -241,9 +243,6 @@ Example:
#### Path Parameters
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
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
default_roles = user,domainuser,domainadmin,monasca-user
agent_roles = monasca-agent
delegate_roles = admin

View File

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

View File

@ -31,7 +31,11 @@ role_m_opts = [
default=None,
help=('List of roles, that if set, mean that request '
'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')
@ -81,12 +85,15 @@ class RoleMiddleware(om.ConfigurableMiddleware):
path = /v2.0/log
default_roles = monasca-user
agent_roles = monasca-log-agent
delegate_roles = admin
Configuration explained:
* path (list) - path (or list of paths) middleware should be applied
* agent_roles (list) - list of roles that identifies tenant as an agent
* 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:
Being an agent means that tenant is automatically authorized.

View File

@ -20,7 +20,6 @@ from oslo_log import log
import six
from monasca_log_api.api import exceptions
from monasca_log_api.api import logs_api
LOG = log.getLogger(__name__)
CONF = cfg.CONF
@ -214,10 +213,12 @@ def validate_payload_size(req):
)
def validate_is_delegate(role):
if role:
role = role.split(',') if isinstance(role, six.string_types) else role
return logs_api.MONITORING_DELEGATE_ROLE in role
def validate_is_delegate(roles):
delegate_roles = CONF.roles_middleware.delegate_roles
if roles and delegate_roles:
roles = roles.split(',') if isinstance(roles, six.string_types) \
else roles
return any(x in set(delegate_roles) for x in roles)
return False
@ -227,7 +228,7 @@ def validate_cross_tenant(tenant_id, cross_tenant_id, roles):
if cross_tenant_id:
raise falcon.HTTPForbidden(
'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,
value=int(req.content_length))
tenant_id = (req.project_id if req.project_id
else req.cross_project_id)
tenant_id = (req.cross_project_id if req.cross_project_id
else req.project_id)
try:
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 headers
from monasca_log_api.api import logs_api
from monasca_log_api.reference.v2 import logs
from monasca_log_api.tests import base
ROLES = 'admin'
def _init_resource(test):
resource = logs.Logs()
@ -121,7 +122,7 @@ class TestLogs(testing.TestBase):
'/log/single',
method='POST',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: 'a:1',
'Content-Type': 'application/json',
'Content-Length': '0'
@ -147,7 +148,7 @@ class TestLogs(testing.TestBase):
method='POST',
query_string='tenant_id=1',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: 'a:1',
'Content-Type': 'application/json',
'Content-Length': '0'
@ -169,7 +170,7 @@ class TestLogs(testing.TestBase):
'/log/single',
method='POST',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '',
'Content-Type': 'application/json',
'Content-Length': '0'
@ -187,7 +188,7 @@ class TestLogs(testing.TestBase):
'/log/single',
method='POST',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '',
'Content-Type': 'video/3gpp',
'Content-Length': '0'
@ -208,7 +209,7 @@ class TestLogs(testing.TestBase):
'/log/single',
method='POST',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '',
'Content-Type': 'application/json',
'Content-Length': str(content_length)
@ -229,7 +230,7 @@ class TestLogs(testing.TestBase):
'/log/single',
method='POST',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '',
'Content-Type': 'application/json',
'Content-Length': str(content_length)
@ -250,7 +251,7 @@ class TestLogs(testing.TestBase):
'/log/single',
method='POST',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '',
'Content-Type': 'application/json',
'Content-Length': str(content_length)
@ -267,7 +268,7 @@ class TestLogs(testing.TestBase):
'/log/single',
method='POST',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: '',
'Content-Type': 'application/json'
}

View File

@ -16,18 +16,19 @@ import random
import string
import unittest
import falcon
from falcon import testing
import mock
import ujson as json
from monasca_log_api.api import exceptions as log_api_exceptions
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.tests import base
ENDPOINT = '/logs'
TENANT_ID = 'bob'
ROLES = 'admin'
def _init_resource(test):
@ -101,7 +102,7 @@ class TestLogsMonitoring(testing.TestBase):
ENDPOINT,
method='POST',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_TENANT_ID.name: TENANT_ID,
'Content-Type': 'application/json',
'Content-Length': str(content_length)
@ -137,7 +138,7 @@ class TestLogsMonitoring(testing.TestBase):
ENDPOINT,
method='POST',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_TENANT_ID.name: TENANT_ID,
'Content-Type': 'application/json',
'Content-Length': str(content_length)
@ -181,7 +182,7 @@ class TestLogsMonitoring(testing.TestBase):
ENDPOINT,
method='POST',
headers={
headers.X_ROLES.name: logs_api.MONITORING_DELEGATE_ROLE,
headers.X_ROLES.name: ROLES,
headers.X_TENANT_ID.name: TENANT_ID,
'Content-Type': 'application/json',
'Content-Length': str(content_length)
@ -204,3 +205,98 @@ class TestLogsMonitoring(testing.TestBase):
self.assertEqual(1, size_gauge.call_count)
self.assertEqual(content_length,
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 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.v2.common import service as common_service
from monasca_log_api.tests import base
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):
roles = logs_api.MONITORING_DELEGATE_ROLE
self.assertTrue(validation.validate_is_delegate(roles))
self.assertTrue(validation.validate_is_delegate(self._roles))
def test_is_delegate_ok_role_in_roles(self):
roles = logs_api.MONITORING_DELEGATE_ROLE + ',a_role,b_role'
self.assertTrue(validation.validate_is_delegate(roles))
self._roles.extend(['a_role', 'b_role'])
self.assertTrue(validation.validate_is_delegate(self._roles))
def test_is_delegate_not_ok_role(self):
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'}
roles = ['a_role', 'b_role']
self.assertFalse(validation.validate_is_delegate(roles))

View File

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

View File

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

View File

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

View File

@ -110,7 +110,7 @@ class BaseLogsTestCase(test.BaseTestCase):
cls.__name__,
identity_version=auth_version)
credentials = cred_provider.get_creds_by_roles(
['monasca-user']).credentials
['monasca-user', 'admin']).credentials
cls.os = clients.Manager(credentials=credentials)
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 import decorators
from testtools import matchers
from monasca_log_api_tempest.tests import base
@ -23,23 +24,28 @@ _RETRY_WAIT = 2
class TestSingleLog(base.BaseLogsTestCase):
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)
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))
headers = base._get_headers(headers, content_type)
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)
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)
self.assertEqual(1, len(response))
@ -91,12 +97,23 @@ class TestSingleLog(base.BaseLogsTestCase):
def test_send_header_dimensions(self):
sid, message = base.generate_unique_message()
headers = {'X-Dimensions':
'server:WebServer01,environment:production'}
'server:WebServer01,environment:production'}
response = self._run_and_wait(sid, message, headers=headers,
version="v2")
self.assertEqual('production', response[0]['_source']['environment'])
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
# big message from elasticsearch