Using oslo.policy for monasca-api

Added policies and used policy enforcement engine
from monasca-common.

- Replaced security with oslo.policy
- Updated unit tests and implemented some new tests
- Added a new entry point for generating sample policy file by tox

story: 2001233
task: 6355

Change-Id: I4aa444fe6ec883160c03c201145c77994b6615f9
Signed-off-by: Amir Mofakhar <amofakhar@op5.com>
This commit is contained in:
Amir Mofakhar 2018-02-22 12:42:37 +01:00
parent 2e078cdd87
commit 3ca4b057dd
33 changed files with 1058 additions and 201 deletions

View File

@ -5,3 +5,7 @@ config-generator
To generate sample configuration file execute:: To generate sample configuration file execute::
tox -e genconfig tox -e genconfig
To generate the sample policies execute::
tox -e genpolicy

View File

@ -6,3 +6,4 @@ summarize = True
namespace = monasca_api namespace = monasca_api
namespace = oslo.log namespace = oslo.log
namespace = oslo.db namespace = oslo.db
namespace = oslo.policy

View File

@ -0,0 +1,4 @@
[DEFAULT]
output_file = etc/api-policy.yaml.sample
format = yaml
namespace = monasca_api

View File

@ -1,4 +1,5 @@
# Copyright 2016 FUJITSU LIMITED # Copyright 2016 FUJITSU LIMITED
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -14,11 +15,16 @@
import falcon import falcon
from oslo_context import context from monasca_common.policy import policy_engine as policy
from monasca_api.api.core import request_context
from monasca_api.common.repositories import constants from monasca_api.common.repositories import constants
from monasca_api import policies
from monasca_api.v2.common import exceptions from monasca_api.v2.common import exceptions
policy.POLICIES = policies
_TENANT_ID_PARAM = 'tenant_id' _TENANT_ID_PARAM = 'tenant_id'
"""Name of the query-param pointing at project-id (tenant-id)""" """Name of the query-param pointing at project-id (tenant-id)"""
@ -33,7 +39,7 @@ class Request(falcon.Request):
def __init__(self, env, options=None): def __init__(self, env, options=None):
super(Request, self).__init__(env, options) super(Request, self).__init__(env, options)
self.context = context.RequestContext.from_environ(self.env) self.context = request_context.RequestContext.from_environ(self.env)
@property @property
def project_id(self): def project_id(self):
@ -105,5 +111,8 @@ class Request(falcon.Request):
else: else:
return constants.PAGE_LIMIT return constants.PAGE_LIMIT
def can(self, action, target=None):
return self.context.can(action, target)
def __repr__(self): def __repr__(self):
return '%s, context=%s' % (self.path, self.context) return '%s, context=%s' % (self.path, self.context)

View File

@ -0,0 +1,36 @@
# Copyright 2017 FUJITSU LIMITED
# Copyright 2018 OP5 AB
#
# 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 monasca_common.policy import policy_engine as policy
from oslo_context import context
from monasca_api import policies
policy.POLICIES = policies
class RequestContext(context.RequestContext):
"""RequestContext.
RequestContext is customized version of
:py:class:oslo_context.context.RequestContext.
"""
def can(self, action, target=None):
if target is None:
target = {'project_id': self.project_id,
'user_id': self.user_id}
return policy.authorize(self, action=action, target=target)

View File

@ -17,6 +17,10 @@
from oslo_config import cfg from oslo_config import cfg
security_opts = [ security_opts = [
cfg.ListOpt('healthcheck_roles', default=['@'],
help='Roles that are allowed to check the health'),
cfg.ListOpt('versions_roles', default=['@'],
help='Roles that are allowed to check the versions'),
cfg.ListOpt('default_authorized_roles', default=['monasca-user'], cfg.ListOpt('default_authorized_roles', default=['monasca-user'],
help=''' help='''
Roles that are allowed full access to the API Roles that are allowed full access to the API

View File

@ -1,4 +1,5 @@
# Copyright 2017 FUJITSU LIMITED # Copyright 2017 FUJITSU LIMITED
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -16,6 +17,7 @@ import sys
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from oslo_policy import opts as policy_opts
from monasca_api import conf from monasca_api import conf
from monasca_api import version from monasca_api import version
@ -57,6 +59,7 @@ def parse_args(argv=None, config_file=None):
product_name='monasca-api', product_name='monasca-api',
version=version.version_str) version=version.version_str)
conf.register_opts() conf.register_opts()
policy_opts.set_defaults(CONF)
_CONF_LOADED = True _CONF_LOADED = True

View File

@ -1,4 +1,5 @@
# Copyright 2017 FUJITSU LIMITED # Copyright 2017 FUJITSU LIMITED
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -39,6 +40,7 @@ class HealthChecks(healthcheck_api.HealthCheckApi):
res.cache_control = self.CACHE_CONTROL res.cache_control = self.CACHE_CONTROL
def on_get(self, req, res): def on_get(self, req, res):
helpers.validate_authorization(req, ['api:healthcheck'])
kafka_result = self._kafka_check.health_check() kafka_result = self._kafka_check.health_check()
alarms_db_result = self._alarm_db_check.health_check() alarms_db_result = self._alarm_db_check.health_check()
metrics_db_result = self._metrics_db_check.health_check() metrics_db_result = self._metrics_db_check.health_check()

View File

@ -0,0 +1,78 @@
# Copyright 2017 FUJITSU LIMITED
# Copyright 2018 OP5 AB
#
# 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 os
import pkgutil
from oslo_config import cfg
from oslo_log import log
from oslo_utils import importutils
from monasca_api.conf import security
LOG = log.getLogger(__name__)
_BASE_MOD_PATH = 'monasca_api.policies.'
CONF = cfg.CONF
def roles_list_to_check_str(roles_list):
converted_roles_list = ["role:" + role if role != '@' else role for role in roles_list]
return ' or '.join(converted_roles_list)
security.register_opts(CONF)
HEALTHCHECK_ROLES = roles_list_to_check_str(cfg.CONF.security.healthcheck_roles)
VERSIONS_ROLES = roles_list_to_check_str(cfg.CONF.security.versions_roles)
DEFAULT_AUTHORIZED_ROLES = roles_list_to_check_str(cfg.CONF.security.default_authorized_roles)
READ_ONLY_AUTHORIZED_ROLES = roles_list_to_check_str(cfg.CONF.security.read_only_authorized_roles)
AGENT_AUTHORIZED_ROLES = roles_list_to_check_str(cfg.CONF.security.agent_authorized_roles)
DELEGATE_AUTHORIZED_ROLES = roles_list_to_check_str(cfg.CONF.security.delegate_authorized_roles)
def load_policy_modules():
"""Load all modules that contain policies.
Method iterates over modules of :py:mod:`monasca_events_api.policies`
and imports only those that contain following methods:
- list_rules
"""
for modname in _list_module_names():
mod = importutils.import_module(_BASE_MOD_PATH + modname)
if hasattr(mod, 'list_rules'):
yield mod
def _list_module_names():
package_path = os.path.dirname(os.path.abspath(__file__))
for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]):
if not (modname == "opts" and ispkg):
yield modname
def list_rules():
"""List all policy modules rules.
Goes through all policy modules and yields their rules
"""
all_rules = []
for mod in load_policy_modules():
rules = mod.list_rules()
all_rules.extend(rules)
return all_rules

View File

