Merge "Integration with oslo.context"

This commit is contained in:
Jenkins 2017-01-09 10:37:13 +00:00 committed by Gerrit Code Review
commit b23949f442
22 changed files with 335 additions and 450 deletions

View File

@ -2,8 +2,7 @@
name = monasca_api
[pipeline:main]
# Add validator in the pipeline so the metrics messages can be validated.
pipeline = auth keystonecontext api
pipeline = request_id auth api
[app:api]
paste.app_factory = monasca_api.api.server:launch
@ -11,8 +10,8 @@ paste.app_factory = monasca_api.api.server:launch
[filter:auth]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
[filter:keystonecontext]
paste.filter_factory = monasca_api.middleware.keystone_context_filter:filter_factory
[filter:request_id]
paste.filter_factory = oslo_middleware.request_id:RequestId.factory
[server:main]
use = egg:gunicorn#main

View File

@ -5,11 +5,10 @@ keys = root, sqlalchemy, kafka
keys = console, file
[formatters]
keys = generic
keys = context
[logger_root]
level = DEBUG
formatter = default
handlers = console, file
[logger_sqlalchemy]
@ -18,14 +17,12 @@ qualname = sqlalchemy.engine
# "level = DEBUG" logs SQL queries and results.
# "level = WARN" logs neither. (Recommended for production systems.)
level = DEBUG
formatter = default
handlers = console, file
propagate=0
[logger_kafka]
qualname = kafka
level = DEBUG
formatter = default
handlers = console, file
propagate = 0
@ -33,14 +30,14 @@ propagate = 0
class = logging.StreamHandler
args = (sys.stderr,)
level = DEBUG
formatter = generic
formatter = context
[handler_file]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic
formatter = context
# store up to 5*100MB of logs
args = ('/var/log/monasca/api/monasca-api.log', 'a', 104857600, 5)
[formatter_generic]
format = %(asctime)s %(levelname)s [%(name)s][%(threadName)s] %(message)s
[formatter_context]
class = oslo_log.formatters.ContextFormatter

View File

@ -2,8 +2,7 @@
name = monasca_api
[pipeline:main]
# Add validator in the pipeline so the metrics messages can be validated.
pipeline = auth keystonecontext api
pipeline = request_id auth api
[app:api]
paste.app_factory = monasca_api.api.server:launch
@ -11,8 +10,8 @@ paste.app_factory = monasca_api.api.server:launch
[filter:auth]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
[filter:keystonecontext]
paste.filter_factory = monasca_api.middleware.keystone_context_filter:filter_factory
[filter:request_id]
paste.filter_factory = oslo_middleware.request_id:RequestId.factory
[server:main]
use = egg:gunicorn#main

View File

@ -5,11 +5,10 @@ keys = root, sqlalchemy, kafka
keys = console, file
[formatters]
keys = generic
keys = context
[logger_root]
level = DEBUG
formatter = default
handlers = console, file
[logger_sqlalchemy]
@ -18,14 +17,12 @@ qualname = sqlalchemy.engine
# "level = DEBUG" logs SQL queries and results.
# "level = WARN" logs neither. (Recommended for production systems.)
level = DEBUG
formatter = default
handlers = console, file
propagate=0
[logger_kafka]
qualname = kafka
level = DEBUG
formatter = default
handlers = console, file
propagate = 0
@ -33,14 +30,14 @@ propagate = 0
class = logging.StreamHandler
args = (sys.stderr,)
level = DEBUG
formatter = generic
formatter = context
[handler_file]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic
formatter = context
# store up to 5*100MB of logs
args = ('/var/log/monasca/api/monasca-api.log', 'a', 104857600, 5)
[formatter_generic]
format = %(asctime)s %(levelname)s [%(name)s][%(threadName)s] %(message)s
[formatter_context]
class = oslo_log.formatters.ContextFormatter

View File

