Using oslo.policy for monasca-log-api

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

- Updated role_middleware to remove authorization into the routes.
- Updated unit tests and implemented some new tests.
- Added a new entry point for generating sample policy file by tox.

story: 2001233
task: 22086

Change-Id: I3d199fac244eca94fc434d19c78bc5a17e804c37
Signed-off-by: Amir Mofakhar <amofakhar@op5.com>
This commit is contained in:
Amir Mofakhar 2018-06-26 12:47:39 +02:00
parent 1562cdb243
commit 5729d7e7c8
32 changed files with 639 additions and 149 deletions

2
.gitignore vendored
View File

@ -16,7 +16,7 @@ MANIFEST
AUTHORS
ChangeLog
monasca-log-api.log
etc/monasca/log-api.conf.sample
etc/monasca/*.sample
*.swp
*.iml

View File

@ -5,3 +5,9 @@ To generate sample configuration execute
```sh
tox -e genconfig
```
To generate the sample policies execute
```sh
tox -e genpolicy
```

View File

@ -5,3 +5,4 @@ format = ini
summarize = True
namespace = monasca_log_api
namespace = oslo.log
namespace = oslo.policy

View File

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

View File

@ -1 +1 @@
_static/*.conf.sample
_static/*.sample

View File

@ -39,6 +39,7 @@ extensions = [
'oslo_config.sphinxconfiggen',
'oslo_config.sphinxext',
'openstackdocstheme',
'oslo_policy.sphinxpolicygen'
]
# geeneral information about project
@ -54,6 +55,11 @@ author = u'OpenStack Foundation'
# sample config
config_generator_config_file = [
('config-generator/monasca-log-api.conf', '_static/log-api')
]
policy_generator_config_file = [
('config-generator/policy.conf', '_static/log-api')
]
# Add any paths that contain templates here, relative to this directory.

View File

@ -17,7 +17,7 @@ configuration files.
This means that gunicorn reports the CLI options of
oslo as unknown, and vice versa.
There are 3 configuration files. For more details on the configuration
There are 4 configuration files. For more details on the configuration
options, see :ref:`here <configuration-files>`.
Configuring Keystone Authorization
@ -82,7 +82,8 @@ The configuration for ``monitoring`` should either be provided in
Configuring RBAC
----------------
At the moment monasca-log-api does not feature RBAC with ``oslo.policies``.
At the moment monasca-log-api does not feature RBAC fully with
``oslo.policies``.
It provides a custom mechanism, however, that can be configured as follows:
* ``path`` - list of URIs that RBAC applies to
@ -95,6 +96,9 @@ It provides a custom mechanism, however, that can be configured as follows:
The configuration for ``roles_middleware`` should either be provided in
``log-api.conf`` or in a file in one of the configuration directories.
The configuration for accessing the services by ``oslo.policies`` can be
provided in ``log-api.policy.yaml``.
Configuring Logging
-------------------
@ -137,3 +141,21 @@ based on your deployment:
* `oslo.log <https://docs.openstack.org/oslo.log/latest/index.html>`_
* `Python HowTo <https://docs.python.org/2/howto/logging.html>`_
* `Logging handlers <https://docs.python.org/2/library/logging.handlers.html>`_
Configuring Policies
--------------------
The policies for accessing each service can be configured in the
``log-api.policy.yaml`` configuration file::
Policy Description
Method Path
"Policy string": "Roles"
example::
Logs post rule
POST /logs
POST /log/single
"log_api:logs:post": "role:monasca-user"

View File

@ -83,3 +83,17 @@ To enable ``oslo_middleware.debug:Debug`` for ``Log v3`` pipeline,
This particular filter might be useful for examining the
WSGI environment during troubleshooting or local development.
log-api.policy.yaml
-------------------
This is the configuration file for policies to access the services.
the path of the file can be defined in ``log-api.conf``::
[oslo_policy]
policy_file = log-api.policy.yaml
More information about policy file configuration can be found at
`oslo.policy <https://docs.openstack.org/oslo.policy/latest/admin/policy-yaml-file.html>`_
A sample of this configuration file is also available
:ref:`here <sample-configuration-policy>`

View File

@ -38,3 +38,14 @@ This sample configuration can also be viewed in `log-api-paste.ini
<https://github.com/openstack/monasca-log-api/blob/master/etc/monasca/log-api-paste.ini>`_.
.. literalinclude:: ../../../etc/monasca/log-api-paste.ini
.. _sample-configuration-policy:
Sample Configuration For Policy
-------------------------------
This sample configuration can also be viewed in `log-api-policy.yaml.sample
<../_static/log-api-policy.yaml.sample>`_.
.. literalinclude:: ../_static/log-api.policy.yaml.sample

View File

@ -61,7 +61,6 @@ class LogPublisher(object):
"""
def __init__(self):
self._topics = CONF.log_publisher.topics
self.max_message_size = CONF.log_publisher.max_message_size

View File

@ -14,9 +14,14 @@
import falcon
from oslo_context import context
from monasca_common.policy import policy_engine as policy
from monasca_log_api.app.base import request_context
from monasca_log_api.app.base import validation
from monasca_log_api import policies
policy.POLICIES = policies
_TENANT_ID_PARAM = 'tenant_id'
"""Name of the query-param pointing at project-id (tenant-id)"""
@ -32,7 +37,7 @@ class Request(falcon.Request):
def __init__(self, env, options=None):
super(Request, self).__init__(env, options)
self.context = context.RequestContext.from_environ(self.env)
self.context = request_context.RequestContext.from_environ(self.env)
def validate(self, content_types):
"""Performs common request validation
@ -99,5 +104,8 @@ class Request(falcon.Request):
"""
return self.context.roles
def can(self, action, target=None):
return self.context.can(action, target)
def __repr__(self):
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_log_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

@ -244,3 +244,24 @@ def validate_log_message(log_object):
raise exceptions.HTTPUnprocessableEntity(
'Log property should have message'
)
def validate_authorization(http_request, authorized_rules_list):
"""Validates whether is authorized according to provided policy rules list.
If authorization fails, 401 is thrown with appropriate description.
Additionally response specifies 'WWW-Authenticate' header with 'Token'
value challenging the client to use different token (the one with
different set of roles which can access the service).
"""
challenge = 'Token'
for rule in authorized_rules_list:
try:
http_request.can(rule)
return
except Exception as ex:
LOG.debug(ex)
raise falcon.HTTPUnauthorized('Forbidden',
'The request does not have access to this service',
challenge)

View File

@ -15,6 +15,7 @@
import falcon
from monasca_common.rest import utils as rest_utils
from monasca_log_api.app.base.validation import validate_authorization
from monasca_log_api.app.controller.api import healthcheck_api
from monasca_log_api.healthcheck import kafka_check
@ -33,13 +34,14 @@ class HealthChecks(healthcheck_api.HealthChecksApi):
super(HealthChecks, self).__init__()
def on_head(self, req, res):
validate_authorization(req, ['log_api:healthcheck:head'])
res.status = self.HEALTHY_CODE_HEAD
res.cache_control = self.CACHE_CONTROL
def on_get(self, req, res):
# at this point we know API is alive, so
# keep up good work and verify kafka status
validate_authorization(req, ['log_api:healthcheck:get'])
kafka_result = self._kafka_check.healthcheck()
# in case it'd be unhealthy,

View File

@ -18,6 +18,7 @@ import six
from monasca_log_api.app.base import log_publisher
from monasca_log_api.app.base.validation import validate_authorization
from monasca_log_api.app.controller.api import headers
from monasca_log_api.app.controller.api import logs_api
from monasca_log_api.app.controller.v2.aid import service
@ -41,6 +42,7 @@ class Logs(logs_api.LogsApi):
@falcon.deprecated(_DEPRECATED_INFO)
def on_post(self, req, res):
validate_authorization(req, ['log_api:logs:post'])
if CONF.monitoring.enable:
with self._logs_processing_time.time(name=None):
self.process_on_post_request(req, res)

View File

@ -50,6 +50,7 @@ class Logs(logs_api.LogsApi):
self._processor = bulk_processor.BulkProcessor()
def on_post(self, req, res):
validation.validate_authorization(req, ['log_api:logs:post'])
if CONF.monitoring.enable:
with self._logs_processing_time.time(name=None):
self.process_on_post_request(req, res)

View File

@ -18,6 +18,7 @@ import six
from monasca_common.rest import utils as rest_utils
from monasca_log_api.app.base.validation import validate_authorization
from monasca_log_api.app.controller.api import versions_api
_VERSIONS_TPL_DICT = {
@ -69,6 +70,7 @@ class Versions(versions_api.VersionsAPI):
res.status = falcon.HTTP_400
def on_get(self, req, res, version_id=None):
validate_authorization(req, ['log_api:versions:get'])
result = {
'links': _get_common_links(req),
'elements': []

View File

@ -29,7 +29,11 @@ role_m_opts = [
cfg.ListOpt(name='delegate_roles',
default=['admin'],
help=('Roles that are allowed to POST logs on '
'behalf of another tenant (project)'))
'behalf of another tenant (project)')),
cfg.ListOpt(name='check_roles',
default=['@'],
help=('Roles that are allowed to do check '
'version and health'))
]
role_m_group = cfg.OptGroup(name='roles_middleware', title='roles_middleware')

View File

@ -15,6 +15,7 @@
import sys
from oslo_log import log
from oslo_policy import opts as policy_opts
from monasca_log_api import conf
from monasca_log_api import version
@ -56,5 +57,6 @@ def parse_args(argv=None):
version=version.version_str)
conf.register_opts()
policy_opts.set_defaults(CONF)
_CONF_LOADED = True

View File

@ -98,47 +98,38 @@ class RoleMiddleware(om.ConfigurableMiddleware):
return None
is_authenticated = self._is_authenticated(req)
is_authorized, is_agent = self._is_authorized(req)
is_agent = self._is_agent(req)
tenant_id = req.headers.get('X-Tenant-Id')
req.environ[_X_MONASCA_LOG_AGENT] = is_agent
LOG.debug('%s is authenticated=%s, authorized=%s, log_agent=%s',
tenant_id, is_authenticated, is_authorized, is_agent)
LOG.debug('%s is authenticated=%s, log_agent=%s',
tenant_id, is_authenticated, is_agent)
if is_authenticated and is_authorized:
LOG.debug('%s has been authenticated and authorized', tenant_id)
if is_authenticated:
LOG.debug('%s has been authenticated', tenant_id)
return # do return nothing to enter API internal
# whoops
if is_authorized:
explanation = u'Failed to authenticate request for %s' % tenant_id
else:
explanation = (u'Tenant %s is missing a required role to access '
u'this service' % tenant_id)
explanation = u'Failed to authenticate request for %s' % tenant_id
LOG.error(explanation)
json_body = {u'title': u'Unauthorized', u'message': explanation}
return response.Response(status=401,
json_body=json_body,
content_type='application/json')
if explanation is not None:
LOG.error(explanation)
json_body = {u'title': u'Unauthorized', u'message': explanation}
return response.Response(status=401,
json_body=json_body,
content_type='application/json')
def _is_authorized(self, req):
def _is_agent(self, req):
headers = req.headers
roles = headers.get(_X_ROLES)
if not roles:
LOG.warning('Couldn\'t locate %s header,or it was empty', _X_ROLES)
return False, False
return False
else:
roles = _ensure_lower_roles(roles.split(','))
is_agent = len(_intersect(roles, self._agent_roles)) > 0
is_authorized = (len(_intersect(roles, self._default_roles)) > 0 or
is_agent)
return is_authorized, is_agent
return is_agent
def _is_authenticated(self, req):
headers = req.headers

View File

@ -0,0 +1,79 @@
# 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_log_api.conf import role_middleware
LOG = log.getLogger(__name__)
_BASE_MOD_PATH = 'monasca_log_api.policies.'
CONF = cfg.CONF
def roles_list_to_check_str(roles_list):
if roles_list:
converted_roles_list = ["role:" + role if role != '@' else role for role in roles_list]
return ' or '.join(converted_roles_list)
else:
return None
role_middleware.register_opts(CONF)
DEFAULT_AUTHORIZED_ROLES = roles_list_to_check_str(cfg.CONF.roles_middleware.default_roles)
AGENT_AUTHORIZED_ROLES = roles_list_to_check_str(cfg.CONF.roles_middleware.agent_roles)
DELEGATE_AUTHORIZED_ROLES = roles_list_to_check_str(cfg.CONF.roles_middleware.delegate_roles)
CHECK_AUTHORIZED_ROLES = roles_list_to_check_str(cfg.CONF.roles_middleware.check_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,40 @@
# 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_log_api.policies import CHECK_AUTHORIZED_ROLES
rules = [
policy.DocumentedRuleDefault(
name='log_api:healthcheck:head',
check_str=CHECK_AUTHORIZED_ROLES,
description='Healthcheck head rule',
operations=[
{'path': '/', 'method': 'HEAD'}
]
),
policy.DocumentedRuleDefault(
name='log_api:healthcheck:get',
check_str=CHECK_AUTHORIZED_ROLES,
description='Healthcheck get rule',
operations=[
{'path': '/', 'method': 'GET'}
]
),
]
def list_rules():
return rules

View File

@ -0,0 +1,38 @@
# 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_log_api.policies import AGENT_AUTHORIZED_ROLES
from monasca_log_api.policies import DEFAULT_AUTHORIZED_ROLES
from monasca_log_api.policies import DELEGATE_AUTHORIZED_ROLES
rules = [
policy.DocumentedRuleDefault(
name='log_api:logs:post',
check_str=' or '.join(filter(None, [AGENT_AUTHORIZED_ROLES,
DEFAULT_AUTHORIZED_ROLES,
DELEGATE_AUTHORIZED_ROLES])),
description='Logs post rule',
operations=[
{'path': '/logs', 'method': 'POST'},
{'path': '/log/single', 'method': 'POST'}
]
)
]
def list_rules():
return rules

View File

@ -0,0 +1,35 @@
# 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_log_api.policies import CHECK_AUTHORIZED_ROLES
rules = [
policy.DocumentedRuleDefault(
name='log_api:versions:get',
check_str=CHECK_AUTHORIZED_ROLES,
description='Versions get rule',
operations=[
{'path': '/', 'method': 'GET'},
{'path': '/version', 'method': 'GET'},
{'path': '/version/{version_id}', 'method': 'GET'}
]
)
]
def list_rules():
return rules

View File

@ -1,6 +1,7 @@
# coding=utf-8
# Copyright 2015 kornicameister@gmail.com
# Copyright 2015-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
@ -15,6 +16,7 @@
# under the License.
import codecs
import os
import random
import string
@ -22,14 +24,19 @@ import falcon
from falcon import testing
import fixtures
import mock
from monasca_common.policy import policy_engine as policy
from oslo_config import fixture as oo_cfg
from oslo_context import fixture as oo_ctx
from oslo_serialization import jsonutils
from oslotest import base as oslotest_base
import six
from monasca_log_api.app.base import request
from monasca_log_api import conf
from monasca_log_api import config
from monasca_log_api import policies
policy.POLICIES = policies
class MockedAPI(falcon.API):
@ -149,6 +156,40 @@ class ConfigFixture(oo_cfg.Config):
self.conf.set_default('kafka_url', '127.0.0.1', 'log_publisher')
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
class BaseTestCase(oslotest_base.BaseTestCase):
def setUp(self):
@ -156,6 +197,7 @@ class BaseTestCase(oslotest_base.BaseTestCase):
self.useFixture(ConfigFixture())
self.useFixture(DisableStatsdFixture())
self.useFixture(oo_ctx.ClearRequestContext())
self.useFixture(PolicyFixture())
@staticmethod
def conf_override(**kw):

View File

@ -51,7 +51,7 @@ class TestApiLogs(base.BaseApiTestCase):
'/log/single',
method='POST',
headers={
headers.X_ROLES.name: 'some_role',
headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: 'a:1',
'Content-Type': 'application/json',
'Content-Length': '0'
@ -75,7 +75,7 @@ class TestApiLogs(base.BaseApiTestCase):
'Content-Length': '0'
}
)
self.assertEqual(falcon.HTTP_403, self.srmock.status)
self.assertEqual(falcon.HTTP_401, self.srmock.status)
@mock.patch('monasca_log_api.app.controller.v2.aid.service.LogCreator')
@mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher')
@ -90,7 +90,7 @@ class TestApiLogs(base.BaseApiTestCase):
'/log/single',
method='POST',
headers={
headers.X_ROLES.name: 'some_role',
headers.X_ROLES.name: ROLES,
headers.X_DIMENSIONS.name: 'a:1',
'Content-Type': 'application/json',
'Content-Length': '0'

View File

@ -237,11 +237,12 @@ class TestApiLogs(base.BaseApiTestCase):
method='POST',
query_string='tenant_id=1',
headers={
headers.X_ROLES.name: ROLES,
'Content-Type': 'application/json',
'Content-Length': '0'
}
)
self.assertEqual(falcon.HTTP_403, self.srmock.status)
self.assertEqual(falcon.HTTP_400, self.srmock.status)
@mock.patch('monasca_log_api.app.controller.v3.aid.bulk_processor.'
'BulkProcessor')
@ -257,7 +258,7 @@ class TestApiLogs(base.BaseApiTestCase):
'/logs',
method='POST',
headers={
headers.X_ROLES.name: 'some_role',
headers.X_ROLES.name: ROLES,
'Content-Type': 'application/json',
'Content-Length': str(content_length)
},

View File

@ -0,0 +1,214 @@
# 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_log_api.app.base import request
from monasca_log_api.policies import roles_list_to_check_str
from monasca_log_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.default_roles = ['monasca-user', 'admin']
def test_healthchecks_policies_roles(self):
healthcheck_policies = {
'log_api:healthcheck:head': ['any_role'],
'log_api:healthcheck:get': ['any_role']
}
self._assert_rules(healthcheck_policies)
def test_versions_policies_roles(self):
versions_policies = {
'log_api:versions:get': ['any_role']
}
self._assert_rules(versions_policies)
def test_logs_policies_roles(self):
logs_policies = {
'log_api:logs:post': self.default_roles
}
self._assert_rules(logs_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')
self.assertIsNone(roles_list_to_check_str(None))

View File

@ -125,23 +125,7 @@ class RolesMiddlewareSideLogicTest(base.BaseTestCase):
self.assertFalse(instance._is_authenticated(req))
def test_should_return_true_if_authorized_no_agent(self):
roles = 'cmm-admin,cmm-user'
roles_array = roles.split(',')
instance = rm.RoleMiddleware(None)
instance._default_roles = roles_array
instance._agent_roles = []
req = mock.Mock()
req.headers = {rm._X_ROLES: roles}
is_authorized, is_agent = instance._is_authorized(req)
self.assertFalse(is_agent)
self.assertTrue(is_authorized)
def test_should_return_true_if_authorized_with_agent(self):
def test_should_return_true_if_is_agent(self):
roles = 'cmm-admin,cmm-user'
roles_array = roles.split(',')
@ -155,48 +139,9 @@ class RolesMiddlewareSideLogicTest(base.BaseTestCase):
req = mock.Mock()
req.headers = {rm._X_ROLES: roles}
is_authorized, is_agent = instance._is_authorized(req)
is_agent = instance._is_agent(req)
self.assertTrue(is_agent)
self.assertTrue(is_authorized)
def test_should_return_not_authorized_no_x_roles(self):
roles = 'cmm-admin,cmm-user'
roles_array = roles.split(',')
default_roles = [roles_array[0]]
admin_roles = [roles_array[1]]
instance = rm.RoleMiddleware(None)
instance._default_roles = default_roles
instance._agent_roles = admin_roles
req = mock.Mock()
req.headers = {}
is_authorized, is_agent = instance._is_authorized(req)
self.assertFalse(is_agent)
self.assertFalse(is_authorized)
def test_should_return_authorized_if_at_least_agent_true(self):
roles = 'cmm-admin,cmm-user'
roles_array = roles.split(',')
default_roles = ['different_role']
admin_roles = [roles_array[1]]
instance = rm.RoleMiddleware(None)
instance._default_roles = default_roles
instance._agent_roles = admin_roles
req = mock.Mock()
req.headers = {rm._X_ROLES: roles}
is_authorized, is_agent = instance._is_authorized(req)
self.assertTrue(is_agent)
self.assertTrue(is_authorized)
class RolesMiddlewareLogicTest(base.BaseTestCase):
@ -215,7 +160,7 @@ class RolesMiddlewareLogicTest(base.BaseTestCase):
# spying
instance._is_authenticated = mock.Mock()
instance._is_authorized = mock.Mock()
instance._is_agent = mock.Mock()
req = mock.Mock()
req.headers = {rm._X_ROLES: roles}
@ -224,7 +169,7 @@ class RolesMiddlewareLogicTest(base.BaseTestCase):
instance.process_request(req=req)
self.assertFalse(instance._is_authenticated.called)
self.assertFalse(instance._is_authorized.called)
self.assertFalse(instance._is_agent.called)
def test_not_process_further_if_cannot_apply_method(self):
roles = 'cmm-admin,cmm-user'
@ -240,7 +185,7 @@ class RolesMiddlewareLogicTest(base.BaseTestCase):
# spying
instance._is_authenticated = mock.Mock()
instance._is_authorized = mock.Mock()
instance._is_agent = mock.Mock()
req = mock.Mock()
req.headers = {rm._X_ROLES: roles}
@ -250,65 +195,16 @@ class RolesMiddlewareLogicTest(base.BaseTestCase):
instance.process_request(req=req)
self.assertFalse(instance._is_authenticated.called)
self.assertFalse(instance._is_authorized.called)
self.assertFalse(instance._is_agent.called)
def test_should_return_None_if_authenticated_authorized(self):
instance = rm.RoleMiddleware(None)
is_authorized = True
is_agent = True
instance._can_apply_middleware = mock.Mock(return_value=True)
instance._is_authorized = mock.Mock(return_value=(is_authorized,
is_agent))
instance._is_authenticated = mock.Mock(return_value=True)
req = mock.Mock()
req.environ = {}
result = instance.process_request(req=req)
self.assertIsNone(result)
def test_should_produce_json_response_if_not_authorized_but_authenticated(
def test_should_produce_json_response_if_not_authenticated(
self):
instance = rm.RoleMiddleware(None)
is_authorized = False
is_agent = False
is_authenticated = True
instance._can_apply_middleware = mock.Mock(return_value=True)
instance._is_authorized = mock.Mock(return_value=(is_authorized,
is_agent))
instance._is_authenticated = mock.Mock(return_value=is_authenticated)
req = mock.Mock()
req.environ = {}
req.headers = {
'X-Tenant-Id': '11111111'
}
result = instance.process_request(req=req)
self.assertIsNotNone(result)
self.assertIsInstance(result, response.Response)
status = result.status_code
json_body = result.json_body
message = json_body.get('message')
self.assertIn('is missing a required role to access', message)
self.assertEqual(401, status)
def test_should_produce_json_response_if_not_authenticated_but_authorized(
self):
instance = rm.RoleMiddleware(None)
is_authorized = True
is_agent = True
is_authenticated = False
instance._can_apply_middleware = mock.Mock(return_value=True)
instance._is_authorized = mock.Mock(return_value=(is_authorized,
is_agent))
instance._is_agent = mock.Mock(return_value=is_agent)
instance._is_authenticated = mock.Mock(return_value=is_authenticated)
req = mock.Mock()

View File

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

View File

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

View File

@ -89,6 +89,11 @@ basepython = python3
description = Generates sample documentation file for monasca-log-api
commands = oslo-config-generator --config-file=config-generator/monasca-log-api.conf
[testenv:genpolicy]
basepython = python3
description = Generates sample policy.json file for monasca-log-api
commands = oslopolicy-sample-generator --config-file=config-generator/policy.conf
[testenv:docs]
basepython = python3
description = Builds api-ref, api-guide, releasenotes and devdocs