@ -0,0 +1,158 @@
# Copyright 2018 OP5 AB
#
# 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 oslo_policy import policy
from monasca_api.policies import DEFAULT_AUTHORIZED_ROLES
from monasca_api.policies import READ_ONLY_AUTHORIZED_ROLES
rules = [
policy.DocumentedRuleDefault(
name='api:alarms:definition:post',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Post alarm definition role',
operations=[
{
'path': '/v2.0/alarm-definitions/',
'method': 'POST'
}
]
),
policy.DocumentedRuleDefault(
name='api:alarms:definition:get',
check_str=DEFAULT_AUTHORIZED_ROLES + ' or ' + READ_ONLY_AUTHORIZED_ROLES,
description='Get alarm definition role',
operations=[
{
'path': '/v2.0/alarm-definitions/{alarm_definition_id}',
'method': 'GET'
},
{
'path': '/v2.0/alarm-definitions',
'method': 'GET'
}
]
),
policy.DocumentedRuleDefault(
name='api:alarms:definition:put',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Put alarm definition role',
operations=[
{
'path': '/v2.0/alarm-definitions/{alarm_definition_id}',
'method': 'PUT'
},
]
),
policy.DocumentedRuleDefault(
name='api:alarms:definition:patch',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Patch alarm definition role',
operations=[
{
'path': '/v2.0/alarm-definitions/{alarm_definition_id}',
'method': 'PATCH'
},
]
),
policy.DocumentedRuleDefault(
name='api:alarms:definition:delete',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Delete alarm definition role',
operations=[
{
'path': '/v2.0/alarm-definitions/{alarm_definition_id}',
'method': 'DELETE'
},
]
),
policy.DocumentedRuleDefault(
name='api:alarms:put',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Put alarm role',
operations=[
{
'path': '/v2.0/alarms/{alarm_id}',
'method': 'PUT'
},
]
),
policy.DocumentedRuleDefault(
name='api:alarms:patch',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Patch alarm role',
operations=[
{
'path': '/v2.0/alarms/{alarm_id}',
'method': 'PATCH'
},
]
),
policy.DocumentedRuleDefault(
name='api:alarms:delete',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Delete alarm role',
operations=[
{
'path': '/v2.0/alarms/{alarm_id}',
'method': 'DELETE'
},
]
),
policy.DocumentedRuleDefault(
name='api:alarms:get',
check_str=DEFAULT_AUTHORIZED_ROLES + ' or ' + READ_ONLY_AUTHORIZED_ROLES,
description='Get alarm role',
operations=[
{
'path': '/v2.0/alarms/',
'method': 'GET'
},
{
'path': '/v2.0/alarms/{alarm_id}',
'method': 'GET'
},
]
),
policy.DocumentedRuleDefault(
name='api:alarms:count',
check_str=DEFAULT_AUTHORIZED_ROLES + ' or ' + READ_ONLY_AUTHORIZED_ROLES,
description='Count alarm role',
operations=[
{
'path': '/v2.0/alarms/count/',
'method': 'GET'
}
]
),
policy.DocumentedRuleDefault(
name='api:alarms:state_history',
check_str=DEFAULT_AUTHORIZED_ROLES + ' or ' + READ_ONLY_AUTHORIZED_ROLES,
description='Alarm state history role',
operations=[
{
'path': '/v2.0/alarms/state-history',
'method': 'GET'
},
{
'path': '/v2.0/alarms/{alarm_id}/state-history',
'method': 'GET'
}
]
)
]
def list_rules():
return rules

View File

@ -0,0 +1,31 @@
# Copyright 2018 OP5 AB
#
# 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 oslo_policy import policy
from monasca_api.policies import DELEGATE_AUTHORIZED_ROLES
rules = [
policy.RuleDefault(
name='api:delegate',
check_str=DELEGATE_AUTHORIZED_ROLES,
description='The rules that allowes to access the API on'
' behalf of another tenant role',
)
]
def list_rules():
return rules

View File

@ -0,0 +1,32 @@
# Copyright 2018 OP5 AB
#
# 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 oslo_policy import policy
from monasca_api.policies import HEALTHCHECK_ROLES
rules = [
policy.DocumentedRuleDefault(
name='api:healthcheck',
check_str=HEALTHCHECK_ROLES,
description='Healthcheck role',
operations=[
{'path': '/healthcheck', 'method': 'GET'}
]
),
]
def list_rules():
return rules

View File

@ -0,0 +1,62 @@
# Copyright 2018 OP5 AB
#
# 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 oslo_policy import policy
from monasca_api.policies import AGENT_AUTHORIZED_ROLES
from monasca_api.policies import DEFAULT_AUTHORIZED_ROLES
from monasca_api.policies import READ_ONLY_AUTHORIZED_ROLES
rules = [
policy.DocumentedRuleDefault(
name='api:metrics:get',
check_str=DEFAULT_AUTHORIZED_ROLES + ' or ' + READ_ONLY_AUTHORIZED_ROLES,
description='Get metrics role',
operations=[
{'path': '/v2.0/metrics', 'method': 'GET'},
{'path': '/v2.0/metrics/measurements', 'method': 'GET'},
{'path': '/v2.0/metrics/statistics', 'method': 'GET'},
{'path': '/v2.0/metrics/names', 'method': 'GET'}
]
),
policy.DocumentedRuleDefault(
name='api:metrics:post',
check_str=DEFAULT_AUTHORIZED_ROLES + ' or ' + AGENT_AUTHORIZED_ROLES,
description='Post metrics role',
operations=[
{'path': '/v2.0/metrics', 'method': 'POST'}
]
),
policy.DocumentedRuleDefault(
name='api:metrics:dimension:values',
check_str=DEFAULT_AUTHORIZED_ROLES + ' or ' + READ_ONLY_AUTHORIZED_ROLES,
description='Get metrics dimension values role',
operations=[
{'path': '/v2.0/metrics/dimensions/names/values', 'method': 'GET'}
]
),
policy.DocumentedRuleDefault(
name='api:metrics:dimension:names',
check_str=DEFAULT_AUTHORIZED_ROLES + ' or ' + READ_ONLY_AUTHORIZED_ROLES,
description='Get metrics dimension names role',
operations=[
{'path': '/v2.0/metrics/dimensions/names', 'method': 'GET'}
]
),
]
def list_rules():
return rules

View File

@ -0,0 +1,96 @@
# Copyright 2018 OP5 AB
#
# 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 oslo_policy import policy
from monasca_api.policies import DEFAULT_AUTHORIZED_ROLES
from monasca_api.policies import READ_ONLY_AUTHORIZED_ROLES
rules = [
policy.DocumentedRuleDefault(
name='api:notifications:put',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Put notifications role',
operations=[
{
'path': '/v2.0/notification-methods/{notification_method_id}',
'method': 'PUT'
},
]
),
policy.DocumentedRuleDefault(
name='api:notifications:patch',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Patch notifications role',
operations=[
{
'path': '/v2.0/notification-methods/{notification_method_id}',
'method': 'PATCH'
},
]
),
policy.DocumentedRuleDefault(
name='api:notifications:delete',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Delete notifications role',
operations=[
{
'path': '/v2.0/notification-methods/{notification_method_id}',
'method': 'DELETE'
},
]
),
policy.DocumentedRuleDefault(
name='api:notifications:get',
check_str=DEFAULT_AUTHORIZED_ROLES + ' or ' + READ_ONLY_AUTHORIZED_ROLES,
description='Get notifications role',
operations=[
{
'path': '/v2.0/notification-methods',
'method': 'GET'
},
{
'path': '/v2.0/notification-methods/{notification_method_id}',
'method': 'GET'
},
]
),
policy.DocumentedRuleDefault(
name='api:notifications:post',
check_str=DEFAULT_AUTHORIZED_ROLES,
description='Post notifications role',
operations=[
{
'path': '/v2.0/notification-methods',
'method': 'POST'
}
]
),
policy.DocumentedRuleDefault(
name='api:notifications:type',
check_str=DEFAULT_AUTHORIZED_ROLES + ' or ' + READ_ONLY_AUTHORIZED_ROLES,
description='Get notifications type role',
operations=[
{
'path': '/v2.0/notification-methods/types',
'method': 'GET'
}
]
)
]
def list_rules():
return rules

