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:
parent
1562cdb243
commit
5729d7e7c8
|
@ -16,7 +16,7 @@ MANIFEST
|
||||||
AUTHORS
|
AUTHORS
|
||||||
ChangeLog
|
ChangeLog
|
||||||
monasca-log-api.log
|
monasca-log-api.log
|
||||||
etc/monasca/log-api.conf.sample
|
etc/monasca/*.sample
|
||||||
|
|
||||||
*.swp
|
*.swp
|
||||||
*.iml
|
*.iml
|
||||||
|
|
|
@ -5,3 +5,9 @@ To generate sample configuration execute
|
||||||
```sh
|
```sh
|
||||||
tox -e genconfig
|
tox -e genconfig
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To generate the sample policies execute
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tox -e genpolicy
|
||||||
|
```
|
||||||
|
|
|
@ -5,3 +5,4 @@ format = ini
|
||||||
summarize = True
|
summarize = True
|
||||||
namespace = monasca_log_api
|
namespace = monasca_log_api
|
||||||
namespace = oslo.log
|
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.sphinxconfiggen',
|
||||||
'oslo_config.sphinxext',
|
'oslo_config.sphinxext',
|
||||||
'openstackdocstheme',
|
'openstackdocstheme',
|
||||||
|
'oslo_policy.sphinxpolicygen'
|
||||||
]
|
]
|
||||||
|
|
||||||
# geeneral information about project
|
# geeneral information about project
|
||||||
|
@ -54,6 +55,11 @@ author = u'OpenStack Foundation'
|
||||||
# sample config
|
# sample config
|
||||||
config_generator_config_file = [
|
config_generator_config_file = [
|
||||||
('config-generator/monasca-log-api.conf', '_static/log-api')
|
('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.
|
# 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
|
This means that gunicorn reports the CLI options of
|
||||||
oslo as unknown, and vice versa.
|
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>`.
|
options, see :ref:`here <configuration-files>`.
|
||||||
|
|
||||||
Configuring Keystone Authorization
|
Configuring Keystone Authorization
|
||||||
|
@ -82,7 +82,8 @@ The configuration for ``monitoring`` should either be provided in
|
||||||
Configuring RBAC
|
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:
|
It provides a custom mechanism, however, that can be configured as follows:
|
||||||
|
|
||||||
* ``path`` - list of URIs that RBAC applies to
|
* ``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
|
The configuration for ``roles_middleware`` should either be provided in
|
||||||
``log-api.conf`` or in a file in one of the configuration directories.
|
``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
|
Configuring Logging
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
@ -137,3 +141,21 @@ based on your deployment:
|
||||||
* `oslo.log <https://docs.openstack.org/oslo.log/latest/index.html>`_
|
* `oslo.log <https://docs.openstack.org/oslo.log/latest/index.html>`_
|
||||||
* `Python HowTo <https://docs.python.org/2/howto/logging.html>`_
|
* `Python HowTo <https://docs.python.org/2/howto/logging.html>`_
|
||||||
* `Logging handlers <https://docs.python.org/2/library/logging.handlers.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
|
This particular filter might be useful for examining the
|
||||||
WSGI environment during troubleshooting or local development.
|
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>`_.
|
<https://github.com/openstack/monasca-log-api/blob/master/etc/monasca/log-api-paste.ini>`_.
|
||||||
|
|
||||||
.. literalinclude:: ../../../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):
|
def __init__(self):
|
||||||
|
|
||||||
self._topics = CONF.log_publisher.topics
|
self._topics = CONF.log_publisher.topics
|
||||||
self.max_message_size = CONF.log_publisher.max_message_size
|
self.max_message_size = CONF.log_publisher.max_message_size
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,14 @@
|
||||||
|
|
||||||
import falcon
|
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.app.base import validation
|
||||||
|
from monasca_log_api import policies
|
||||||
|
|
||||||
|
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)"""
|
||||||
|
@ -32,7 +37,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)
|
||||||
|
|
||||||
def validate(self, content_types):
|
def validate(self, content_types):
|
||||||
"""Performs common request validation
|
"""Performs common request validation
|
||||||
|
@ -99,5 +104,8 @@ class Request(falcon.Request):
|
||||||
"""
|
"""
|
||||||
return self.context.roles
|
return self.context.roles
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
@ -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(
|
raise exceptions.HTTPUnprocessableEntity(
|
||||||
'Log property should have message'
|
'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
|
import falcon
|
||||||
from monasca_common.rest import utils as rest_utils
|
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.app.controller.api import healthcheck_api
|
||||||
from monasca_log_api.healthcheck import kafka_check
|
from monasca_log_api.healthcheck import kafka_check
|
||||||
|
|
||||||
|
@ -33,13 +34,14 @@ class HealthChecks(healthcheck_api.HealthChecksApi):
|
||||||
super(HealthChecks, self).__init__()
|
super(HealthChecks, self).__init__()
|
||||||
|
|
||||||
def on_head(self, req, res):
|
def on_head(self, req, res):
|
||||||
|
validate_authorization(req, ['log_api:healthcheck:head'])
|
||||||
res.status = self.HEALTHY_CODE_HEAD
|
res.status = self.HEALTHY_CODE_HEAD
|
||||||
res.cache_control = self.CACHE_CONTROL
|
res.cache_control = self.CACHE_CONTROL
|
||||||
|
|
||||||
def on_get(self, req, res):
|
def on_get(self, req, res):
|
||||||
# at this point we know API is alive, so
|
# at this point we know API is alive, so
|
||||||
# keep up good work and verify kafka status
|
# keep up good work and verify kafka status
|
||||||
|
validate_authorization(req, ['log_api:healthcheck:get'])
|
||||||
kafka_result = self._kafka_check.healthcheck()
|
kafka_result = self._kafka_check.healthcheck()
|
||||||
|
|
||||||
# in case it'd be unhealthy,
|
# 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 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 headers
|
||||||
from monasca_log_api.app.controller.api import logs_api
|
from monasca_log_api.app.controller.api import logs_api
|
||||||
from monasca_log_api.app.controller.v2.aid import service
|
from monasca_log_api.app.controller.v2.aid import service
|
||||||
|
@ -41,6 +42,7 @@ class Logs(logs_api.LogsApi):
|
||||||
|
|
||||||
@falcon.deprecated(_DEPRECATED_INFO)
|
@falcon.deprecated(_DEPRECATED_INFO)
|
||||||
def on_post(self, req, res):
|
def on_post(self, req, res):
|
||||||
|
validate_authorization(req, ['log_api:logs:post'])
|
||||||
if CONF.monitoring.enable:
|
if CONF.monitoring.enable:
|
||||||
with self._logs_processing_time.time(name=None):
|
with self._logs_processing_time.time(name=None):
|
||||||
self.process_on_post_request(req, res)
|
self.process_on_post_request(req, res)
|
||||||
|
|
|
@ -50,6 +50,7 @@ class Logs(logs_api.LogsApi):
|
||||||
self._processor = bulk_processor.BulkProcessor()
|
self._processor = bulk_processor.BulkProcessor()
|
||||||
|
|
||||||
def on_post(self, req, res):
|
def on_post(self, req, res):
|
||||||
|
validation.validate_authorization(req, ['log_api:logs:post'])
|
||||||
if CONF.monitoring.enable:
|
if CONF.monitoring.enable:
|
||||||
with self._logs_processing_time.time(name=None):
|
with self._logs_processing_time.time(name=None):
|
||||||
self.process_on_post_request(req, res)
|
self.process_on_post_request(req, res)
|
||||||
|
|
|
@ -18,6 +18,7 @@ import six
|
||||||
|
|
||||||
from monasca_common.rest import utils as rest_utils
|
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
|
from monasca_log_api.app.controller.api import versions_api
|
||||||
|
|
||||||
_VERSIONS_TPL_DICT = {
|
_VERSIONS_TPL_DICT = {
|
||||||
|
@ -69,6 +70,7 @@ class Versions(versions_api.VersionsAPI):
|
||||||
res.status = falcon.HTTP_400
|
res.status = falcon.HTTP_400
|
||||||
|
|
||||||
def on_get(self, req, res, version_id=None):
|
def on_get(self, req, res, version_id=None):
|
||||||
|
validate_authorization(req, ['log_api:versions:get'])
|
||||||
result = {
|
result = {
|
||||||
'links': _get_common_links(req),
|
'links': _get_common_links(req),
|
||||||
'elements': []
|
'elements': []
|
||||||
|
|
|
@ -29,7 +29,11 @@ role_m_opts = [
|
||||||
cfg.ListOpt(name='delegate_roles',
|
cfg.ListOpt(name='delegate_roles',
|
||||||
default=['admin'],
|
default=['admin'],
|
||||||
help=('Roles that are allowed to POST logs on '
|
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')
|
role_m_group = cfg.OptGroup(name='roles_middleware', title='roles_middleware')
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
from oslo_policy import opts as policy_opts
|
||||||
|
|
||||||
from monasca_log_api import conf
|
from monasca_log_api import conf
|
||||||
from monasca_log_api import version
|
from monasca_log_api import version
|
||||||
|
@ -56,5 +57,6 @@ def parse_args(argv=None):
|
||||||
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
|
||||||
|
|
|
@ -98,47 +98,38 @@ class RoleMiddleware(om.ConfigurableMiddleware):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
is_authenticated = self._is_authenticated(req)
|
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')
|
tenant_id = req.headers.get('X-Tenant-Id')
|
||||||
|
|
||||||
req.environ[_X_MONASCA_LOG_AGENT] = is_agent
|
req.environ[_X_MONASCA_LOG_AGENT] = is_agent
|
||||||
|
|
||||||
LOG.debug('%s is authenticated=%s, authorized=%s, log_agent=%s',
|
LOG.debug('%s is authenticated=%s, log_agent=%s',
|
||||||
tenant_id, is_authenticated, is_authorized, is_agent)
|
tenant_id, is_authenticated, is_agent)
|
||||||
|
|
||||||
if is_authenticated and is_authorized:
|
if is_authenticated:
|
||||||
LOG.debug('%s has been authenticated and authorized', tenant_id)
|
LOG.debug('%s has been authenticated', tenant_id)
|
||||||
return # do return nothing to enter API internal
|
return # do return nothing to enter API internal
|
||||||
|
|
||||||
# whoops
|
explanation = u'Failed to authenticate request for %s' % tenant_id
|
||||||
if is_authorized:
|
LOG.error(explanation)
|
||||||
explanation = u'Failed to authenticate request for %s' % tenant_id
|
json_body = {u'title': u'Unauthorized', u'message': explanation}
|
||||||
else:
|
return response.Response(status=401,
|
||||||
explanation = (u'Tenant %s is missing a required role to access '
|
json_body=json_body,
|
||||||
u'this service' % tenant_id)
|
content_type='application/json')
|
||||||
|
|
||||||
if explanation is not None:
|
def _is_agent(self, req):
|
||||||
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):
|
|
||||||
headers = req.headers
|
headers = req.headers
|
||||||
roles = headers.get(_X_ROLES)
|
roles = headers.get(_X_ROLES)
|
||||||
|
|
||||||
if not roles:
|
if not roles:
|
||||||
LOG.warning('Couldn\'t locate %s header,or it was empty', _X_ROLES)
|
LOG.warning('Couldn\'t locate %s header,or it was empty', _X_ROLES)
|
||||||
return False, False
|
return False
|
||||||
else:
|
else:
|
||||||
roles = _ensure_lower_roles(roles.split(','))
|
roles = _ensure_lower_roles(roles.split(','))
|
||||||
|
|
||||||
is_agent = len(_intersect(roles, self._agent_roles)) > 0
|
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):
|
def _is_authenticated(self, req):
|
||||||
headers = req.headers
|
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
|
# coding=utf-8
|
||||||
# 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
|
||||||
|
@ -15,6 +16,7 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
@ -22,14 +24,19 @@ import falcon
|
||||||
from falcon import testing
|
from falcon import testing
|
||||||
import fixtures
|
import fixtures
|
||||||
import mock
|
import mock
|
||||||
|
from monasca_common.policy import policy_engine as policy
|
||||||
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
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from monasca_log_api.app.base import request
|
from monasca_log_api.app.base import request
|
||||||
from monasca_log_api import conf
|
from monasca_log_api import conf
|
||||||
from monasca_log_api import config
|
from monasca_log_api import config
|
||||||
|
from monasca_log_api import policies
|
||||||
|
|
||||||
|
policy.POLICIES = policies
|
||||||
|
|
||||||
|
|
||||||
class MockedAPI(falcon.API):
|
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')
|
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):
|
class BaseTestCase(oslotest_base.BaseTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -156,6 +197,7 @@ class BaseTestCase(oslotest_base.BaseTestCase):
|
||||||
self.useFixture(ConfigFixture())
|
self.useFixture(ConfigFixture())
|
||||||
self.useFixture(DisableStatsdFixture())
|
self.useFixture(DisableStatsdFixture())
|
||||||
self.useFixture(oo_ctx.ClearRequestContext())
|
self.useFixture(oo_ctx.ClearRequestContext())
|
||||||
|
self.useFixture(PolicyFixture())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def conf_override(**kw):
|
def conf_override(**kw):
|
||||||
|
|
|
@ -51,7 +51,7 @@ class TestApiLogs(base.BaseApiTestCase):
|
||||||
'/log/single',
|
'/log/single',
|
||||||
method='POST',
|
method='POST',
|
||||||
headers={
|
headers={
|
||||||
headers.X_ROLES.name: 'some_role',
|
headers.X_ROLES.name: ROLES,
|
||||||
headers.X_DIMENSIONS.name: 'a:1',
|
headers.X_DIMENSIONS.name: 'a:1',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Content-Length': '0'
|
'Content-Length': '0'
|
||||||
|
@ -75,7 +75,7 @@ class TestApiLogs(base.BaseApiTestCase):
|
||||||
'Content-Length': '0'
|
'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.controller.v2.aid.service.LogCreator')
|
||||||
@mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher')
|
@mock.patch('monasca_log_api.app.base.log_publisher.LogPublisher')
|
||||||
|
@ -90,7 +90,7 @@ class TestApiLogs(base.BaseApiTestCase):
|
||||||
'/log/single',
|
'/log/single',
|
||||||
method='POST',
|
method='POST',
|
||||||
headers={
|
headers={
|
||||||
headers.X_ROLES.name: 'some_role',
|
headers.X_ROLES.name: ROLES,
|
||||||
headers.X_DIMENSIONS.name: 'a:1',
|
headers.X_DIMENSIONS.name: 'a:1',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Content-Length': '0'
|
'Content-Length': '0'
|
||||||
|
|
|
@ -237,11 +237,12 @@ class TestApiLogs(base.BaseApiTestCase):
|
||||||
method='POST',
|
method='POST',
|
||||||
query_string='tenant_id=1',
|
query_string='tenant_id=1',
|
||||||
headers={
|
headers={
|
||||||
|
headers.X_ROLES.name: ROLES,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Content-Length': '0'
|
'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.'
|
@mock.patch('monasca_log_api.app.controller.v3.aid.bulk_processor.'
|
||||||
'BulkProcessor')
|
'BulkProcessor')
|
||||||
|
@ -257,7 +258,7 @@ class TestApiLogs(base.BaseApiTestCase):
|
||||||
'/logs',
|
'/logs',
|
||||||
method='POST',
|
method='POST',
|
||||||
headers={
|
headers={
|
||||||
headers.X_ROLES.name: 'some_role',
|
headers.X_ROLES.name: ROLES,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Content-Length': str(content_length)
|
'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))
|
self.assertFalse(instance._is_authenticated(req))
|
||||||
|
|
||||||
def test_should_return_true_if_authorized_no_agent(self):
|
def test_should_return_true_if_is_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):
|
|
||||||
roles = 'cmm-admin,cmm-user'
|
roles = 'cmm-admin,cmm-user'
|
||||||
roles_array = roles.split(',')
|
roles_array = roles.split(',')
|
||||||
|
|
||||||
|
@ -155,48 +139,9 @@ class RolesMiddlewareSideLogicTest(base.BaseTestCase):
|
||||||
req = mock.Mock()
|
req = mock.Mock()
|
||||||
req.headers = {rm._X_ROLES: roles}
|
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_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):
|
class RolesMiddlewareLogicTest(base.BaseTestCase):
|
||||||
|
@ -215,7 +160,7 @@ class RolesMiddlewareLogicTest(base.BaseTestCase):
|
||||||
|
|
||||||
# spying
|
# spying
|
||||||
instance._is_authenticated = mock.Mock()
|
instance._is_authenticated = mock.Mock()
|
||||||
instance._is_authorized = mock.Mock()
|
instance._is_agent = mock.Mock()
|
||||||
|
|
||||||
req = mock.Mock()
|
req = mock.Mock()
|
||||||
req.headers = {rm._X_ROLES: roles}
|
req.headers = {rm._X_ROLES: roles}
|
||||||
|
@ -224,7 +169,7 @@ class RolesMiddlewareLogicTest(base.BaseTestCase):
|
||||||
instance.process_request(req=req)
|
instance.process_request(req=req)
|
||||||
|
|
||||||
self.assertFalse(instance._is_authenticated.called)
|
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):
|
def test_not_process_further_if_cannot_apply_method(self):
|
||||||
roles = 'cmm-admin,cmm-user'
|
roles = 'cmm-admin,cmm-user'
|
||||||
|
@ -240,7 +185,7 @@ class RolesMiddlewareLogicTest(base.BaseTestCase):
|
||||||
|
|
||||||
# spying
|
# spying
|
||||||
instance._is_authenticated = mock.Mock()
|
instance._is_authenticated = mock.Mock()
|
||||||
instance._is_authorized = mock.Mock()
|
instance._is_agent = mock.Mock()
|
||||||
|
|
||||||
req = mock.Mock()
|
req = mock.Mock()
|
||||||
req.headers = {rm._X_ROLES: roles}
|
req.headers = {rm._X_ROLES: roles}
|
||||||
|
@ -250,65 +195,16 @@ class RolesMiddlewareLogicTest(base.BaseTestCase):
|
||||||
instance.process_request(req=req)
|
instance.process_request(req=req)
|
||||||
|
|
||||||
self.assertFalse(instance._is_authenticated.called)
|
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):
|
def test_should_produce_json_response_if_not_authenticated(
|
||||||
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(
|
|
||||||
self):
|
self):
|
||||||
instance = rm.RoleMiddleware(None)
|
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_agent = True
|
||||||
is_authenticated = False
|
is_authenticated = False
|
||||||
|
|
||||||
instance._can_apply_middleware = mock.Mock(return_value=True)
|
instance._can_apply_middleware = mock.Mock(return_value=True)
|
||||||
instance._is_authorized = mock.Mock(return_value=(is_authorized,
|
instance._is_agent = mock.Mock(return_value=is_agent)
|
||||||
is_agent))
|
|
||||||
instance._is_authenticated = mock.Mock(return_value=is_authenticated)
|
instance._is_authenticated = mock.Mock(return_value=is_authenticated)
|
||||||
|
|
||||||
req = mock.Mock()
|
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 =
|
oslo.config.opts =
|
||||||
monasca_log_api = monasca_log_api.conf:list_opts
|
monasca_log_api = monasca_log_api.conf:list_opts
|
||||||
|
|
||||||
|
oslo.policy.policies =
|
||||||
|
monasca_log_api = monasca_log_api.policies:list_rules
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
all_files = 1
|
all_files = 1
|
||||||
build-dir = doc/build
|
build-dir = doc/build
|
||||||
|
|
5
tox.ini
5
tox.ini
|
@ -89,6 +89,11 @@ basepython = python3
|
||||||
description = Generates sample documentation file for monasca-log-api
|
description = Generates sample documentation file for monasca-log-api
|
||||||
commands = oslo-config-generator --config-file=config-generator/monasca-log-api.conf
|
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]
|
[testenv:docs]
|
||||||
basepython = python3
|
basepython = python3
|
||||||
description = Builds api-ref, api-guide, releasenotes and devdocs
|
description = Builds api-ref, api-guide, releasenotes and devdocs
|
||||||
|
|
Loading…
Reference in New Issue