Merge "Using oslo.policy for monasca-log-api"
This commit is contained in:
commit
8b6582d8d4
|
@ -16,7 +16,7 @@ MANIFEST
|
|||
AUTHORS
|
||||
ChangeLog
|
||||
monasca-log-api.log
|
||||
etc/monasca/log-api.conf.sample
|
||||
etc/monasca/*.sample
|
||||
|
||||
*.swp
|
||||
*.iml
|
||||
|
|
|
@ -5,3 +5,9 @@ To generate sample configuration execute
|
|||
```sh
|
||||
tox -e genconfig
|
||||
```
|
||||
|
||||
To generate the sample policies execute
|
||||
|
||||
```sh
|
||||
tox -e genpolicy
|
||||
```
|
||||
|
|
|
@ -5,3 +5,4 @@ format = ini
|
|||
summarize = True
|
||||
namespace = monasca_log_api
|
||||
namespace = oslo.log
|
||||
namespace = oslo.policy
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
[DEFAULT]
|
||||
output_file = etc/monasca/log-api.policy.yaml.sample
|
||||
format = yaml
|
||||
namespace = monasca_log_api
|
|
@ -1 +1 @@
|
|||
_static/*.conf.sample
|
||||
_static/*.sample
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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>`
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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': []
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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))
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
|
5
tox.ini
5
tox.ini
|
@ -74,6 +74,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
|
||||
|
|
Loading…
Reference in New Issue