View File

@ -0,0 +1,33 @@
# Copyright 2018 OP5 AB
#
# 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 oslo_policy import policy
from monasca_api.policies import VERSIONS_ROLES
rules = [
policy.DocumentedRuleDefault(
name='api:versions',
check_str=VERSIONS_ROLES,
description='Get versions role',
operations=[
{'path': '/', 'method': 'GET'},
{'path': '/v2.0', 'method': 'GET'}
]
),
]
def list_rules():
return rules

View File

@ -1,5 +1,6 @@
# Copyright 2015 kornicameister@gmail.com # Copyright 2015 kornicameister@gmail.com
# Copyright 2015-2017 FUJITSU LIMITED # Copyright 2015-2017 FUJITSU LIMITED
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -12,17 +13,25 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
import falcon import falcon
from falcon import testing from falcon import testing
import fixtures
from monasca_common.policy import policy_engine as policy
from oslo_config import cfg from oslo_config import cfg
from oslo_config import fixture as oo_cfg from oslo_config import fixture as oo_cfg
from oslo_context import fixture as oo_ctx from oslo_context import fixture as oo_ctx
from oslo_serialization import jsonutils
from oslotest import base as oslotest_base from oslotest import base as oslotest_base
from monasca_api.api.core import request from monasca_api.api.core import request
from monasca_api import conf from monasca_api import conf
from monasca_api import config from monasca_api import config
from monasca_api import policies
policy.POLICIES = policies
class MockedAPI(falcon.API): class MockedAPI(falcon.API):
@ -70,6 +79,7 @@ class BaseTestCase(oslotest_base.BaseTestCase):
super(BaseTestCase, self).setUp() super(BaseTestCase, self).setUp()
self.useFixture(ConfigFixture()) self.useFixture(ConfigFixture())
self.useFixture(oo_ctx.ClearRequestContext()) self.useFixture(oo_ctx.ClearRequestContext())
self.useFixture(PolicyFixture())
@staticmethod @staticmethod
def conf_override(**kw): def conf_override(**kw):
@ -95,3 +105,37 @@ class BaseApiTestCase(BaseTestCase, testing.TestBase):
*args, *args,
**kwargs **kwargs
) )
class PolicyFixture(fixtures.Fixture):
"""Override the policy with a completely new policy file.
This overrides the policy with a completely fake and synthetic
policy file.
"""
def setUp(self):
super(PolicyFixture, self).setUp()
self._prepare_policy()
policy.reset()
policy.init()
def _prepare_policy(self):
policy_dir = self.useFixture(fixtures.TempDir())
policy_file = os.path.join(policy_dir.path, 'policy.yaml')
# load the fake_policy data and add the missing default rules.
policy_rules = jsonutils.loads('{}')
self.add_missing_default_rules(policy_rules)
with open(policy_file, 'w') as f:
jsonutils.dump(policy_rules, f)
BaseTestCase.conf_override(policy_file=policy_file,
group='oslo_policy')
BaseTestCase.conf_override(policy_dirs=[], group='oslo_policy')
@staticmethod
def add_missing_default_rules(rules):
for rule in policies.list_rules():
if rule.name not in rules:
rules[rule.name] = rule.check_str

View File

@ -192,7 +192,6 @@ class TestAlarmsStateHistory(AlarmTestBase):
'X-Roles': CONF.security.default_authorized_roles[0], 'X-Roles': CONF.security.default_authorized_roles[0],
'X-Tenant-Id': TENANT_ID, 'X-Tenant-Id': TENANT_ID,
}) })
self.assertEqual(self.srmock.status, falcon.HTTP_200) self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertThat(response, RESTResponseEquals(expected_elements)) self.assertThat(response, RESTResponseEquals(expected_elements))

View File

@ -0,0 +1,98 @@
# Copyright 2018 OP5 AB
#
# 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 falcon import testing
from monasca_common.policy import policy_engine as policy
from oslo_policy import policy as os_policy
from monasca_api.api.core import request
from monasca_api.tests import base
import monasca_api.v2.reference.helpers as helpers
class TestGetXTenantOrTenantId(base.BaseApiTestCase):
def setUp(self):
super(TestGetXTenantOrTenantId, self).setUp()
rules = [
os_policy.RuleDefault("example:allowed", "@"),
os_policy.RuleDefault("example:denied", "!"),
os_policy.RuleDefault("example:authorized",
"role:role_1 or role:role_2")
]
policy.reset()
policy.init()
policy._ENFORCER.register_defaults(rules)
def test_return_tenant_id_on_authorized_roles(self):
for role in ['role_1', 'role_2']:
req_context = self._get_request_context(role)
self.assertEqual(
'fake_tenant_id',
helpers.get_x_tenant_or_tenant_id(
req_context, ['example:authorized']
)
)
def test_return_tenant_id_on_allowed_rules(self):
req_context = self._get_request_context()
self.assertEqual(
'fake_tenant_id',
helpers.get_x_tenant_or_tenant_id(
req_context,
['example:allowed']
)
)
def test_return_project_id_on_unauthorized_role(self):
req_context = self._get_request_context()
self.assertEqual('fake_project_id',
helpers.get_x_tenant_or_tenant_id(
req_context,
['example:authorized']))
def test_return_project_id_on_denied_rules(self):
req_context = self._get_request_context()
self.assertEqual(
'fake_project_id',
helpers.get_x_tenant_or_tenant_id(
req_context,
['example:denied']
)
)
def test_return_project_id_on_unavailable_tenant_id(self):
req_context = self._get_request_context()
req_context.query_string = ''
self.assertEqual(
'fake_project_id',
helpers.get_x_tenant_or_tenant_id(
req_context,
['example:allowed']
)
)
@staticmethod
def _get_request_context(role='fake_role'):
return request.Request(
testing.create_environ(
path="/",
query_string="tenant_id=fake_tenant_id",
headers={
"X_PROJECT_ID": "fake_project_id",
"X_ROLES": role
}
)
)

View File