@ -0,0 +1,109 @@
# Copyright 2016 FUJITSU LIMITED
#
# 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 falcon
from oslo_context import context
from monasca_api.common.repositories import constants
from monasca_api.v2.common import exceptions
_TENANT_ID_PARAM = 'tenant_id'
"""Name of the query-param pointing at project-id (tenant-id)"""
class Request(falcon.Request):
"""Variation of falcon.Request with context
Following class enhances :py:class:`falcon.Request` with
:py:class:`context.RequestContext`.
"""
def __init__(self, env, options=None):
super(Request, self).__init__(env, options)
self.context = context.RequestContext.from_environ(self.env)
@property
def project_id(self):
"""Returns project-id (tenant-id)
:return: project-id
:rtype: str
"""
return self.context.tenant
@property
def cross_project_id(self):
"""Returns project-id (tenant-id) found in query params.
This particular project-id is later on identified as
cross-project-id
:return: project-id
:rtype: str
"""
return self.get_param(_TENANT_ID_PARAM, required=False)
@property
def user_id(self):
"""Returns user-id
:return: user-id
:rtype: str
"""
return self.context.user
@property
def roles(self):
"""Returns roles associated with user
:return: user's roles
:rtype: list
"""
return self.context.roles
@property
def limit(self):
"""Returns LIMIT query param value.
'limit' is not required query param.
In case it is not found, py:data:'.constants.PAGE_LIMIT'
value is returned.
:return: value of 'limit' query param or default value
:rtype: int
:raise exceptions.HTTPUnprocessableEntityError: if limit is not valid integer
"""
limit = self.get_param('limit', required=False, default=None)
if limit is not None:
if limit.isdigit():
limit = int(limit)
if limit > constants.PAGE_LIMIT:
return constants.PAGE_LIMIT
else:
return limit
else:
err_msg = 'Limit parameter must be a positive integer'
raise exceptions.HTTPUnprocessableEntityError('Invalid limit', err_msg)
else:
return constants.PAGE_LIMIT
def __repr__(self):
return '%s, context=%s' % (self.path, self.context)

View File