@ -0,0 +1,254 @@
# Copyright 2016-2017 FUJITSU LIMITED
# Copyright 2018 OP5 AB
#
# 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 falcon import testing
from monasca_common.policy import policy_engine as policy
from oslo_context import context
from oslo_policy import policy as os_policy
from monasca_api.api.core import request
from monasca_api.policies import roles_list_to_check_str
from monasca_api.tests import base
class TestPolicyFileCase(base.BaseTestCase):
def setUp(self):
super(TestPolicyFileCase, self).setUp()
self.context = context.RequestContext(user='fake',
tenant='fake',
roles=['fake'])
self.target = {'tenant_id': 'fake'}
def test_modified_policy_reloads(self):
tmp_file = \
self.create_tempfiles(files=[('policies', '{}')], ext='.yaml')[0]
base.BaseTestCase.conf_override(policy_file=tmp_file,
group='oslo_policy')
policy.reset()
policy.init()
action = 'example:test'
rule = os_policy.RuleDefault(action, '')
policy._ENFORCER.register_defaults([rule])
with open(tmp_file, 'w') as policy_file:
policy_file.write('{"example:test": ""}')
policy.authorize(self.context, action, self.target)
with open(tmp_file, 'w') as policy_file:
policy_file.write('{"example:test": "!"}')
policy._ENFORCER.load_rules(True)
self.assertRaises(os_policy.PolicyNotAuthorized, policy.authorize,
self.context, action, self.target)
class TestPolicyCase(base.BaseTestCase):
def setUp(self):
super(TestPolicyCase, self).setUp()
rules = [
os_policy.RuleDefault("true", "@"),
os_policy.RuleDefault("example:allowed", "@"),
os_policy.RuleDefault("example:denied", "!"),
os_policy.RuleDefault("example:lowercase_monasca_user",
"role:monasca_user or role:sysadmin"),
os_policy.RuleDefault("example:uppercase_monasca_user",
"role:MONASCA_USER or role:sysadmin"),
]
policy.reset()
policy.init()
policy._ENFORCER.register_defaults(rules)
def test_authorize_nonexist_action_throws(self):
action = "example:noexist"
ctx = request.Request(
testing.create_environ(
path="/",
headers={
"X_USER_ID": "fake",
"X_PROJECT_ID": "fake",
"X_ROLES": "member"
}
)
)
self.assertRaises(os_policy.PolicyNotRegistered, policy.authorize,
ctx.context, action, {})
def test_authorize_bad_action_throws(self):
action = "example:denied"
ctx = request.Request(
testing.create_environ(
path="/",
headers={
"X_USER_ID": "fake",
"X_PROJECT_ID": "fake",
"X_ROLES": "member"
}
)
)
self.assertRaises(os_policy.PolicyNotAuthorized, policy.authorize,
ctx.context, action, {})
def test_authorize_bad_action_no_exception(self):
action = "example:denied"
ctx = request.Request(
testing.create_environ(
path="/",
headers={
"X_USER_ID": "fake",
"X_PROJECT_ID": "fake",
"X_ROLES": "member"
}
)
)
result = policy.authorize(ctx.context, action, {}, False)
self.assertFalse(result)
def test_authorize_good_action(self):
action = "example:allowed"
ctx = request.Request(
testing.create_environ(
path="/",
headers={
"X_USER_ID": "fake",
"X_PROJECT_ID": "fake",
"X_ROLES": "member"
}
)
)
result = policy.authorize(ctx.context, action, {}, False)
self.assertTrue(result)
def test_ignore_case_role_check(self):
lowercase_action = "example:lowercase_monasca_user"
uppercase_action = "example:uppercase_monasca_user"
monasca_user_context = request.Request(
testing.create_environ(
path="/",
headers={
"X_USER_ID": "monasca_user",
"X_PROJECT_ID": "fake",
"X_ROLES": "MONASCA_user"
}
)
)
self.assertTrue(policy.authorize(monasca_user_context.context,
lowercase_action,
{}))
self.assertTrue(policy.authorize(monasca_user_context.context,
uppercase_action,
{}))
class RegisteredPoliciesTestCase(base.BaseTestCase):
def __init__(self, *args, **kwds):
super(RegisteredPoliciesTestCase, self).__init__(*args, **kwds)
self.agent_roles = ['agent']
self.readonly_roles = ['monasca-read-only-user']
self.default_roles = ['monasca-user']
self.delegate_roles = ['admin']
def test_alarms_policies_roles(self):
alarms_policies = {
'api:alarms:definition:post': self.default_roles,
'api:alarms:definition:get':
self.default_roles + self.readonly_roles,
'api:alarms:definition:put': self.default_roles,
'api:alarms:definition:patch': self.default_roles,
'api:alarms:definition:delete': self.default_roles,
'api:alarms:put': self.default_roles,
'api:alarms:patch': self.default_roles,
'api:alarms:delete': self.default_roles,
'api:alarms:get': self.default_roles + self.readonly_roles,
'api:alarms:count': self.default_roles + self.readonly_roles,
'api:alarms:state_history': self.default_roles + self.readonly_roles
}
self._assert_rules(alarms_policies)
def test_metrics_policies_roles(self):
metrics_policies = {
'api:metrics:get': self.default_roles + self.readonly_roles,
'api:metrics:post': self.agent_roles + self.default_roles,
'api:metrics:dimension:values':
self.default_roles + self.readonly_roles,
'api:metrics:dimension:names':
self.default_roles + self.readonly_roles
}
self._assert_rules(metrics_policies)
def test_notifications_policies_roles(self):
notifications_policies = {
'api:notifications:put': self.default_roles,
'api:notifications:patch': self.default_roles,
'api:notifications:delete': self.default_roles,
'api:notifications:get': self.default_roles + self.readonly_roles,
'api:notifications:post': self.default_roles,
'api:notifications:type': self.default_roles + self.readonly_roles,
}
self._assert_rules(notifications_policies)
def test_versions_policies_roles(self):
versions_policies = {
'api:versions': ['any_rule!']
}
self._assert_rules(versions_policies)
def test_healthcheck_policies_roles(self):
healthcheck_policies = {
'api:healthcheck': ['any_rule!']
}
self._assert_rules(healthcheck_policies)
def test_delegate_policies_roles(self):
delegate_policies = {
'api:delegate': self.delegate_roles
}
self._assert_rules(delegate_policies)
def _assert_rules(self, policies_list):
for policy_name in policies_list:
registered_rule = policy.get_rules()[policy_name]
if hasattr(registered_rule, 'rules'):
self.assertEqual(len(registered_rule.rules),
len(policies_list[policy_name]))
for role in policies_list[policy_name]:
ctx = self._get_request_context(role)
self.assertTrue(policy.authorize(ctx.context,
policy_name,
{})
)
@staticmethod
def _get_request_context(role):
return request.Request(
testing.create_environ(
path='/',
headers={'X_ROLES': role}
)
)
class PolicyUtilsTestCase(base.BaseTestCase):
def test_roles_list_to_check_str(self):
self.assertEqual(roles_list_to_check_str(['test_role']), 'role:test_role')
self.assertEqual(roles_list_to_check_str(['role1', 'role2', 'role3']),
'role:role1 or role:role2 or role:role3')
self.assertEqual(roles_list_to_check_str(['@']), '@')
self.assertEqual(roles_list_to_check_str(['role1', '@', 'role2']),
'role:role1 or @ or role:role2')

View File

@ -1,4 +1,5 @@
# Copyright 2016-2017 FUJITSU LIMITED # Copyright 2016-2017 FUJITSU LIMITED
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -20,6 +21,8 @@ from monasca_api.v2.common import exceptions
class TestRequest(base.BaseApiTestCase): class TestRequest(base.BaseApiTestCase):
def setUp(self):
super(TestRequest, self).setUp()
def test_use_context_from_request(self): def test_use_context_from_request(self):
req = request.Request( req = request.Request(

View File

@ -26,6 +26,11 @@ import monasca_api.v2.common.validation as validation
import monasca_api.v2.reference.helpers as helpers import monasca_api.v2.reference.helpers as helpers
def mock_req_can(authorised_rule):
if authorised_rule != 'authorized':
raise Exception
class TestStateValidation(base.BaseTestCase): class TestStateValidation(base.BaseTestCase):
VALID_STATES = "OK", "ALARM", "UNDETERMINED" VALID_STATES = "OK", "ALARM", "UNDETERMINED"
@ -71,49 +76,20 @@ class TestSeverityValidation(base.BaseTestCase):
'|'.join([self.VALID_SEVERITIES[0], 'BOGUS'])) '|'.join([self.VALID_SEVERITIES[0], 'BOGUS']))
class TestRoleValidation(base.BaseTestCase): class TestRuleValidation(base.BaseApiTestCase):
def test_rule_valid(self):
def test_role_valid(self):
req_roles = 'role0', 'rOlE1'
authorized_roles = ['RolE1', 'Role2']
req = mock.Mock() req = mock.Mock()
req.roles = req_roles req.can = mock_req_can
test_rules = ['Rule1', 'authorized']
helpers.validate_authorization(req, authorized_roles) helpers.validate_authorization(req, test_rules)
def test_role_invalid(self):
req_roles = 'role2', 'role3'
authorized_roles = ['role0', 'role1']
def test_rule_invalid(self):
req = mock.Mock() req = mock.Mock()
req.roles = req_roles req.can = mock_req_can
test_rules = ['rule1', 'rule2']
self.assertRaises( self.assertRaises(
falcon.HTTPUnauthorized, falcon.HTTPUnauthorized,
helpers.validate_authorization, req, authorized_roles) helpers.validate_authorization, req, test_rules)
def test_empty_role_header(self):
req_roles = []
authorized_roles = ['Role1', 'Role2']
req = mock.Mock()
req.roles = req_roles
self.assertRaises(
falcon.HTTPUnauthorized,
helpers.validate_authorization, req, authorized_roles)
def test_no_role_header(self):
req_roles = None
authorized_roles = ['Role1', 'Role2']
req = mock.Mock()
req.roles = req_roles
self.assertRaises(
falcon.HTTPUnauthorized,
helpers.validate_authorization, req, authorized_roles)
class TestTimestampsValidation(base.BaseTestCase): class TestTimestampsValidation(base.BaseTestCase):

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2017 Hewlett Packard Enterprise Development LP # (C) Copyright 2014-2017 Hewlett Packard Enterprise Development LP
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -44,11 +45,6 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
try: try:
super(AlarmDefinitions, self).__init__() super(AlarmDefinitions, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._default_authorized_roles = (
cfg.CONF.security.default_authorized_roles)
self._get_alarmdefs_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._alarm_definitions_repo = simport.load( self._alarm_definitions_repo = simport.load(
cfg.CONF.repositories.alarm_definitions_driver)() cfg.CONF.repositories.alarm_definitions_driver)()
@ -58,7 +54,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_post(self, req, res): def on_post(self, req, res):
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:alarms:definition:post'])
alarm_definition = helpers.from_json(req) alarm_definition = helpers.from_json(req)
@ -87,8 +83,8 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res, alarm_definition_id=None): def on_get(self, req, res, alarm_definition_id=None):
helpers.validate_authorization(req, ['api:alarms:definition:get'])
if alarm_definition_id is None: if alarm_definition_id is None:
helpers.validate_authorization(req, self._get_alarmdefs_authorized_roles)
name = helpers.get_query_name(req) name = helpers.get_query_name(req)
dimensions = helpers.get_query_dimensions(req) dimensions = helpers.get_query_dimensions(req)
severity = helpers.get_query_param(req, "severity", default_val=None) severity = helpers.get_query_param(req, "severity", default_val=None)
@ -118,20 +114,16 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
req.uri, sort_by, req.uri, sort_by,
offset, req.limit) offset, req.limit)
res.body = helpers.to_json(result)
res.status = falcon.HTTP_200
else: else:
helpers.validate_authorization(req, self._get_alarmdefs_authorized_roles)
result = self._alarm_definition_show(req.project_id, result = self._alarm_definition_show(req.project_id,
alarm_definition_id) alarm_definition_id)
helpers.add_links_to_resource(result, helpers.add_links_to_resource(result,
re.sub('/' + alarm_definition_id, '', re.sub('/' + alarm_definition_id, '',
req.uri)) req.uri))
res.body = helpers.to_json(result)
res.status = falcon.HTTP_200 res.body = helpers.to_json(result)
res.status = falcon.HTTP_200
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_put(self, req, res, alarm_definition_id=None): def on_put(self, req, res, alarm_definition_id=None):
@ -139,7 +131,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
if not alarm_definition_id: if not alarm_definition_id:
raise HTTPBadRequestError('Bad Request', 'Alarm definition ID not provided') raise HTTPBadRequestError('Bad Request', 'Alarm definition ID not provided')
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:alarms:definition:put'])
alarm_definition = helpers.from_json(req) alarm_definition = helpers.from_json(req)
@ -181,7 +173,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
if not alarm_definition_id: if not alarm_definition_id:
raise HTTPBadRequestError('Bad Request', 'Alarm definition ID not provided') raise HTTPBadRequestError('Bad Request', 'Alarm definition ID not provided')
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:alarms:definition:patch'])
alarm_definition = helpers.from_json(req) alarm_definition = helpers.from_json(req)
@ -230,7 +222,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API,
if not alarm_definition_id: if not alarm_definition_id:
raise HTTPBadRequestError('Bad Request', 'Alarm definition ID not provided') raise HTTPBadRequestError('Bad Request', 'Alarm definition ID not provided')
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:alarms:definition:delete'])
self._alarm_definition_delete(req.project_id, alarm_definition_id) self._alarm_definition_delete(req.project_id, alarm_definition_id)
res.status = falcon.HTTP_204 res.status = falcon.HTTP_204

View File