@ -22,6 +22,8 @@ from oslo_config import cfg
from oslo_log import log
import paste.deploy
from monasca_api.api.core import request
dispatcher_opts = [cfg.StrOpt('versions', default=None,
help='Versions'),
cfg.StrOpt('version_2_0', default=None,
@ -66,7 +68,7 @@ def launch(conf, config_file="/etc/monasca/api-config.conf"):
default_config_files=[config_file])
log.setup(cfg.CONF, 'monasca_api')
app = falcon.API()
app = falcon.API(request_type=request.Request)
versions = simport.load(cfg.CONF.dispatcher.versions)()
app.add_route("/", versions)

View File

@ -1,83 +0,0 @@
# Copyright (c) 2015 OpenStack Foundation
#
# 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.
"""RequestContext: context for requests that persist through monasca."""
import uuid
from oslo_log import log
from oslo_utils import timeutils
LOG = log.getLogger(__name__)
class RequestContext(object):
"""Security context and request information.
Represents the user taking a given action within the system.
"""
def __init__(self, user_id, project_id, domain_id=None, domain_name=None,
roles=None, timestamp=None, request_id=None,
auth_token=None, user_name=None, project_name=None,
service_catalog=None, user_auth_plugin=None, **kwargs):
"""Creates the Keystone Context. Supports additional parameters:
:param user_auth_plugin:
The auth plugin for the current request's authentication data.
:param kwargs:
Extra arguments that might be present
"""
if kwargs:
LOG.warning(
'Arguments dropped when creating context: %s') % str(kwargs)
self._roles = roles or []
self.timestamp = timeutils.utcnow()
if not request_id:
request_id = self.generate_request_id()
self._request_id = request_id
self._auth_token = auth_token
self._service_catalog = service_catalog
self._domain_id = domain_id
self._domain_name = domain_name
self._user_id = user_id
self._user_name = user_name
self._project_id = project_id
self._project_name = project_name
self._user_auth_plugin = user_auth_plugin
def to_dict(self):
return {'user_id': self._user_id,
'project_id': self._project_id,
'domain_id': self._domain_id,
'domain_name': self._domain_name,
'roles': self._roles,
'timestamp': timeutils.strtime(self._timestamp),
'request_id': self._request_id,
'auth_token': self._auth_token,
'user_name': self._user_name,
'service_catalog': self._service_catalog,
'project_name': self._project_name,
'user': self._user}
def generate_request_id(self):
return b'req-' + str(uuid.uuid4()).encode('ascii')

View File

@ -1,49 +0,0 @@
# 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.
class Inspector(object):
"""The middleware that logs the request body and header
This is a middleware for debug purposes. To enable this middleware, add
the following lines into the configuration file, for example:
[pipeline:main]
pipeline = inspector api
[filter:inspector]
use = egg: monasca_api_server#inspector
"""
def __init__(self, app, conf):
"""Inspect each request
:param app: OptionParser to use. If not sent one will be created.
:param conf: Override sys.argv; used in testing
"""
self.app = app
self.conf = conf
print('Inspect config:', self.conf)
def __call__(self, environ, start_response):
print('environ is ', environ)
return self.app(environ, start_response)
def filter_factory(global_conf, **local_conf):
def login_filter(app):
return Inspector(app, local_conf)
return login_filter

View File

@ -1,109 +0,0 @@
# Copyright (c) 2015 OpenStack Foundation
#
# 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 falcon
from oslo_log import log
from oslo_middleware import request_id
from oslo_serialization import jsonutils
from monasca_api.middleware import context
LOG = log.getLogger(__name__)
def filter_factory(global_conf, **local_conf):
def validator_filter(app):
return KeystoneContextFilter(app, local_conf)
return validator_filter
class KeystoneContextFilter(object):
"""Make a request context from keystone headers."""
def __init__(self, app, conf):
self._app = app
self._conf = conf
def __call__(self, env, start_response):
LOG.debug("Creating Keystone Context Object.")
user_id = env.get('HTTP_X_USER_ID', env.get('HTTP_X_USER'))
if user_id is None:
msg = "Neither X_USER_ID nor X_USER found in request"
LOG.error(msg)
raise falcon.HTTPUnauthorized(title='Forbidden', description=msg)
roles = self._get_roles(env)
project_id = env.get('HTTP_X_PROJECT_ID')
project_name = env.get('HTTP_X_PROJECT_NAME')
domain_id = env.get('HTTP_X_DOMAIN_ID')
domain_name = env.get('HTTP_X_DOMAIN_NAME')
user_name = env.get('HTTP_X_USER_NAME')
req_id = env.get(request_id.ENV_REQUEST_ID)
# Get the auth token
auth_token = env.get('HTTP_X_AUTH_TOKEN',
env.get('HTTP_X_STORAGE_TOKEN'))
service_catalog = None
if env.get('HTTP_X_SERVICE_CATALOG') is not None:
try:
catalog_header = env.get('HTTP_X_SERVICE_CATALOG')
service_catalog = jsonutils.loads(catalog_header)
except ValueError:
msg = "Invalid service catalog json."
LOG.error(msg)
raise falcon.HTTPInternalServerError(msg)
# NOTE(jamielennox): This is a full auth plugin set by auth_token
# middleware in newer versions.
user_auth_plugin = env.get('keystone.token_auth')
# Build a context
ctx = context.RequestContext(user_id,
project_id,
user_name=user_name,
project_name=project_name,
domain_id=domain_id,
domain_name=domain_name,
roles=roles,
auth_token=auth_token,
service_catalog=service_catalog,
request_id=req_id,
user_auth_plugin=user_auth_plugin)
env['monasca.context'] = ctx
LOG.debug("Keystone Context successfully created.")
return self._app(env, start_response)
def _get_roles(self, env):
"""Get the list of roles."""
if 'HTTP_X_ROLES' in env:
roles = env.get('HTTP_X_ROLES', '')
else:
# Fallback to deprecated role header:
roles = env.get('HTTP_X_ROLE', '')
if roles:
LOG.warning(
'Sourcing roles from deprecated X-Role HTTP header')
return [r.strip() for r in roles.split(',')]

View File

@ -1,37 +0,0 @@
# Copyright 2014 Hewlett-Packard
#
# 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.
class MockAuthFilter(object):
"""Authorization filter.
This authorization filter doesn't do any authentication, it just copies the
auth token to the tenant ID and supplies the 'admin' role and is meant for
testing purposes only.
"""
def __init__(self, app, conf):
self.app = app
self.conf = conf
def __call__(self, env, start_response):
env['HTTP_X_TENANT_ID'] = env['HTTP_X_AUTH_TOKEN']
env['HTTP_X_ROLES'] = 'admin'
return self.app(env, start_response)
def filter_factory(global_conf, **local_conf):
def validator_filter(app):
return MockAuthFilter(app, local_conf)
return validator_filter

36
monasca_api/tests/base.py Normal file
View File

@ -0,0 +1,36 @@
# Copyright 2015 kornicameister@gmail.com
# Copyright 2015 FUJITSU LIMITED
#
# 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 falcon
from monasca_api.api.core import request
class MockedAPI(falcon.API):
"""MockedAPI
Subclasses :py:class:`falcon.API` in order to overwrite
request_type property with custom :py:class:`request.Request`
"""
def __init__(self):
super(MockedAPI, self).__init__(
media_type=falcon.DEFAULT_MEDIA_TYPE,
request_type=request.Request,
response_type=falcon.Response,
middleware=None,
router=None
)

View File

@ -24,10 +24,10 @@ import fixtures
import testtools.matchers as matchers
from monasca_api.common.repositories.model import sub_alarm_definition
from monasca_api.tests import base
from monasca_api.v2.reference import alarm_definitions
from monasca_api.v2.reference import alarms
import oslo_config
import oslo_config.fixture
import oslotest.base as oslotest
@ -159,6 +159,8 @@ class RESTResponseEquals(object):
class AlarmTestBase(falcon.testing.TestBase, oslotest.BaseTestCase):
api_class = base.MockedAPI
def setUp(self):
super(AlarmTestBase, self).setUp()

View File

@ -0,0 +1,105 @@
# Copyright 2016 FUJITSU LIMITED
#
# 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.
from mock import mock
from oslo_config import fixture as oo_cfg
from oslo_context import fixture as oo_ctx
from falcon import testing
from monasca_api.api.core import request
from monasca_api.v2.common import exceptions
class TestRequest(testing.TestBase):
def test_use_context_from_request(self):
req = request.Request(
testing.create_environ(
path='/',
headers={
'X_AUTH_TOKEN': '111',
'X_USER_ID': '222',
'X_PROJECT_ID': '333',
'X_ROLES': 'terminator,predator'
}
)
)
self.assertEqual('111', req.context.auth_token)
self.assertEqual('222', req.user_id)
self.assertEqual('333', req.project_id)
self.assertEqual(['terminator', 'predator'], req.roles)
class TestRequestLimit(testing.TestBase):
def setUp(self):
super(TestRequestLimit, self).setUp()
self.useFixture(oo_cfg.Config())
self.useFixture(oo_ctx.ClearRequestContext())
def test_valid_limit(self):
expected_limit = 10
req = request.Request(
testing.create_environ(
path='/',
query_string='limit=%d' % expected_limit,
headers={
'X_AUTH_TOKEN': '111',
'X_USER_ID': '222',
'X_PROJECT_ID': '333',
'X_ROLES': 'terminator,predator'
}
)
)
self.assertEqual(expected_limit, req.limit)
def test_invalid_limit(self):
req = request.Request(
testing.create_environ(
path='/',
query_string='limit=abc',
headers={
'X_AUTH_TOKEN': '111',
'X_USER_ID': '222',
'X_PROJECT_ID': '333',
'X_ROLES': 'terminator,predator'
}
)
)
# note(trebskit) assertRaises fails to call property
# so we need the actual function
def property_wrapper():
return req.limit
self.assertRaises(
exceptions.HTTPUnprocessableEntityError,
property_wrapper
)
@mock.patch('monasca_api.common.repositories.constants.PAGE_LIMIT')
def test_default_limit(self, page_limit):
req = request.Request(
testing.create_environ(
path='/',
headers={
'X_AUTH_TOKEN': '111',
'X_USER_ID': '222',
'X_PROJECT_ID': '333',
'X_ROLES': 'terminator,predator'
}
)
)
self.assertEqual(page_limit, req.limit)

View File

@ -140,31 +140,31 @@ class TestValueMetaValidation(unittest.TestCase):
class TestRoleValidation(unittest.TestCase):
def test_role_valid(self):
req_roles = 'role0,rOlE1'
req_roles = 'role0', 'rOlE1'
authorized_roles = ['RolE1', 'Role2']
req = mock.Mock()
req.get_header.return_value = req_roles
req.roles = req_roles
helpers.validate_authorization(req, authorized_roles)
def test_role_invalid(self):
req_roles = 'role2 ,role3'
authorized_roles = ['role0', 'role1', 'role2']
req_roles = 'role2', 'role3'
authorized_roles = ['role0', 'role1']
req = mock.Mock()
req.get_header.return_value = req_roles
req.roles = req_roles
self.assertRaises(
falcon.HTTPUnauthorized,
helpers.validate_authorization, req, authorized_roles)
def test_empty_role_header(self):
req_roles = ''
req_roles = []
authorized_roles = ['Role1', 'Role2']
req = mock.Mock()
req.get_header.return_value = req_roles
req.roles = req_roles
self.assertRaises(
falcon.HTTPUnauthorized,
@ -175,7 +175,7 @@ class TestRoleValidation(unittest.TestCase):
authorized_roles = ['Role1', 'Role2']
req = mock.Mock()
req.get_header.return_value = req_roles
req.roles = req_roles
self.assertRaises(
falcon.HTTPUnauthorized,

View File

@ -60,7 +60,6 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
self._validate_alarm_definition(alarm_definition)
tenant_id = helpers.get_tenant_id(req)
name = get_query_alarm_definition_name(alarm_definition)
expression = get_query_alarm_definition_expression(alarm_definition)
description = get_query_alarm_definition_description(alarm_definition)
@ -72,7 +71,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
alarm_definition)
ok_actions = get_query_ok_actions(alarm_definition)
result = self._alarm_definition_create(tenant_id, name, expression,
result = self._alarm_definition_create(req.project_id, name, expression,
description, severity, match_by,
alarm_actions,
undetermined_actions,
@ -85,7 +84,6 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
def on_get(self, req, res, alarm_definition_id=None):
if alarm_definition_id is None:
helpers.validate_authorization(req, self._get_alarmdefs_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
name = helpers.get_query_name(req)
dimensions = helpers.get_query_dimensions(req)
severity = helpers.get_query_param(req, "severity", default_val=None)
@ -108,19 +106,18 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
except Exception:
raise HTTPUnprocessableEntityError('Unprocessable Entity',
'Offset value {} must be an integer'.format(offset))
limit = helpers.get_limit(req)
result = self._alarm_definition_list(tenant_id, name, dimensions, severity,
req.uri, sort_by, offset, limit)
result = self._alarm_definition_list(req.project_id, name,
dimensions, severity,
req.uri, sort_by,
offset, req.limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
else:
helpers.validate_authorization(req, self._get_alarmdefs_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
result = self._alarm_definition_show(tenant_id,
result = self._alarm_definition_show(req.project_id,
alarm_definition_id)
helpers.add_links_to_resource(result,
@ -137,8 +134,6 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
self._validate_alarm_definition(alarm_definition, require_all=True)
tenant_id = helpers.get_tenant_id(req)
name = get_query_alarm_definition_name(alarm_definition)
expression = get_query_alarm_definition_expression(alarm_definition)
actions_enabled = (
@ -151,7 +146,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
match_by = get_query_alarm_definition_match_by(alarm_definition)
severity = get_query_alarm_definition_severity(alarm_definition)
result = self._alarm_definition_update_or_patch(tenant_id,
result = self._alarm_definition_update_or_patch(req.project_id,
alarm_definition_id,
name,
expression,
@ -174,8 +169,6 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
alarm_definition = helpers.read_json_msg_body(req)
tenant_id = helpers.get_tenant_id(req)
# Optional args
name = get_query_alarm_definition_name(alarm_definition,
return_none=True)
@ -197,7 +190,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
severity = get_query_alarm_definition_severity(alarm_definition,
return_none=True)
result = self._alarm_definition_update_or_patch(tenant_id,
result = self._alarm_definition_update_or_patch(req.project_id,
alarm_definition_id,
name,
expression,
@ -217,8 +210,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
def on_delete(self, req, res, alarm_definition_id):
helpers.validate_authorization(req, self._default_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
self._alarm_definition_delete(tenant_id, alarm_definition_id)
self._alarm_definition_delete(req.project_id, alarm_definition_id)
res.status = falcon.HTTP_204
def _validate_name_not_conflicting(self, tenant_id, name, expected_id=None):

View File

@ -53,8 +53,6 @@ class Alarms(alarms_api_v2.AlarmsV2API,
helpers.validate_authorization(req, self._default_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
alarm = helpers.read_http_resource(req)
schema_alarm.validate(alarm)
@ -69,10 +67,10 @@ class Alarms(alarms_api_v2.AlarmsV2API,
raise HTTPUnprocessableEntityError('Unprocessable Entity',
"Field 'link' is required")
self._alarm_update(tenant_id, alarm_id, alarm['state'],
self._alarm_update(req.project_id, alarm_id, alarm['state'],
alarm['lifecycle_state'], alarm['link'])
result = self._alarm_show(req.uri, tenant_id, alarm_id)
result = self._alarm_show(req.uri, req.project_id, alarm_id)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
@ -81,12 +79,10 @@ class Alarms(alarms_api_v2.AlarmsV2API,
helpers.validate_authorization(req, self._default_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
alarm = helpers.read_http_resource(req)
schema_alarm.validate(alarm)
old_alarm = self._alarms_repo.get_alarm(tenant_id, alarm_id)[0]
old_alarm = self._alarms_repo.get_alarm(req.project_id, alarm_id)[0]
# if a field is not present or is None, replace it with the old value
if 'state' not in alarm or not alarm['state']:
@ -96,10 +92,10 @@ class Alarms(alarms_api_v2.AlarmsV2API,
if 'link' not in alarm or alarm['link'] is None:
alarm['link'] = old_alarm['link']
self._alarm_patch(tenant_id, alarm_id, alarm['state'],
self._alarm_patch(req.project_id, alarm_id, alarm['state'],
alarm['lifecycle_state'], alarm['link'])
result = self._alarm_show(req.uri, tenant_id, alarm_id)
result = self._alarm_show(req.uri, req.project_id, alarm_id)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
@ -108,15 +104,12 @@ class Alarms(alarms_api_v2.AlarmsV2API,
helpers.validate_authorization(req, self._default_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
self._alarm_delete(tenant_id, alarm_id)
self._alarm_delete(req.project_id, alarm_id)
res.status = falcon.HTTP_204
def on_get(self, req, res, alarm_id=None):
helpers.validate_authorization(req, self._get_alarms_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
if alarm_id is None:
query_parms = falcon.uri.parse_query_string(req.query_string)
@ -155,16 +148,15 @@ class Alarms(alarms_api_v2.AlarmsV2API,
raise HTTPUnprocessableEntityError("Unprocessable Entity",
"Offset value {} must be an integer".format(offset))
limit = helpers.get_limit(req)
result = self._alarm_list(req.uri, tenant_id, query_parms, offset,
limit)
result = self._alarm_list(req.uri, req.project_id,
query_parms, offset,
req.limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
else:
result = self._alarm_show(req.uri, tenant_id, alarm_id)
result = self._alarm_show(req.uri, req.project_id, alarm_id)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
@ -399,7 +391,6 @@ class AlarmsCount(alarms_api_v2.AlarmsCountV2API, alarming.Alarming):
def on_get(self, req, res):
helpers.validate_authorization(req, self._get_alarms_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
query_parms = falcon.uri.parse_query_string(req.query_string)
if 'state' in query_parms:
@ -426,9 +417,7 @@ class AlarmsCount(alarms_api_v2.AlarmsCountV2API, alarming.Alarming):
raise HTTPUnprocessableEntityError("Unprocessable Entity",
"Offset must be a valid integer, was {}".format(offset))
limit = helpers.get_limit(req)
result = self._alarms_count(req.uri, tenant_id, query_parms, offset, limit)
result = self._alarms_count(req.uri, req.project_id, query_parms, offset, req.limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
@ -507,28 +496,25 @@ class AlarmsStateHistory(alarms_api_v2.AlarmsStateHistoryV2API,
if alarm_id is None:
helpers.validate_authorization(req, self._get_alarms_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
start_timestamp = helpers.get_query_starttime_timestamp(req, False)
end_timestamp = helpers.get_query_endtime_timestamp(req, False)
query_parms = falcon.uri.parse_query_string(req.query_string)
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
result = self._alarm_history_list(tenant_id, start_timestamp,
result = self._alarm_history_list(req.project_id, start_timestamp,
end_timestamp, query_parms,
req.uri, offset, limit)
req.uri, offset, req.limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
else:
helpers.validate_authorization(req, self._get_alarms_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
result = self._alarm_history(tenant_id, [alarm_id], req.uri,
offset, limit)
result = self._alarm_history(req.project_id, [alarm_id],
req.uri, offset,
req.limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200

View File

@ -20,9 +20,9 @@ import falcon
from oslo_log import log
from oslo_utils import timeutils
import simplejson
import six
import six.moves.urllib.parse as urlparse
from monasca_api.common.repositories import constants
from monasca_api.v2.common.exceptions import HTTPUnprocessableEntityError
from monasca_api.v2.common.schemas import dimensions_schema
from monasca_api.v2.common.schemas import exceptions as schemas_exceptions
@ -54,24 +54,6 @@ def validate_json_content_type(req):
'application/json')
def is_in_role(req, authorized_roles):
"""Is one or more of the X-ROLES in the supplied authorized_roles.
:param req: HTTP request object. Must contain "X-ROLES" in the HTTP
request header.
:param authorized_roles: List of authorized roles to check against.
:return: Returns True if in the list of authorized roles, otherwise False.
"""
str_roles = req.get_header('X-ROLES')
if str_roles is None:
return False
roles = str_roles.lower().split(',')
for role in roles:
if role in authorized_roles:
return True
return False
def validate_authorization(req, authorized_roles):
"""Validates whether one or more X-ROLES in the HTTP header is authorized.
@ -85,15 +67,16 @@ def validate_authorization(req, authorized_roles):
:param authorized_roles: List of authorized roles to check against.
:raises falcon.HTTPUnauthorized
"""
str_roles = req.get_header('X-ROLES')
roles = req.roles
challenge = 'Token'
if str_roles is None:
if not roles:
raise falcon.HTTPUnauthorized('Forbidden',
'Tenant does not have any roles',
challenge)
roles = str_roles.lower().split(',')
roles = roles.split(',') if isinstance(roles, six.string_types) else roles
authorized_roles_lower = [r.lower() for r in authorized_roles]
for role in roles:
role = role.lower()
if role in authorized_roles_lower:
return
raise falcon.HTTPUnauthorized('Forbidden',
@ -102,14 +85,6 @@ def validate_authorization(req, authorized_roles):
challenge)
def get_tenant_id(req):
"""Returns the tenant ID in the HTTP request header.
:param req: HTTP request object.
"""
return req.get_header('X-TENANT-ID')
def get_x_tenant_or_tenant_id(req, delegate_authorized_roles):
"""Evaluates whether the tenant ID or cross tenant ID should be returned.
@ -118,12 +93,12 @@ def get_x_tenant_or_tenant_id(req, delegate_authorized_roles):
delegate privileges.
:returns: Returns the cross tenant or tenant ID.
"""
if is_in_role(req, delegate_authorized_roles):
if any(x in set(delegate_authorized_roles) for x in req.roles):
params = falcon.uri.parse_query_string(req.query_string)
if 'tenant_id' in params:
tenant_id = params['tenant_id']
return tenant_id
return get_tenant_id(req)
return req.project_id
def get_query_param(req, param_name, required=False, default_val=None):
@ -769,21 +744,3 @@ def dumpit_utf8(thingy):
def str_2_bool(s):
return s.lower() in ("true")
def get_limit(req):
limit = get_query_param(req, 'limit')
if limit:
if limit.isdigit():
limit = int(limit)
if limit > constants.PAGE_LIMIT:
return constants.PAGE_LIMIT
else:
return limit
else:
raise HTTPUnprocessableEntityError("Invalid limit",
"Limit parameter must "
"be a positive integer")
else:
return constants.PAGE_LIMIT

View File

@ -131,18 +131,17 @@ class Metrics(metrics_api_v2.MetricsV2API):
def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
name = helpers.get_query_name(req)
helpers.validate_query_name(name)
dimensions = helpers.get_query_dimensions(req)
helpers.validate_query_dimensions(dimensions)
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
start_timestamp = helpers.get_query_starttime_timestamp(req, False)
end_timestamp = helpers.get_query_endtime_timestamp(req, False)
helpers.validate_start_end_timestamps(start_timestamp, end_timestamp)
result = self._list_metrics(tenant_id, name, dimensions,
req.uri, offset, limit,
result = self._list_metrics(req.project_id, name,
dimensions, req.uri,
offset, req.limit,
start_timestamp, end_timestamp)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
@ -171,7 +170,6 @@ class MetricsMeasurements(metrics_api_v2.MetricsMeasurementsV2API):
def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
name = helpers.get_query_name(req, True)
helpers.validate_query_name(name)
dimensions = helpers.get_query_dimensions(req)
@ -180,14 +178,13 @@ class MetricsMeasurements(metrics_api_v2.MetricsMeasurementsV2API):
end_timestamp = helpers.get_query_endtime_timestamp(req, False)
helpers.validate_start_end_timestamps(start_timestamp, end_timestamp)
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
merge_metrics_flag = get_merge_metrics_flag(req)
group_by = helpers.get_query_group_by(req)
result = self._measurement_list(tenant_id, name, dimensions,
result = self._measurement_list(req.project_id, name, dimensions,
start_timestamp, end_timestamp,
req.uri, offset,
limit, merge_metrics_flag,
req.limit, merge_metrics_flag,
group_by)
res.body = helpers.dumpit_utf8(result)
@ -230,7 +227,6 @@ class MetricsStatistics(metrics_api_v2.MetricsStatisticsV2API):
def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
name = helpers.get_query_name(req, True)
helpers.validate_query_name(name)
dimensions = helpers.get_query_dimensions(req)
@ -241,14 +237,13 @@ class MetricsStatistics(metrics_api_v2.MetricsStatisticsV2API):
statistics = helpers.get_query_statistics(req)
period = helpers.get_query_period(req)
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
merge_metrics_flag = get_merge_metrics_flag(req)
group_by = helpers.get_query_group_by(req)
result = self._metric_statistics(tenant_id, name, dimensions,
result = self._metric_statistics(req.project_id, name, dimensions,
start_timestamp, end_timestamp,
statistics, period, req.uri,
offset, limit, merge_metrics_flag,
offset, req.limit, merge_metrics_flag,
group_by)
res.body = helpers.dumpit_utf8(result)
@ -292,13 +287,11 @@ class MetricsNames(metrics_api_v2.MetricsNamesV2API):
def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
dimensions = helpers.get_query_dimensions(req)
helpers.validate_query_dimensions(dimensions)
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
result = self._list_metric_names(tenant_id, dimensions,
req.uri, offset, limit)
result = self._list_metric_names(req.project_id, dimensions,
req.uri, offset, req.limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
@ -331,14 +324,12 @@ class DimensionValues(metrics_api_v2.DimensionValuesV2API):
def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
metric_name = helpers.get_query_param(req, 'metric_name')
dimension_name = helpers.get_query_param(req, 'dimension_name',
required=True)
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
result = self._dimension_values(tenant_id, req.uri, metric_name,
dimension_name, offset, limit)
result = self._dimension_values(req.project_id, req.uri, metric_name,
dimension_name, offset, req.limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
@ -372,12 +363,10 @@ class DimensionNames(metrics_api_v2.DimensionNamesV2API):
def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
metric_name = helpers.get_query_param(req, 'metric_name')
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
result = self._dimension_names(tenant_id, req.uri, metric_name,
offset, limit)
result = self._dimension_names(req.project_id, req.uri, metric_name,
offset, req.limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200

View File

@ -202,8 +202,7 @@ class Notifications(notifications_api_v2.NotificationsV2API):
helpers.validate_authorization(req, self._default_authorized_roles)
notification = helpers.read_http_resource(req)
self._parse_and_validate_notification(notification)
tenant_id = helpers.get_tenant_id(req)
result = self._create_notification(tenant_id, notification, req.uri)
result = self._create_notification(req.project_id, notification, req.uri)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_201
@ -211,7 +210,6 @@ class Notifications(notifications_api_v2.NotificationsV2API):
if notification_method_id is None:
helpers.validate_authorization(req,
self._get_notifications_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
sort_by = helpers.get_query_param(req, 'sort_by', default_val=None)
if sort_by is not None:
if isinstance(sort_by, basestring):
@ -223,16 +221,14 @@ class Notifications(notifications_api_v2.NotificationsV2API):
validation.validate_sort_by(sort_by, allowed_sort_by)
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
result = self._list_notifications(tenant_id, req.uri, sort_by,
offset, limit)
result = self._list_notifications(req.project_id, req.uri, sort_by,
offset, req.limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
else:
helpers.validate_authorization(req,
self._get_notifications_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
result = self._list_notification(tenant_id,
result = self._list_notification(req.project_id,
notification_method_id,
req.uri)
res.body = helpers.dumpit_utf8(result)
@ -240,8 +236,7 @@ class Notifications(notifications_api_v2.NotificationsV2API):
def on_delete(self, req, res, notification_method_id):
helpers.validate_authorization(req, self._default_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
self._delete_notification(tenant_id, notification_method_id)
self._delete_notification(req.project_id, notification_method_id)
res.status = falcon.HTTP_204
def on_put(self, req, res, notification_method_id):
@ -249,8 +244,7 @@ class Notifications(notifications_api_v2.NotificationsV2API):
helpers.validate_authorization(req, self._default_authorized_roles)
notification = helpers.read_http_resource(req)
self._parse_and_validate_notification(notification, require_all=True)
tenant_id = helpers.get_tenant_id(req)
result = self._update_notification(notification_method_id, tenant_id,
result = self._update_notification(notification_method_id, req.project_id,
notification, req.uri)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
@ -259,10 +253,9 @@ class Notifications(notifications_api_v2.NotificationsV2API):
helpers.validate_json_content_type(req)
helpers.validate_authorization(req, self._default_authorized_roles)
notification = helpers.read_http_resource(req)
tenant_id = helpers.get_tenant_id(req)
self._patch_get_notification(tenant_id, notification_method_id, notification)
self._patch_get_notification(req.project_id, notification_method_id, notification)
self._parse_and_validate_notification(notification, require_all=True)
result = self._update_notification(notification_method_id, tenant_id,
result = self._update_notification(notification_method_id, req.project_id,
notification, req.uri)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200

View File

@ -37,8 +37,7 @@ class NotificationsType(notificationstype_api_v2.NotificationsTypeV2API):
# This is to provide consistency. Pagination is not really supported here as there
# are not that many rows
limit = helpers.get_limit(req)
result = self._list_notifications(req.uri, limit)
result = self._list_notifications(req.uri, req.limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200

View File

@ -2,6 +2,7 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
oslo.config!=3.18.0,>=3.14.0 # Apache-2.0
oslo.context>=2.9.0 # Apache-2.0
oslo.log>=3.11.0 # Apache-2.0
oslo.middleware>=3.0.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0