@ -1,4 +1,5 @@
# Copyright 2014-2017 Hewlett Packard Enterprise Development LP # Copyright 2014-2017 Hewlett Packard Enterprise Development LP
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -38,11 +39,6 @@ class Alarms(alarms_api_v2.AlarmsV2API,
try: try:
super(Alarms, self).__init__() super(Alarms, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._default_authorized_roles = (
cfg.CONF.security.default_authorized_roles)
self._get_alarms_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._alarms_repo = simport.load( self._alarms_repo = simport.load(
cfg.CONF.repositories.alarms_driver)() cfg.CONF.repositories.alarms_driver)()
@ -53,7 +49,7 @@ class Alarms(alarms_api_v2.AlarmsV2API,
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_put(self, req, res, alarm_id): def on_put(self, req, res, alarm_id):
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:alarms:put'])
alarm = helpers.from_json(req) alarm = helpers.from_json(req)
schema_alarm.validate(alarm) schema_alarm.validate(alarm)
@ -80,7 +76,7 @@ class Alarms(alarms_api_v2.AlarmsV2API,
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_patch(self, req, res, alarm_id): def on_patch(self, req, res, alarm_id):
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:alarms:patch'])
alarm = helpers.from_json(req) alarm = helpers.from_json(req)
schema_alarm.validate(alarm) schema_alarm.validate(alarm)
@ -106,7 +102,7 @@ class Alarms(alarms_api_v2.AlarmsV2API,
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_delete(self, req, res, alarm_id): def on_delete(self, req, res, alarm_id):
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:alarms:delete'])
self._alarm_delete(req.project_id, alarm_id) self._alarm_delete(req.project_id, alarm_id)
@ -114,7 +110,7 @@ class Alarms(alarms_api_v2.AlarmsV2API,
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res, alarm_id=None): def on_get(self, req, res, alarm_id=None):
helpers.validate_authorization(req, self._get_alarms_authorized_roles) helpers.validate_authorization(req, ['api:alarms:get'])
if alarm_id is None: if alarm_id is None:
query_parms = falcon.uri.parse_query_string(req.query_string) query_parms = falcon.uri.parse_query_string(req.query_string)
@ -359,9 +355,6 @@ class AlarmsCount(alarms_api_v2.AlarmsCountV2API, alarming.Alarming):
try: try:
super(AlarmsCount, self).__init__() super(AlarmsCount, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._get_alarms_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._alarms_repo = simport.load( self._alarms_repo = simport.load(
cfg.CONF.repositories.alarms_driver)() cfg.CONF.repositories.alarms_driver)()
@ -371,7 +364,7 @@ class AlarmsCount(alarms_api_v2.AlarmsCountV2API, alarming.Alarming):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res): def on_get(self, req, res):
helpers.validate_authorization(req, self._get_alarms_authorized_roles) helpers.validate_authorization(req, ['api:alarms:count'])
query_parms = falcon.uri.parse_query_string(req.query_string) query_parms = falcon.uri.parse_query_string(req.query_string)
if 'state' in query_parms: if 'state' in query_parms:
@ -464,9 +457,6 @@ class AlarmsStateHistory(alarms_api_v2.AlarmsStateHistoryV2API,
try: try:
super(AlarmsStateHistory, self).__init__() super(AlarmsStateHistory, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._get_alarms_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._alarms_repo = simport.load( self._alarms_repo = simport.load(
cfg.CONF.repositories.alarms_driver)() cfg.CONF.repositories.alarms_driver)()
self._metrics_repo = simport.load( self._metrics_repo = simport.load(
@ -478,12 +468,13 @@ class AlarmsStateHistory(alarms_api_v2.AlarmsStateHistoryV2API,
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res, alarm_id=None): def on_get(self, req, res, alarm_id=None):
helpers.validate_authorization(req, ['api:alarms:state_history'])
offset = helpers.get_query_param(req, 'offset')
if alarm_id is None: if alarm_id is None:
helpers.validate_authorization(req, self._get_alarms_authorized_roles)
start_timestamp = helpers.get_query_starttime_timestamp(req, False) start_timestamp = helpers.get_query_starttime_timestamp(req, False)
end_timestamp = helpers.get_query_endtime_timestamp(req, False) end_timestamp = helpers.get_query_endtime_timestamp(req, False)
offset = helpers.get_query_param(req, 'offset')
dimensions = helpers.get_query_dimensions(req) dimensions = helpers.get_query_dimensions(req)
helpers.validate_query_dimensions(dimensions) helpers.validate_query_dimensions(dimensions)
@ -491,19 +482,13 @@ class AlarmsStateHistory(alarms_api_v2.AlarmsStateHistoryV2API,
end_timestamp, dimensions, end_timestamp, dimensions,
req.uri, offset, req.limit) req.uri, offset, req.limit)
res.body = helpers.to_json(result)
res.status = falcon.HTTP_200
else: else:
helpers.validate_authorization(req, self._get_alarms_authorized_roles)
offset = helpers.get_query_param(req, 'offset')
result = self._alarm_history(req.project_id, alarm_id, result = self._alarm_history(req.project_id, alarm_id,
req.uri, offset, req.uri, offset,
req.limit) req.limit)
res.body = helpers.to_json(result) res.body = helpers.to_json(result)
res.status = falcon.HTTP_200 res.status = falcon.HTTP_200
def _alarm_history_list(self, tenant_id, start_timestamp, def _alarm_history_list(self, tenant_id, start_timestamp,
end_timestamp, dimensions, req_uri, offset, end_timestamp, dimensions, req_uri, offset,

View File

@ -1,6 +1,7 @@
# Copyright 2015 Cray Inc. All Rights Reserved. # Copyright 2015 Cray Inc. All Rights Reserved.
# (C) Copyright 2014,2016-2017 Hewlett Packard Enterprise Development LP # (C) Copyright 2014,2016-2017 Hewlett Packard Enterprise Development LP
# (C) Copyright 2017 SUSE LLC # (C) Copyright 2017 SUSE LLC
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -68,53 +69,41 @@ def validate_json_content_type(req):
'application/json') 'application/json')
def validate_authorization(req, authorized_roles): def validate_authorization(http_request, authorized_rules_list):
"""Validates whether one or more X-ROLES in the HTTP header is authorized. """Validates whether is authorized according to provided policy rules list.
If authorization fails, 401 is thrown with appropriate description. If authorization fails, 401 is thrown with appropriate description.
Additionally response specifies 'WWW-Authenticate' header with 'Token' Additionally response specifies 'WWW-Authenticate' header with 'Token'
value challenging the client to use different token (the one with value challenging the client to use different token (the one with
different set of roles). different set of roles which can access the service).
:param req: HTTP request object. Must contain "X-ROLES" in the HTTP
request header.
:param authorized_roles: List of authorized roles to check against.
:raises falcon.HTTPUnauthorized
""" """
roles = req.roles
challenge = 'Token' challenge = 'Token'
if not roles: for rule in authorized_rules_list:
raise falcon.HTTPUnauthorized('Forbidden', try:
'Tenant does not have any roles', http_request.can(rule)
challenge)
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 return
except Exception as ex:
LOG.debug(ex)
raise falcon.HTTPUnauthorized('Forbidden', raise falcon.HTTPUnauthorized('Forbidden',
'Tenant ID is missing a required role to ' 'The request does not have access to this service',
'access this service',
challenge) challenge)
def get_x_tenant_or_tenant_id(req, delegate_authorized_roles): def get_x_tenant_or_tenant_id(http_request, delegate_authorized_rules_list):
"""Evaluates whether the tenant ID or cross tenant ID should be returned. params = falcon.uri.parse_query_string(http_request.query_string)
if 'tenant_id' in params:
tenant_id = params['tenant_id']
:param req: HTTP request object. for rule in delegate_authorized_rules_list:
:param delegate_authorized_roles: List of authorized roles that have try:
delegate privileges. http_request.can(rule)
return tenant_id
except Exception as ex:
LOG.debug(ex)
:returns: Returns the cross tenant or tenant ID. return http_request.project_id
"""
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 req.project_id
def get_query_param(req, param_name, required=False, default_val=None): def get_query_param(req, param_name, required=False, default_val=None):

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2017 Hewlett Packard Enterprise Development LP # (C) Copyright 2014-2017 Hewlett Packard Enterprise Development LP
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -52,14 +53,6 @@ class Metrics(metrics_api_v2.MetricsV2API):
try: try:
super(Metrics, self).__init__() super(Metrics, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._delegate_authorized_roles = (
cfg.CONF.security.delegate_authorized_roles)
self._get_metrics_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._post_metrics_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.agent_authorized_roles)
self._message_queue = simport.load(cfg.CONF.messaging.driver)( self._message_queue = simport.load(cfg.CONF.messaging.driver)(
'metrics') 'metrics')
self._metrics_repo = simport.load( self._metrics_repo = simport.load(
@ -94,8 +87,7 @@ class Metrics(metrics_api_v2.MetricsV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_post(self, req, res): def on_post(self, req, res):
helpers.validate_json_content_type(req) helpers.validate_json_content_type(req)
helpers.validate_authorization(req, helpers.validate_authorization(req, ['api:metrics:post'])
self._post_metrics_authorized_roles)
metrics = helpers.from_json(req) metrics = helpers.from_json(req)
try: try:
metric_validation.validate(metrics) metric_validation.validate(metrics)
@ -103,9 +95,7 @@ class Metrics(metrics_api_v2.MetricsV2API):
LOG.exception(ex) LOG.exception(ex)
raise HTTPUnprocessableEntityError("Unprocessable Entity", str(ex)) raise HTTPUnprocessableEntityError("Unprocessable Entity", str(ex))
tenant_id = ( tenant_id = helpers.get_x_tenant_or_tenant_id(req, ['api:delegate'])
helpers.get_x_tenant_or_tenant_id(req,
self._delegate_authorized_roles))
transformed_metrics = metrics_message.transform( transformed_metrics = metrics_message.transform(
metrics, tenant_id, self._region) metrics, tenant_id, self._region)
self._send_metrics(transformed_metrics) self._send_metrics(transformed_metrics)
@ -113,10 +103,8 @@ class Metrics(metrics_api_v2.MetricsV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res): def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles) helpers.validate_authorization(req, ['api:metrics:get'])
tenant_id = ( tenant_id = helpers.get_x_tenant_or_tenant_id(req, ['api:delegate'])
helpers.get_x_tenant_or_tenant_id(req,
self._delegate_authorized_roles))
name = helpers.get_query_name(req) name = helpers.get_query_name(req)
helpers.validate_query_name(name) helpers.validate_query_name(name)
dimensions = helpers.get_query_dimensions(req) dimensions = helpers.get_query_dimensions(req)
@ -138,14 +126,6 @@ class MetricsMeasurements(metrics_api_v2.MetricsMeasurementsV2API):
try: try:
super(MetricsMeasurements, self).__init__() super(MetricsMeasurements, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._delegate_authorized_roles = (
cfg.CONF.security.delegate_authorized_roles)
self._get_metrics_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._post_metrics_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.agent_authorized_roles)
self._metrics_repo = simport.load( self._metrics_repo = simport.load(
cfg.CONF.repositories.metrics_driver)() cfg.CONF.repositories.metrics_driver)()
@ -156,10 +136,8 @@ class MetricsMeasurements(metrics_api_v2.MetricsMeasurementsV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res): def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles) helpers.validate_authorization(req, ['api:metrics:get'])
tenant_id = ( tenant_id = helpers.get_x_tenant_or_tenant_id(req, ['api:delegate'])
helpers.get_x_tenant_or_tenant_id(req,
self._delegate_authorized_roles))
name = helpers.get_query_name(req, True) name = helpers.get_query_name(req, True)
helpers.validate_query_name(name) helpers.validate_query_name(name)
dimensions = helpers.get_query_dimensions(req) dimensions = helpers.get_query_dimensions(req)
@ -203,11 +181,6 @@ class MetricsStatistics(metrics_api_v2.MetricsStatisticsV2API):
try: try:
super(MetricsStatistics, self).__init__() super(MetricsStatistics, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._delegate_authorized_roles = (
cfg.CONF.security.delegate_authorized_roles)
self._get_metrics_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._metrics_repo = simport.load( self._metrics_repo = simport.load(
cfg.CONF.repositories.metrics_driver)() cfg.CONF.repositories.metrics_driver)()
@ -218,10 +191,8 @@ class MetricsStatistics(metrics_api_v2.MetricsStatisticsV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res): def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles) helpers.validate_authorization(req, ['api:metrics:get'])
tenant_id = ( tenant_id = helpers.get_x_tenant_or_tenant_id(req, ['api:delegate'])
helpers.get_x_tenant_or_tenant_id(req,
self._delegate_authorized_roles))
name = helpers.get_query_name(req, True) name = helpers.get_query_name(req, True)
helpers.validate_query_name(name) helpers.validate_query_name(name)
dimensions = helpers.get_query_dimensions(req) dimensions = helpers.get_query_dimensions(req)
@ -268,11 +239,6 @@ class MetricsNames(metrics_api_v2.MetricsNamesV2API):
try: try:
super(MetricsNames, self).__init__() super(MetricsNames, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._delegate_authorized_roles = (
cfg.CONF.security.delegate_authorized_roles)
self._get_metrics_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._metrics_repo = simport.load( self._metrics_repo = simport.load(
cfg.CONF.repositories.metrics_driver)() cfg.CONF.repositories.metrics_driver)()
@ -283,10 +249,8 @@ class MetricsNames(metrics_api_v2.MetricsNamesV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res): def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles) helpers.validate_authorization(req, ['api:metrics:get'])
tenant_id = ( tenant_id = helpers.get_x_tenant_or_tenant_id(req, ['api:delegate'])
helpers.get_x_tenant_or_tenant_id(req,
self._delegate_authorized_roles))
dimensions = helpers.get_query_dimensions(req) dimensions = helpers.get_query_dimensions(req)
helpers.validate_query_dimensions(dimensions) helpers.validate_query_dimensions(dimensions)
offset = helpers.get_query_param(req, 'offset') offset = helpers.get_query_param(req, 'offset')
@ -310,11 +274,6 @@ class DimensionValues(metrics_api_v2.DimensionValuesV2API):
try: try:
super(DimensionValues, self).__init__() super(DimensionValues, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._delegate_authorized_roles = (
cfg.CONF.security.delegate_authorized_roles)
self._get_metrics_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._metrics_repo = simport.load( self._metrics_repo = simport.load(
cfg.CONF.repositories.metrics_driver)() cfg.CONF.repositories.metrics_driver)()
@ -324,10 +283,8 @@ class DimensionValues(metrics_api_v2.DimensionValuesV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res): def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles) helpers.validate_authorization(req, ['api:metrics:dimension:values'])
tenant_id = ( tenant_id = helpers.get_x_tenant_or_tenant_id(req, ['api:delegate'])
helpers.get_x_tenant_or_tenant_id(req,
self._delegate_authorized_roles))
metric_name = helpers.get_query_param(req, 'metric_name') metric_name = helpers.get_query_param(req, 'metric_name')
dimension_name = helpers.get_query_param(req, 'dimension_name', dimension_name = helpers.get_query_param(req, 'dimension_name',
required=True) required=True)
@ -353,11 +310,6 @@ class DimensionNames(metrics_api_v2.DimensionNamesV2API):
try: try:
super(DimensionNames, self).__init__() super(DimensionNames, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._delegate_authorized_roles = (
cfg.CONF.security.delegate_authorized_roles)
self._get_metrics_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._metrics_repo = simport.load( self._metrics_repo = simport.load(
cfg.CONF.repositories.metrics_driver)() cfg.CONF.repositories.metrics_driver)()
@ -368,10 +320,8 @@ class DimensionNames(metrics_api_v2.DimensionNamesV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res): def on_get(self, req, res):
helpers.validate_authorization(req, self._get_metrics_authorized_roles) helpers.validate_authorization(req, ['api:metrics:dimension:names'])
tenant_id = ( tenant_id = helpers.get_x_tenant_or_tenant_id(req, ['api:delegate'])
helpers.get_x_tenant_or_tenant_id(req,
self._delegate_authorized_roles))
metric_name = helpers.get_query_param(req, 'metric_name') metric_name = helpers.get_query_param(req, 'metric_name')
offset = helpers.get_query_param(req, 'offset') offset = helpers.get_query_param(req, 'offset')
result = self._dimension_names(tenant_id, req.uri, metric_name, result = self._dimension_names(tenant_id, req.uri, metric_name,

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2017 Hewlett Packard Enterprise Development LP # (C) Copyright 2014-2017 Hewlett Packard Enterprise Development LP
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -37,11 +38,6 @@ class Notifications(notifications_api_v2.NotificationsV2API):
super(Notifications, self).__init__() super(Notifications, self).__init__()
self._region = cfg.CONF.region self._region = cfg.CONF.region
self._default_authorized_roles = (
cfg.CONF.security.default_authorized_roles)
self._get_notifications_authorized_roles = (
cfg.CONF.security.default_authorized_roles +
cfg.CONF.security.read_only_authorized_roles)
self._notifications_repo = simport.load( self._notifications_repo = simport.load(
cfg.CONF.repositories.notifications_driver)() cfg.CONF.repositories.notifications_driver)()
self._notification_method_type_repo = simport.load( self._notification_method_type_repo = simport.load(
@ -205,7 +201,7 @@ class Notifications(notifications_api_v2.NotificationsV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_post(self, req, res): def on_post(self, req, res):
helpers.validate_json_content_type(req) helpers.validate_json_content_type(req)
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:notifications:post'])
notification = helpers.from_json(req) notification = helpers.from_json(req)
self._parse_and_validate_notification(notification) self._parse_and_validate_notification(notification)
result = self._create_notification(req.project_id, notification, req.uri) result = self._create_notification(req.project_id, notification, req.uri)
@ -214,9 +210,8 @@ class Notifications(notifications_api_v2.NotificationsV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res, notification_method_id=None): def on_get(self, req, res, notification_method_id=None):
helpers.validate_authorization(req, ['api:notifications:get'])
if notification_method_id is None: if notification_method_id is None:
helpers.validate_authorization(req,
self._get_notifications_authorized_roles)
sort_by = helpers.get_query_param(req, 'sort_by', default_val=None) sort_by = helpers.get_query_param(req, 'sort_by', default_val=None)
if sort_by is not None: if sort_by is not None:
if isinstance(sort_by, six.string_types): if isinstance(sort_by, six.string_types):
@ -238,27 +233,26 @@ class Notifications(notifications_api_v2.NotificationsV2API):
result = self._list_notifications(req.project_id, req.uri, sort_by, result = self._list_notifications(req.project_id, req.uri, sort_by,
offset, req.limit) offset, req.limit)
res.body = helpers.to_json(result)
res.status = falcon.HTTP_200
else: else:
helpers.validate_authorization(req,
self._get_notifications_authorized_roles)
result = self._list_notification(req.project_id, result = self._list_notification(req.project_id,
notification_method_id, notification_method_id,
req.uri) req.uri)
res.body = helpers.to_json(result)
res.status = falcon.HTTP_200 res.body = helpers.to_json(result)
res.status = falcon.HTTP_200
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_delete(self, req, res, notification_method_id): def on_delete(self, req, res, notification_method_id):
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:notifications:delete'])
self._delete_notification(req.project_id, notification_method_id) self._delete_notification(req.project_id, notification_method_id)
res.status = falcon.HTTP_204 res.status = falcon.HTTP_204
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_put(self, req, res, notification_method_id): def on_put(self, req, res, notification_method_id):
helpers.validate_json_content_type(req) helpers.validate_json_content_type(req)
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:notifications:put'])
notification = helpers.from_json(req) notification = helpers.from_json(req)
self._parse_and_validate_notification(notification, require_all=True) self._parse_and_validate_notification(notification, require_all=True)
result = self._update_notification(notification_method_id, req.project_id, result = self._update_notification(notification_method_id, req.project_id,
@ -269,7 +263,7 @@ class Notifications(notifications_api_v2.NotificationsV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_patch(self, req, res, notification_method_id): def on_patch(self, req, res, notification_method_id):
helpers.validate_json_content_type(req) helpers.validate_json_content_type(req)
helpers.validate_authorization(req, self._default_authorized_roles) helpers.validate_authorization(req, ['api:notifications:patch'])
notification = helpers.from_json(req) notification = helpers.from_json(req)
self._patch_get_notification(req.project_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) self._parse_and_validate_notification(notification, require_all=True)

View File

@ -1,4 +1,5 @@
# (C) Copyright 2016-2017 Hewlett Packard Enterprise Development LP # (C) Copyright 2016-2017 Hewlett Packard Enterprise Development LP
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -34,7 +35,7 @@ class NotificationsType(notificationstype_api_v2.NotificationsTypeV2API):
@resource.resource_try_catch_block @resource.resource_try_catch_block
def on_get(self, req, res): def on_get(self, req, res):
helpers.validate_authorization(req, ['api:notifications:type'])
# This is to provide consistency. Pagination is not really supported here as there # This is to provide consistency. Pagination is not really supported here as there
# are not that many rows # are not that many rows
result = self._list_notifications(req.uri, req.limit) result = self._list_notifications(req.uri, req.limit)

View File

@ -1,4 +1,5 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. # Copyright 2016 Hewlett Packard Enterprise Development Company, L.P.
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -20,6 +21,8 @@ class Version2(object):
super(Version2, self).__init__() super(Version2, self).__init__()
def on_get(self, req, res): def on_get(self, req, res):
helpers.validate_authorization(req,
['api:versions'])
result = { result = {
'id': 'v2.0', 'id': 'v2.0',
'links': [{ 'links': [{

View File

@ -1,4 +1,5 @@
# Copyright 2014 Hewlett-Packard # Copyright 2014 Hewlett-Packard
# Copyright 2018 OP5 AB
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -38,6 +39,8 @@ class Versions(versions_api.VersionsAPI):
def on_get(self, req, res, version_id=None): def on_get(self, req, res, version_id=None):
req_uri = req.uri.decode('utf8') if six.PY2 else req.uri req_uri = req.uri.decode('utf8') if six.PY2 else req.uri
helpers.validate_authorization(req,
['api:versions'])
result = { result = {
'links': [{ 'links': [{
'rel': 'self', 'rel': 'self',

View File

@ -0,0 +1,5 @@
---
features:
- |
Use of oslo mechanisms for defining and enforcing policy.
A command line entry point that allow the user to generate a sample policy file.

View File

@ -6,6 +6,7 @@ oslo.config>=5.2.0 # Apache-2.0
oslo.context>=2.19.2 # Apache-2.0 oslo.context>=2.19.2 # Apache-2.0
oslo.log>=3.36.0 # Apache-2.0 oslo.log>=3.36.0 # Apache-2.0
oslo.middleware>=3.31.0 # Apache-2.0 oslo.middleware>=3.31.0 # Apache-2.0
oslo.policy>=1.30.0 # Apache-2.0
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0

View File

@ -39,6 +39,9 @@ console_scripts =
oslo.config.opts = oslo.config.opts =
monasca_api = monasca_api.conf:list_opts monasca_api = monasca_api.conf:list_opts
oslo.policy.policies =
monasca_api = monasca_api.policies:list_rules
[build_sphinx] [build_sphinx]
all_files = 1 all_files = 1
build-dir = doc/build build-dir = doc/build

View File

@ -117,6 +117,10 @@ commands =
description = Generates sample configuration file for monasca-api description = Generates sample configuration file for monasca-api
commands = oslo-config-generator --config-file=config-generator/api-config.conf commands = oslo-config-generator --config-file=config-generator/api-config.conf
[testenv:genpolicy]
description = Generates sample policy.json file for monasca-api
commands = oslopolicy-sample-generator --config-file=config-generator/policy.conf
[testenv:venv] [testenv:venv]
commands = {posargs} commands = {posargs}