Register and Document policy in code
Adds below things for the implementation of framework for registering and using default policy rules. * Policy-in-code The framework for registering and using default policy rules. Rules should be defined and returned from a module in masakari/policies/, and then added to the list in masakari/policies/__init__.py. A new context.can() method has been added for policy enforcement of registered rules. It has the same parameters as the enforce() method currently being used. * Add policy sample generation The entry point and config file necessary for using the oslo.policy sample generation script. It also adds a tox target to simplify the usage of it. * Add policy documentation and sample file Documentation and sample file for default policy in code feature. * Hacking check for policy registration It ensures that policy registration happens in the centralized masakari/policies/ directory. * Hacking check for _ENFORCER.enforce() Hacking check in order to ensure that only registered policies are used for authorization checks _ENFORCER.authorize should be used rather than _ENFORCER.enforce. * Add entry_point for oslo policy scripts There are two helper scripts in oslo.policy to help deployers understand their policy configuration better. With the setup.cfg entry these can be called directly from oslo.policy. Changes done here are with the reference of [1] at NOVA side which is contributed by Andrew Laski and Claudiu Belu [1] https://review.openstack.org/#/q/topic:bp/policy-in-code+project:openstack/nova+status:merged Change-Id: If885a66d92c31be440d27d6780635800a0b12e3e
This commit is contained in:
parent
b6a6e3cbf3
commit
d7592cbe25
4
.gitignore
vendored
4
.gitignore
vendored
@ -60,3 +60,7 @@ releasenotes/build
|
||||
|
||||
# PyCharm IDE
|
||||
.idea/
|
||||
|
||||
# policy sample generation
|
||||
etc/masakari/policy.yaml.sample
|
||||
|
||||
|
@ -43,3 +43,5 @@ Masakari Specific Commandments
|
||||
- [M329] Deprecated library function os.popen()
|
||||
- [M331] LOG.warn is deprecated. Enforce use of LOG.warning.
|
||||
- [M332] Yield must always be followed by a space when yielding a value.
|
||||
- [M333] Policy registration should be in the central location ``masakari/policies/``
|
||||
- [M334] Do not use the oslo_policy.policy.Enforcer.enforce() method.
|
||||
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"admin_api": "is_admin:True",
|
||||
"context_is_admin": "role:admin",
|
||||
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
|
||||
"default": "rule:admin_api",
|
||||
"os_masakari_api:extensions": "rule:admin_api",
|
||||
"os_masakari_api:segments": "rule:admin_api",
|
||||
"os_masakari_api:os-hosts": "rule:admin_api",
|
||||
"os_masakari_api:notifications": "rule:admin_api"
|
||||
}
|
90
doc/source/_static/masakari.policy.yaml.sample
Normal file
90
doc/source/_static/masakari.policy.yaml.sample
Normal file
@ -0,0 +1,90 @@
|
||||
# Decides what is required for the 'is_admin:True' check to succeed.
|
||||
#"context_is_admin": "role:admin"
|
||||
|
||||
# Default rule for most non-Admin APIs.
|
||||
#"admin_or_owner": "is_admin:True or project_id:%(project_id)s"
|
||||
|
||||
# Default rule for most Admin APIs.
|
||||
#"admin_api": "is_admin:True"
|
||||
|
||||
# List available extensions.
|
||||
# GET /extensions
|
||||
#"os_masakari_api:extensions:index": "rule:admin_api"
|
||||
|
||||
# Shows information for an extension.
|
||||
# GET /extensions/{extensions_id}
|
||||
#"os_masakari_api:extensions:detail": "rule:admin_api"
|
||||
|
||||
# Extension Info API extensions to change the API.
|
||||
#"os_masakari_api:extensions:discoverable": "rule:admin_api"
|
||||
|
||||
# Lists IDs, names, type, reserved, on_maintenance for all hosts.
|
||||
# GET /segments/{segment_id}/hosts
|
||||
#"os_masakari_api:os-hosts:index": "rule:admin_api"
|
||||
|
||||
# Shows details for a host.
|
||||
# GET /segments/{segment_id}/hosts/{host_id}
|
||||
#"os_masakari_api:os-hosts:detail": "rule:admin_api"
|
||||
|
||||
# Creates a host under given segment.
|
||||
# POST /segments/{segment_id}/hosts
|
||||
#"os_masakari_api:os-hosts:create": "rule:admin_api"
|
||||
|
||||
# Updates the editable attributes of an existing host.
|
||||
# PUT /segments/{segment_id}/hosts/{host_id}
|
||||
#"os_masakari_api:os-hosts:update": "rule:admin_api"
|
||||
|
||||
# Deletes a host from given segment.
|
||||
# DELETE /segments/{segment_id}/hosts/{host_id}
|
||||
#"os_masakari_api:os-hosts:delete": "rule:admin_api"
|
||||
|
||||
# Host API extensions to change the API.
|
||||
#"os_masakari_api:os-hosts:discoverable": "rule:admin_api"
|
||||
|
||||
# Lists IDs, notification types, host_name, generated_time, payload
|
||||
# and status for all notifications.
|
||||
# GET /notifications
|
||||
#"os_masakari_api:notifications:index": "rule:admin_api"
|
||||
|
||||
# Shows details for a notification.
|
||||
# GET /notifications/{notification_id}
|
||||
#"os_masakari_api:notifications:detail": "rule:admin_api"
|
||||
|
||||
# Creates a notiification.
|
||||
# POST /notifications
|
||||
#"os_masakari_api:notifications:create": "rule:admin_api"
|
||||
|
||||
# Notification API extensions to change the API.
|
||||
#"os_masakari_api:notifications:discoverable": "rule:admin_api"
|
||||
|
||||
# Lists IDs, names, description, recovery_method, service_type for all
|
||||
# segments.
|
||||
# GET /segments
|
||||
#"os_masakari_api:segments:index": "rule:admin_api"
|
||||
|
||||
# Shows details for a segment.
|
||||
# GET /segments/{segment_id}
|
||||
#"os_masakari_api:segments:detail": "rule:admin_api"
|
||||
|
||||
# Creates a segment.
|
||||
# POST /segments
|
||||
#"os_masakari_api:segments:create": "rule:admin_api"
|
||||
|
||||
# Updates the editable attributes of an existing host.
|
||||
# PUT /segments/{segment_id}
|
||||
#"os_masakari_api:segments:update": "rule:admin_api"
|
||||
|
||||
# Deletes a segment.
|
||||
# DELETE /segments/{segment_id}
|
||||
#"os_masakari_api:segments:delete": "rule:admin_api"
|
||||
|
||||
# Segment API extensions to change the API.
|
||||
#"os_masakari_api:segments:discoverable": "rule:admin_api"
|
||||
|
||||
# List all versions.
|
||||
# GET /
|
||||
#"os_masakari_api:versions:index": "@"
|
||||
|
||||
# Version API extensions to change the API.
|
||||
#"os_masakari_api:versions:discoverable": "@"
|
||||
|
@ -6,4 +6,4 @@ The following is a sample masakari policy file. Operator can configure policies
|
||||
as per his requirement. It is recommended that all api's of masakari should
|
||||
be allowed to admin user only.
|
||||
|
||||
.. literalinclude:: _static/masakari.policy.json.sample
|
||||
.. literalinclude:: _static/masakari.policy.yaml.sample
|
||||
|
3
etc/masakari/masakari-policy-generator.conf
Normal file
3
etc/masakari/masakari-policy-generator.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
output_file = etc/masakari/policy.yaml.sample
|
||||
namespace = masakari
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"admin_api": "is_admin:True",
|
||||
"context_is_admin": "role:admin",
|
||||
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
|
||||
"default": "rule:admin_api",
|
||||
"os_masakari_api:extensions": "rule:admin_api",
|
||||
"os_masakari_api:segments": "rule:admin_api",
|
||||
"os_masakari_api:os-hosts": "rule:admin_api",
|
||||
"os_masakari_api:notifications": "rule:admin_api"
|
||||
}
|
@ -22,11 +22,9 @@ import six
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
import masakari.api.openstack
|
||||
from masakari.api.openstack import wsgi
|
||||
from masakari import exception
|
||||
from masakari.i18n import _
|
||||
import masakari.policy
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@ -316,45 +314,6 @@ def load_standard_extensions(ext_mgr, logger, path, package, ext_list=None):
|
||||
dirnames[:] = subdirs
|
||||
|
||||
|
||||
# This will be deprecated after policy cleanup finished
|
||||
def core_authorizer(api_name, extension_name):
|
||||
def authorize(context, target=None, action=None):
|
||||
if target is None:
|
||||
target = {'project_id': context.project_id,
|
||||
'user_id': context.user_id}
|
||||
if action is None:
|
||||
act = '%s:%s' % (api_name, extension_name)
|
||||
else:
|
||||
act = '%s:%s:%s' % (api_name, extension_name, action)
|
||||
masakari.policy.enforce(context, act, target)
|
||||
return authorize
|
||||
|
||||
|
||||
def _soft_authorizer(hard_authorizer, api_name, extension_name):
|
||||
hard_authorize = hard_authorizer(api_name, extension_name)
|
||||
|
||||
def authorize(context, target=None, action=None):
|
||||
try:
|
||||
hard_authorize(context, target=target, action=action)
|
||||
return True
|
||||
except exception.Forbidden:
|
||||
return False
|
||||
return authorize
|
||||
|
||||
|
||||
# This will be deprecated after policy cleanup finished
|
||||
def soft_core_authorizer(api_name, extension_name):
|
||||
return _soft_authorizer(core_authorizer, api_name, extension_name)
|
||||
|
||||
|
||||
def os_masakari_authorizer(extension_name):
|
||||
return core_authorizer('os_masakari_api', extension_name)
|
||||
|
||||
|
||||
def os_masakari_soft_authorizer(extension_name):
|
||||
return soft_core_authorizer('os_masakari_api', extension_name)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class V1APIExtensionBase(object):
|
||||
"""Abstract base class for all v1 API extensions.
|
||||
|
@ -19,10 +19,11 @@ import webob.exc
|
||||
from masakari.api.openstack import extensions
|
||||
from masakari.api.openstack import wsgi
|
||||
from masakari import exception
|
||||
from masakari.policies import base as base_policies
|
||||
from masakari.policies import extension_info as extension_policies
|
||||
|
||||
ALIAS = 'extensions'
|
||||
LOG = logging.getLogger(__name__)
|
||||
authorize = extensions.os_masakari_authorizer(ALIAS)
|
||||
|
||||
|
||||
class FakeExtension(object):
|
||||
@ -53,10 +54,10 @@ class ExtensionInfoController(wsgi.Controller):
|
||||
"""Filter extensions list based on policy."""
|
||||
|
||||
discoverable_extensions = dict()
|
||||
|
||||
for alias, ext in self.extension_info.get_extensions().items():
|
||||
authorize = extensions.os_masakari_soft_authorizer(alias)
|
||||
if authorize(context, action='discoverable'):
|
||||
action = ':'.join([
|
||||
base_policies.MASAKARI_API, alias, 'discoverable'])
|
||||
if context.can(action, fatal=False):
|
||||
discoverable_extensions[alias] = ext
|
||||
else:
|
||||
LOG.debug("Filter out extension %s from discover list",
|
||||
@ -67,7 +68,7 @@ class ExtensionInfoController(wsgi.Controller):
|
||||
@extensions.expected_errors(())
|
||||
def index(self, req):
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(extension_policies.EXTENSIONS % 'index')
|
||||
discoverable_extensions = self._get_extensions(context)
|
||||
sorted_ext_list = sorted(discoverable_extensions.items())
|
||||
|
||||
@ -80,7 +81,7 @@ class ExtensionInfoController(wsgi.Controller):
|
||||
@extensions.expected_errors(http_client.NOT_FOUND)
|
||||
def show(self, req, id):
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(extension_policies.EXTENSIONS % 'detail')
|
||||
try:
|
||||
ext = self._get_extensions(context)[id]
|
||||
except KeyError:
|
||||
|
@ -29,9 +29,9 @@ from masakari import exception
|
||||
from masakari.ha import api as host_api
|
||||
from masakari.i18n import _
|
||||
from masakari import objects
|
||||
from masakari.policies import hosts as host_policies
|
||||
|
||||
ALIAS = "os-hosts"
|
||||
authorize = extensions.os_masakari_authorizer(ALIAS)
|
||||
|
||||
|
||||
class HostsController(wsgi.Controller):
|
||||
@ -45,7 +45,7 @@ class HostsController(wsgi.Controller):
|
||||
def index(self, req, segment_id):
|
||||
"""Returns a list a hosts."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(host_policies.HOSTS % 'index')
|
||||
|
||||
try:
|
||||
filters = {}
|
||||
@ -103,7 +103,7 @@ class HostsController(wsgi.Controller):
|
||||
def create(self, req, segment_id, body):
|
||||
"""Creates a host."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(host_policies.HOSTS % 'create')
|
||||
host_data = body.get('host')
|
||||
try:
|
||||
host = self.api.create_host(context, segment_id, host_data)
|
||||
@ -118,7 +118,7 @@ class HostsController(wsgi.Controller):
|
||||
def show(self, req, segment_id, id):
|
||||
"""Shows the details of a host."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(host_policies.HOSTS % 'detail')
|
||||
try:
|
||||
host = self.api.get_host(context, segment_id, id)
|
||||
except exception.HostNotFound as e:
|
||||
@ -133,7 +133,7 @@ class HostsController(wsgi.Controller):
|
||||
def update(self, req, segment_id, id, body):
|
||||
"""Updates the existing host."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(host_policies.HOSTS % 'update')
|
||||
host_data = body.get('host')
|
||||
try:
|
||||
host = self.api.update_host(context, segment_id, id, host_data)
|
||||
@ -152,7 +152,7 @@ class HostsController(wsgi.Controller):
|
||||
def delete(self, req, segment_id, id):
|
||||
"""Removes a host by id."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(host_policies.HOSTS % 'delete')
|
||||
try:
|
||||
self.api.delete_host(context, segment_id, id)
|
||||
except exception.FailoverSegmentNotFound as e:
|
||||
|
@ -25,9 +25,9 @@ from masakari.api import validation
|
||||
from masakari import exception
|
||||
from masakari.ha import api as notification_api
|
||||
from masakari.i18n import _
|
||||
from masakari.policies import notifications as notifications_policies
|
||||
|
||||
ALIAS = 'notifications'
|
||||
authorize = extensions.os_masakari_authorizer(ALIAS)
|
||||
|
||||
|
||||
class NotificationsController(wsgi.Controller):
|
||||
@ -43,7 +43,7 @@ class NotificationsController(wsgi.Controller):
|
||||
def create(self, req, body):
|
||||
"""Creates a new notification."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(notifications_policies.NOTIFICATIONS % 'create')
|
||||
|
||||
notification_data = body['notification']
|
||||
try:
|
||||
@ -61,7 +61,7 @@ class NotificationsController(wsgi.Controller):
|
||||
def index(self, req):
|
||||
"""Returns a summary list of notifications."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(notifications_policies.NOTIFICATIONS % 'index')
|
||||
try:
|
||||
limit, marker = common.get_limit_and_marker(req)
|
||||
sort_keys, sort_dirs = common.get_sort_params(req.params)
|
||||
@ -95,7 +95,7 @@ class NotificationsController(wsgi.Controller):
|
||||
def show(self, req, id):
|
||||
"""Return data about the given notification id."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(notifications_policies.NOTIFICATIONS % 'detail')
|
||||
|
||||
try:
|
||||
notification = self.api.get_notification(context, id)
|
||||
|
@ -23,10 +23,10 @@ from masakari.api.openstack import wsgi
|
||||
from masakari.api import validation
|
||||
from masakari import exception
|
||||
from masakari.ha import api as segment_api
|
||||
from masakari.policies import segments as segment_policies
|
||||
|
||||
|
||||
ALIAS = 'segments'
|
||||
authorize = extensions.os_masakari_authorizer(ALIAS)
|
||||
|
||||
|
||||
class SegmentsController(wsgi.Controller):
|
||||
@ -39,7 +39,7 @@ class SegmentsController(wsgi.Controller):
|
||||
def index(self, req):
|
||||
"""Returns a summary list of failover segments."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(segment_policies.SEGMENTS % 'index')
|
||||
|
||||
try:
|
||||
limit, marker = common.get_limit_and_marker(req)
|
||||
@ -66,7 +66,7 @@ class SegmentsController(wsgi.Controller):
|
||||
def show(self, req, id):
|
||||
"""Return data about the given segment id."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(segment_policies.SEGMENTS % 'detail')
|
||||
|
||||
try:
|
||||
segment = self.api.get_segment(context, id)
|
||||
@ -80,7 +80,7 @@ class SegmentsController(wsgi.Controller):
|
||||
def create(self, req, body):
|
||||
"""Creates a new failover segment."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(segment_policies.SEGMENTS % 'create')
|
||||
|
||||
segment_data = body['segment']
|
||||
try:
|
||||
@ -95,7 +95,7 @@ class SegmentsController(wsgi.Controller):
|
||||
def update(self, req, id, body):
|
||||
"""Updates the existing segment."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(segment_policies.SEGMENTS % 'update')
|
||||
segment_data = body['segment']
|
||||
|
||||
try:
|
||||
@ -113,7 +113,7 @@ class SegmentsController(wsgi.Controller):
|
||||
def delete(self, req, id):
|
||||
"""Removes a segment by uuid."""
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
context.can(segment_policies.SEGMENTS % 'delete')
|
||||
|
||||
try:
|
||||
self.api.delete_segment(context, id)
|
||||
|
@ -27,6 +27,7 @@ from oslo_log import log as logging
|
||||
from oslo_utils import timeutils
|
||||
import six
|
||||
|
||||
from masakari import exception
|
||||
from masakari.i18n import _
|
||||
from masakari import policy
|
||||
from masakari import utils
|
||||
@ -202,6 +203,39 @@ class RequestContext(context.RequestContext):
|
||||
|
||||
return context
|
||||
|
||||
def can(self, action, target=None, fatal=True):
|
||||
"""Verifies that the given action is valid on the target in this context.
|
||||
|
||||
:param action: string representing the action to be checked.
|
||||
:param target: dictionary representing the object of the action
|
||||
for object creation this should be a dictionary representing the
|
||||
location of the object e.g. ``{'project_id': context.project_id}``.
|
||||
If None, then this default target will be considered:
|
||||
{'project_id': self.project_id, 'user_id': self.user_id}
|
||||
:param fatal: if False, will return False when an exception.Forbidden
|
||||
occurs.
|
||||
|
||||
:raises masakari.exception.Forbidden: if verification fails and fatal
|
||||
is True.
|
||||
|
||||
:return: returns a non-False value (not necessarily "True") if
|
||||
authorized and False if not authorized and fatal is False.
|
||||
"""
|
||||
if target is None:
|
||||
target = {'project_id': self.project_id,
|
||||
'user_id': self.user_id}
|
||||
try:
|
||||
return policy.authorize(self, action, target)
|
||||
except exception.Forbidden:
|
||||
if fatal:
|
||||
raise
|
||||
return False
|
||||
|
||||
def to_policy_values(self):
|
||||
policy = super(RequestContext, self).to_policy_values()
|
||||
policy['is_admin'] = self.is_admin
|
||||
return policy
|
||||
|
||||
def __str__(self):
|
||||
return "<Context %s>" % self.to_dict()
|
||||
|
||||
|
@ -35,6 +35,8 @@ UNDERSCORE_IMPORT_FILES = []
|
||||
session_check = re.compile(r"\w*def [a-zA-Z0-9].*[(].*session.*[)]")
|
||||
cfg_re = re.compile(r".*\scfg\.")
|
||||
cfg_opt_re = re.compile(r".*[\s\[]cfg\.[a-zA-Z]*Opt\(")
|
||||
rule_default_re = re.compile(r".*RuleDefault\(")
|
||||
policy_enforce_re = re.compile(r".*_ENFORCER\.enforce\(")
|
||||
asse_trueinst_re = re.compile(
|
||||
r"(.)*assertTrue\(isinstance\((\w|\.|\'|\"|\[|\])+, "
|
||||
"(\w|\.|\'|\"|\[|\])+\)\)")
|
||||
@ -412,6 +414,38 @@ def yield_followed_by_space(logical_line):
|
||||
"M332: Yield keyword should be followed by a space.")
|
||||
|
||||
|
||||
def check_policy_registration_in_central_place(logical_line, filename):
|
||||
msg = ('M333: Policy registration should be in the central location '
|
||||
'"/masakari/policies/*".')
|
||||
# This is where registration should happen
|
||||
if "masakari/policies/" in filename:
|
||||
return
|
||||
# A couple of policy tests register rules
|
||||
if "masakari/tests/unit/test_policy.py" in filename:
|
||||
return
|
||||
|
||||
if rule_default_re.match(logical_line):
|
||||
yield (0, msg)
|
||||
|
||||
|
||||
def check_policy_enforce(logical_line, filename):
|
||||
"""Look for uses of masakari.policy._ENFORCER.enforce()
|
||||
|
||||
Now that policy defaults are registered in code the _ENFORCER.authorize
|
||||
method should be used. That ensures that only registered policies are used.
|
||||
Uses of _ENFORCER.enforce could allow unregistered policies to be used, so
|
||||
this check looks for uses of that method.
|
||||
|
||||
M333
|
||||
"""
|
||||
|
||||
msg = ('M334: masakari.policy._ENFORCER.enforce() should not be used. '
|
||||
'Use the authorize() method instead.')
|
||||
|
||||
if policy_enforce_re.match(logical_line):
|
||||
yield (0, msg)
|
||||
|
||||
|
||||
def factory(register):
|
||||
register(no_db_session_in_public_api)
|
||||
register(use_timeutils_utcnow)
|
||||
@ -438,3 +472,5 @@ def factory(register):
|
||||
register(no_os_popen)
|
||||
register(no_log_warn)
|
||||
register(yield_followed_by_space)
|
||||
register(check_policy_registration_in_central_place)
|
||||
register(check_policy_enforce)
|
||||
|
35
masakari/policies/__init__.py
Normal file
35
masakari/policies/__init__.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Copyright (C) 2018 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 itertools
|
||||
|
||||
from masakari.policies import base
|
||||
from masakari.policies import extension_info
|
||||
from masakari.policies import hosts
|
||||
from masakari.policies import notifications
|
||||
from masakari.policies import segments
|
||||
from masakari.policies import versions
|
||||
|
||||
|
||||
def list_rules():
|
||||
return itertools.chain(
|
||||
base.list_rules(),
|
||||
extension_info.list_rules(),
|
||||
hosts.list_rules(),
|
||||
notifications.list_rules(),
|
||||
segments.list_rules(),
|
||||
versions.list_rules()
|
||||
)
|
41
masakari/policies/base.py
Normal file
41
masakari/policies/base.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Copyright (C) 2018 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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
|
||||
|
||||
MASAKARI_API = 'os_masakari_api'
|
||||
|
||||
RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner'
|
||||
RULE_ADMIN_API = 'rule:admin_api'
|
||||
RULE_ANY = '@'
|
||||
|
||||
rules = [
|
||||
policy.RuleDefault(
|
||||
"context_is_admin",
|
||||
"role:admin",
|
||||
"Decides what is required for the 'is_admin:True' check to succeed."),
|
||||
policy.RuleDefault(
|
||||
"admin_or_owner",
|
||||
"is_admin:True or project_id:%(project_id)s",
|
||||
"Default rule for most non-Admin APIs."),
|
||||
policy.RuleDefault(
|
||||
"admin_api",
|
||||
"is_admin:True",
|
||||
"Default rule for most Admin APIs.")
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rules
|
53
masakari/policies/extension_info.py
Normal file
53
masakari/policies/extension_info.py
Normal file
@ -0,0 +1,53 @@
|
||||
# Copyright (C) 2018 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 masakari.policies import base
|
||||
|
||||
EXTENSIONS = 'os_masakari_api:extensions:%s'
|
||||
|
||||
rules = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=EXTENSIONS % 'index',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="List available extensions.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/extensions'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=EXTENSIONS % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Shows information for an extension.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/extensions/{extensions_id}'
|
||||
}
|
||||
]),
|
||||
policy.RuleDefault(
|
||||
name=EXTENSIONS % 'discoverable',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Extension Info API extensions to change the API.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rules
|
85
masakari/policies/hosts.py
Normal file
85
masakari/policies/hosts.py
Normal file
@ -0,0 +1,85 @@
|
||||
# Copyright (C) 2018 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 masakari.policies import base
|
||||
|
||||
|
||||
HOSTS = 'os_masakari_api:os-hosts:%s'
|
||||
|
||||
rules = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=HOSTS % 'index',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Lists IDs, names, type, reserved, on_maintenance for all"
|
||||
" hosts.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/segments/{segment_id}/hosts'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=HOSTS % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Shows details for a host.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/segments/{segment_id}/hosts/{host_id}'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=HOSTS % 'create',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Creates a host under given segment.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'POST',
|
||||
'path': '/segments/{segment_id}/hosts'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=HOSTS % 'update',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Updates the editable attributes of an existing host.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'PUT',
|
||||
'path': '/segments/{segment_id}/hosts/{host_id}'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=HOSTS % 'delete',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Deletes a host from given segment.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'DELETE',
|
||||
'path': '/segments/{segment_id}/hosts/{host_id}'
|
||||
}
|
||||
]),
|
||||
policy.RuleDefault(
|
||||
name=HOSTS % 'discoverable',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Host API extensions to change the API.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rules
|
65
masakari/policies/notifications.py
Normal file
65
masakari/policies/notifications.py
Normal file
@ -0,0 +1,65 @@
|
||||
# Copyright (C) 2018 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 masakari.policies import base
|
||||
|
||||
|
||||
NOTIFICATIONS = 'os_masakari_api:notifications:%s'
|
||||
|
||||
rules = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=NOTIFICATIONS % 'index',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Lists IDs, notification types, host_name, generated_time,"
|
||||
" payload and status for all notifications.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/notifications'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=NOTIFICATIONS % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Shows details for a notification.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/notifications/{notification_id}'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=NOTIFICATIONS % 'create',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Creates a notiification.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'POST',
|
||||
'path': '/notifications'
|
||||
}
|
||||
]),
|
||||
policy.RuleDefault(
|
||||
name=NOTIFICATIONS % 'discoverable',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Notification API extensions to change the API.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rules
|
85
masakari/policies/segments.py
Normal file
85
masakari/policies/segments.py
Normal file
@ -0,0 +1,85 @@
|
||||
# Copyright (C) 2018 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 masakari.policies import base
|
||||
|
||||
|
||||
SEGMENTS = 'os_masakari_api:segments:%s'
|
||||
|
||||
rules = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=SEGMENTS % 'index',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Lists IDs, names, description, recovery_method, "
|
||||
"service_type for all segments.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/segments'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=SEGMENTS % 'detail',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Shows details for a segment.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/segments/{segment_id}'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=SEGMENTS % 'create',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Creates a segment.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'POST',
|
||||
'path': '/segments'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=SEGMENTS % 'update',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Updates the editable attributes of an existing host.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'PUT',
|
||||
'path': '/segments/{segment_id}'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=SEGMENTS % 'delete',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Deletes a segment.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'DELETE',
|
||||
'path': '/segments/{segment_id}'
|
||||
}
|
||||
]),
|
||||
policy.RuleDefault(
|
||||
name=SEGMENTS % 'discoverable',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description="Segment API extensions to change the API.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rules
|
44
masakari/policies/versions.py
Normal file
44
masakari/policies/versions.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Copyright (C) 2018 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 masakari.policies import base
|
||||
|
||||
|
||||
VERSIONS = 'os_masakari_api:versions:%s'
|
||||
|
||||
rules = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=VERSIONS % 'index',
|
||||
check_str=base.RULE_ANY,
|
||||
description="List all versions.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/'
|
||||
}
|
||||
]),
|
||||
policy.RuleDefault(
|
||||
name=VERSIONS % 'discoverable',
|
||||
check_str=base.RULE_ANY,
|
||||
description="Version API extensions to change the API.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rules
|
@ -15,18 +15,27 @@
|
||||
|
||||
"""Policy Engine For Masakari."""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_policy import policy
|
||||
from oslo_utils import excutils
|
||||
|
||||
from masakari import exception
|
||||
# from masakari.i18n import _LE, _LW
|
||||
from masakari import policies
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
_ENFORCER = None
|
||||
# saved_file_rules and used to compare with new rules to determine the
|
||||
# rules whether were updated.
|
||||
saved_file_rules = []
|
||||
KEY_EXPR = re.compile(r'%\((\w+)\)s')
|
||||
|
||||
|
||||
def reset():
|
||||
@ -48,12 +57,44 @@ def init(policy_file=None, rules=None, default_rule=None, use_conf=True):
|
||||
:param use_conf: Whether to load rules from config file.
|
||||
"""
|
||||
global _ENFORCER
|
||||
global saved_file_rules
|
||||
if not _ENFORCER:
|
||||
_ENFORCER = policy.Enforcer(CONF,
|
||||
policy_file=policy_file,
|
||||
rules=rules,
|
||||
default_rule=default_rule,
|
||||
use_conf=use_conf)
|
||||
register_rules(_ENFORCER)
|
||||
_ENFORCER.load_rules()
|
||||
|
||||
# Only the rules which are loaded from file may be changed.
|
||||
current_file_rules = _ENFORCER.file_rules
|
||||
current_file_rules = _serialize_rules(current_file_rules)
|
||||
|
||||
# Checks whether the rules are updated in the runtime
|
||||
if saved_file_rules != current_file_rules:
|
||||
_warning_for_deprecated_user_based_rules(current_file_rules)
|
||||
saved_file_rules = copy.deepcopy(current_file_rules)
|
||||
|
||||
|
||||
def _serialize_rules(rules):
|
||||
"""Serialize all the Rule object as string which is used to compare the
|
||||
rules list.
|
||||
"""
|
||||
result = [(rule_name, str(rule))
|
||||
for rule_name, rule in rules.items()]
|
||||
return sorted(result, key=lambda rule: rule[0])
|
||||
|
||||
|
||||
def _warning_for_deprecated_user_based_rules(rules):
|
||||
"""Warning user based policy enforcement used in the rule but the rule
|
||||
doesn't support it.
|
||||
"""
|
||||
for rule in rules:
|
||||
if 'user_id' in KEY_EXPR.findall(rule[1]):
|
||||
LOG.debug(("The user_id attribute isn't supported in the rule%s'. "
|
||||
"All the user_id based policy enforcement will be "
|
||||
"removed in the future."), rule[0])
|
||||
|
||||
|
||||
def set_rules(rules, overwrite=True, use_conf=False):
|
||||
@ -69,34 +110,46 @@ def set_rules(rules, overwrite=True, use_conf=False):
|
||||
_ENFORCER.set_rules(rules, overwrite, use_conf)
|
||||
|
||||
|
||||
def enforce(context, action, target, do_raise=True, exc=None):
|
||||
def authorize(context, action, target, do_raise=True, exc=None):
|
||||
"""Verifies that the action is valid on the target in this context.
|
||||
|
||||
:param context: masakari context
|
||||
:param action: string representing the action to be checked
|
||||
this should be colon separated for clarity.
|
||||
i.e. ``os_masakari_api:segments``,
|
||||
``os_masakari_api:os-hosts``,
|
||||
``os_masakari_api:notifications``,
|
||||
``os_masakari_api:extensions``
|
||||
:param target: dictionary representing the object of the action
|
||||
for object creation this should be a dictionary representing the
|
||||
location of the object e.g. ``{'project_id': context.project_id}``
|
||||
:param do_raise: if True (the default), raises PolicyNotAuthorized;
|
||||
if False, returns False
|
||||
:param exc: Class of the exception to raise if the check fails.
|
||||
Any remaining arguments passed to :meth:`authorize` (both
|
||||
positional and keyword arguments) will be passed to
|
||||
the exception class. If not specified,
|
||||
:class:`PolicyNotAuthorized` will be used.
|
||||
|
||||
:raises masakari.exception.PolicyNotAuthorized: if verification fails
|
||||
and do_raise is True.
|
||||
and do_raise is True. Or if 'exc' is specified it will raise an
|
||||
exception of that type.
|
||||
|
||||
:return: returns a non-False value (not necessarily "True") if
|
||||
authorized, and the exact value False if not authorized and
|
||||
do_raise is False.
|
||||
"""
|
||||
init()
|
||||
credentials = context.to_dict()
|
||||
credentials = context.to_policy_values()
|
||||
if not exc:
|
||||
exc = exception.PolicyNotAuthorized
|
||||
try:
|
||||
result = _ENFORCER.enforce(action, target, credentials,
|
||||
do_raise=do_raise, exc=exc, action=action)
|
||||
result = _ENFORCER.authorize(action, target, credentials,
|
||||
do_raise=do_raise, exc=exc, action=action)
|
||||
except policy.PolicyNotRegistered:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.debug('Policy not registered')
|
||||
except Exception:
|
||||
credentials.pop('auth_token', None)
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.debug('Policy check for %(action)s failed with credentials '
|
||||
'%(credentials)s',
|
||||
@ -111,9 +164,9 @@ def check_is_admin(context):
|
||||
|
||||
init()
|
||||
# the target is user-self
|
||||
credentials = context.to_dict()
|
||||
credentials = context.to_policy_values()
|
||||
target = credentials
|
||||
return _ENFORCER.enforce('context_is_admin', target, credentials)
|
||||
return _ENFORCER.authorize('context_is_admin', target, credentials)
|
||||
|
||||
|
||||
@policy.register('is_admin')
|
||||
@ -136,3 +189,27 @@ class IsAdminCheck(policy.Check):
|
||||
def get_rules():
|
||||
if _ENFORCER:
|
||||
return _ENFORCER.rules
|
||||
|
||||
|
||||
def register_rules(enforcer):
|
||||
enforcer.register_defaults(policies.list_rules())
|
||||
|
||||
|
||||
def get_enforcer():
|
||||
# This method is for use by oslopolicy CLI scripts. Those scripts need the
|
||||
# 'output-file' and 'namespace' options, but having those in sys.argv means
|
||||
# loading the Masakari config options will fail as those are not expected
|
||||
# to be present. So we pass in an arg list with those stripped out.
|
||||
conf_args = []
|
||||
# Start at 1 because cfg.CONF expects the equivalent of sys.argv[1:]
|
||||
i = 1
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i].strip('-') in ['namespace', 'output-file']:
|
||||
i += 2
|
||||
continue
|
||||
conf_args.append(sys.argv[i])
|
||||
i += 1
|
||||
|
||||
cfg.CONF(conf_args, project='masakari')
|
||||
init()
|
||||
return _ENFORCER
|
||||
|
@ -64,14 +64,14 @@ class ExtensionInfoTest(test.NoDBTestCase):
|
||||
self.assertEqual(e['links'], [])
|
||||
self.assertEqual(6, len(e))
|
||||
|
||||
@mock.patch.object(policy, 'enforce', mock.Mock(return_value=True))
|
||||
@mock.patch.object(policy, 'authorize', mock.Mock(return_value=True))
|
||||
def test_extension_info_list(self):
|
||||
req = fakes.HTTPRequest.blank('/extensions')
|
||||
res_dict = self.controller.index(req)
|
||||
self.assertGreaterEqual(len(res_dict['extensions']), 3)
|
||||
self._filter_extensions(res_dict)
|
||||
|
||||
@mock.patch.object(policy, 'enforce', mock.Mock(return_value=True))
|
||||
@mock.patch.object(policy, 'authorize', mock.Mock(return_value=True))
|
||||
def test_extension_info_show(self):
|
||||
req = fakes.HTTPRequest.blank('/extensions/ext1-alias')
|
||||
res_dict = self.controller.show(req, 'ext1-alias')
|
||||
@ -86,7 +86,7 @@ class ExtensionInfoTest(test.NoDBTestCase):
|
||||
self.assertEqual(res_dict['extension']['links'], [])
|
||||
self.assertEqual(6, len(res_dict['extension']))
|
||||
|
||||
@mock.patch.object(policy, 'enforce')
|
||||
@mock.patch.object(policy, 'authorize')
|
||||
def test_extension_info_list_not_all_discoverable(self, mock_authorize):
|
||||
mock_authorize.side_effect = fake_policy_authorize_selective
|
||||
req = fakes.HTTPRequest.blank('/extensions')
|
||||
|
@ -480,25 +480,27 @@ class HostTestCasePolicyNotAuthorized(test.NoDBTestCase):
|
||||
self.req = fakes.HTTPRequest.blank(
|
||||
'/v1/segments/%s/hosts' % uuidsentinel.fake_segment1)
|
||||
self.context = self.req.environ['masakari.context']
|
||||
self.rule_name = "os_masakari_api:os-hosts"
|
||||
self.policy.set_rules({self.rule_name: "project:non_fake"})
|
||||
|
||||
def setUp(self):
|
||||
super(HostTestCasePolicyNotAuthorized, self).setUp()
|
||||
self._set_up()
|
||||
|
||||
def _check_rule(self, exc):
|
||||
def _check_rule(self, exc, rule_name):
|
||||
self.assertEqual(
|
||||
"Policy doesn't allow %s to be performed." % self.rule_name,
|
||||
"Policy doesn't allow %s to be performed." % rule_name,
|
||||
exc.format_message())
|
||||
|
||||
def test_index_no_admin(self):
|
||||
rule_name = "os_masakari_api:os-hosts:index"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.index,
|
||||
self.req, uuidsentinel.fake_segment1)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
||||
def test_create_no_admin(self):
|
||||
rule_name = "os_masakari_api:os-hosts:create"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
body = {
|
||||
"host": {
|
||||
"name": "host-1", "type": "fake", "reserved": False,
|
||||
@ -510,16 +512,20 @@ class HostTestCasePolicyNotAuthorized(test.NoDBTestCase):
|
||||
self.controller.create,
|
||||
self.req, uuidsentinel.fake_segment1,
|
||||
body=body)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
||||
def test_show_no_admin(self):
|
||||
rule_name = "os_masakari_api:os-hosts:detail"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.show,
|
||||
self.req, uuidsentinel.fake_segment1,
|
||||
uuidsentinel.fake_host_1)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
||||
def test_update_no_admin(self):
|
||||
rule_name = "os_masakari_api:os-hosts:update"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
body = {
|
||||
"host": {
|
||||
"name": "host-1", "type": "fake", "reserved": False,
|
||||
@ -531,11 +537,13 @@ class HostTestCasePolicyNotAuthorized(test.NoDBTestCase):
|
||||
self.controller.update,
|
||||
self.req, uuidsentinel.fake_segment1,
|
||||
uuidsentinel.fake_host_1, body=body)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
||||
def test_delete_no_admin(self):
|
||||
rule_name = "os_masakari_api:os-hosts:delete"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.delete,
|
||||
self.req, uuidsentinel.fake_segment1,
|
||||
uuidsentinel.fake_host_1)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
@ -338,15 +338,15 @@ class NotificationCasePolicyNotAuthorized(test.NoDBTestCase):
|
||||
self.controller = notifications.NotificationsController()
|
||||
self.req = fakes.HTTPRequest.blank('/v1/notifications')
|
||||
self.context = self.req.environ['masakari.context']
|
||||
self.rule_name = "os_masakari_api:notifications"
|
||||
self.policy.set_rules({self.rule_name: "project:non_fake"})
|
||||
|
||||
def _check_rule(self, exc):
|
||||
def _check_rule(self, exc, rule_name):
|
||||
self.assertEqual(
|
||||
"Policy doesn't allow %s to be performed." % self.rule_name,
|
||||
"Policy doesn't allow %s to be performed." % rule_name,
|
||||
exc.format_message())
|
||||
|
||||
def test_create_no_admin(self):
|
||||
rule_name = "os_masakari_api:notifications:create"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
body = {
|
||||
"notification": {"hostname": "fake_host",
|
||||
"payload": {"event": "STOPPED",
|
||||
@ -357,16 +357,20 @@ class NotificationCasePolicyNotAuthorized(test.NoDBTestCase):
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.create,
|
||||
self.req, body=body)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
||||
def test_show_no_admin(self):
|
||||
rule_name = "os_masakari_api:notifications:detail"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.show,
|
||||
self.req, uuidsentinel.fake_notification)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
||||
def test_index_no_admin(self):
|
||||
rule_name = "os_masakari_api:notifications:index"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.index,
|
||||
self.req)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
@ -322,21 +322,23 @@ class FailoverSegmentTestCasePolicyNotAuthorized(test.NoDBTestCase):
|
||||
self.controller = segments.SegmentsController()
|
||||
self.req = fakes.HTTPRequest.blank('/v1/segments')
|
||||
self.context = self.req.environ['masakari.context']
|
||||
self.rule_name = "os_masakari_api:segments"
|
||||
self.policy.set_rules({self.rule_name: "project:non_fake"})
|
||||
|
||||
def _check_rule(self, exc):
|
||||
def _check_rule(self, exc, rule_name):
|
||||
self.assertEqual(
|
||||
"Policy doesn't allow %s to be performed." % self.rule_name,
|
||||
"Policy doesn't allow %s to be performed." % rule_name,
|
||||
exc.format_message())
|
||||
|
||||
def test_index_no_admin(self):
|
||||
rule_name = "os_masakari_api:segments:index"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.index,
|
||||
self.req)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
||||
def test_create_no_admin(self):
|
||||
rule_name = "os_masakari_api:segments:create"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
body = {
|
||||
"segment": {
|
||||
"name": "segment1",
|
||||
@ -348,15 +350,19 @@ class FailoverSegmentTestCasePolicyNotAuthorized(test.NoDBTestCase):
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.create,
|
||||
self.req, body=body)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
||||
def test_show_no_admin(self):
|
||||
rule_name = "os_masakari_api:segments:detail"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.show,
|
||||
self.req, uuidsentinel.fake_segment)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
||||
def test_update_no_admin(self):
|
||||
rule_name = "os_masakari_api:segments:update"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
body = {
|
||||
"segment": {
|
||||
"name": "segment1",
|
||||
@ -368,10 +374,12 @@ class FailoverSegmentTestCasePolicyNotAuthorized(test.NoDBTestCase):
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.update,
|
||||
self.req, uuidsentinel.fake_segment, body=body)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
||||
def test_delete_no_admin(self):
|
||||
rule_name = "os_masakari_api:segments:delete"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
exc = self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.controller.delete,
|
||||
self.req, uuidsentinel.fake_segment)
|
||||
self._check_rule(exc)
|
||||
self._check_rule(exc, rule_name)
|
||||
|
@ -17,9 +17,20 @@ policy_data = """
|
||||
{
|
||||
"context_is_admin": "role:admin or role:administrator",
|
||||
|
||||
"os_masakari_api:extensions": "",
|
||||
"os_masakari_api:segments": "",
|
||||
"os_masakari_api:os-hosts": "",
|
||||
"os_masakari_api:notifications": ""
|
||||
"os_masakari_api:extensions:index": "",
|
||||
"os_masakari_api:extensions:detail": "",
|
||||
"os_masakari_api:segments:index": "",
|
||||
"os_masakari_api:segments:detail": "",
|
||||
"os_masakari_api:segments:create": "",
|
||||
"os_masakari_api:segments:update": "",
|
||||
"os_masakari_api:segments:delete": "",
|
||||
"os_masakari_api:os-hosts:index": "",
|
||||
"os_masakari_api:os-hosts:detail": "",
|
||||
"os_masakari_api:os-hosts:create": "",
|
||||
"os_masakari_api:os-hosts:update": "",
|
||||
"os_masakari_api:os-hosts:delete": "",
|
||||
"os_masakari_api:notifications:index": "",
|
||||
"os_masakari_api:notifications:detail": "",
|
||||
"os_masakari_api:notifications:create": ""
|
||||
}
|
||||
"""
|
||||
|
@ -20,6 +20,7 @@ from oslo_serialization import jsonutils
|
||||
|
||||
import masakari.conf
|
||||
from masakari.conf import paths
|
||||
from masakari import policies
|
||||
import masakari.policy
|
||||
from masakari.tests.unit import fake_policy
|
||||
|
||||
@ -52,9 +53,10 @@ class RealPolicyFixture(fixtures.Fixture):
|
||||
masakari.policy.init()
|
||||
self.addCleanup(masakari.policy.reset)
|
||||
|
||||
def set_rules(self, rules):
|
||||
def set_rules(self, rules, overwrite=True):
|
||||
policy = masakari.policy._ENFORCER
|
||||
policy.set_rules(oslo_policy.Rules.from_dict(rules))
|
||||
policy.set_rules(oslo_policy.Rules.from_dict(rules),
|
||||
overwrite=overwrite)
|
||||
|
||||
|
||||
class PolicyFixture(RealPolicyFixture):
|
||||
@ -91,13 +93,10 @@ class RoleBasedPolicyFixture(RealPolicyFixture):
|
||||
self.role = role
|
||||
|
||||
def _prepare_policy(self):
|
||||
with open(CONF.oslo_policy.policy_file) as fp:
|
||||
policy = fp.read()
|
||||
policy = jsonutils.loads(policy)
|
||||
|
||||
# Convert all actions to require specified role
|
||||
for action, rule in policy.items():
|
||||
policy[action] = 'role:%s' % self.role
|
||||
# Convert all actions to require the specified role
|
||||
policy = {}
|
||||
for rule in policies.list_rules():
|
||||
policy[rule.name] = 'role:%s' % self.role
|
||||
|
||||
self.policy_dir = self.useFixture(fixtures.TempDir())
|
||||
self.policy_file = os.path.join(self.policy_dir.path, 'policy.json')
|
||||
|
@ -457,3 +457,39 @@ class HackingTestCase(test.NoDBTestCase):
|
||||
yieldx_func(a, b)
|
||||
"""
|
||||
self._assert_has_no_errors(code, checks.yield_followed_by_space)
|
||||
|
||||
def test_check_policy_registration_in_central_place(self):
|
||||
errors = [(3, 0, "M333")]
|
||||
code = """
|
||||
from masakari import policy
|
||||
|
||||
policy.RuleDefault('context_is_admin', 'role:admin')
|
||||
"""
|
||||
# registration in the proper place
|
||||
self._assert_has_no_errors(
|
||||
code, checks.check_policy_registration_in_central_place,
|
||||
filename="masakari/policies/base.py")
|
||||
# option at a location which is not in scope right now
|
||||
self._assert_has_errors(
|
||||
code, checks.check_policy_registration_in_central_place,
|
||||
filename="masakari/api/openstack/ha/non_existent.py",
|
||||
expected_errors=errors)
|
||||
|
||||
def test_check_policy_enforce(self):
|
||||
errors = [(3, 0, "M334")]
|
||||
code = """
|
||||
from masakari import policy
|
||||
|
||||
policy._ENFORCER.enforce('context_is_admin', target, credentials)
|
||||
"""
|
||||
self._assert_has_errors(code, checks.check_policy_enforce,
|
||||
expected_errors=errors)
|
||||
|
||||
def test_check_policy_enforce_does_not_catch_other_enforce(self):
|
||||
# Simulate a different enforce method defined in masakari
|
||||
code = """
|
||||
from masakari import foo
|
||||
|
||||
foo.enforce()
|
||||
"""
|
||||
self._assert_has_no_errors(code, checks.check_policy_enforce)
|
||||
|
@ -56,11 +56,11 @@ class PolicyFileTestCase(test.NoDBTestCase):
|
||||
action = "example:test"
|
||||
with open(tmpfilename, "w") as policyfile:
|
||||
policyfile.write('{"example:test": ""}')
|
||||
policy.enforce(self.context, action, self.target)
|
||||
policy.authorize(self.context, action, self.target)
|
||||
with open(tmpfilename, "w") as policyfile:
|
||||
policyfile.write('{"example:test": "!"}')
|
||||
policy._ENFORCER.load_rules(True)
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.enforce,
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
|
||||
self.context, action, self.target)
|
||||
|
||||
|
||||
@ -90,39 +90,70 @@ class PolicyTestCase(test.NoDBTestCase):
|
||||
self.context = context.RequestContext('fake', 'fake', roles=['member'])
|
||||
self.target = {}
|
||||
|
||||
def test_enforce_bad_action_throws(self):
|
||||
action = "example:denied"
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.enforce,
|
||||
def test_authorize_nonexistent_action_throws(self):
|
||||
action = "example:noexist"
|
||||
self.assertRaises(oslo_policy.PolicyNotRegistered, policy.authorize,
|
||||
self.context, action, self.target)
|
||||
|
||||
def test_enforce_bad_action_noraise(self):
|
||||
def test_authorize_bad_action_throws(self):
|
||||
action = "example:denied"
|
||||
result = policy.enforce(self.context, action, self.target, False)
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
|
||||
self.context, action, self.target)
|
||||
|
||||
def test_authorize_bad_action_noraise(self):
|
||||
action = "example:denied"
|
||||
result = policy.authorize(self.context, action, self.target, False)
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_enforce_good_action(self):
|
||||
def test_authorize_good_action(self):
|
||||
action = "example:allowed"
|
||||
result = policy.enforce(self.context, action, self.target)
|
||||
result = policy.authorize(self.context, action, self.target)
|
||||
self.assertTrue(result)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_enforce_http_true(self, req_mock):
|
||||
def test_authorize_http_true(self, req_mock):
|
||||
req_mock.post('http://www.example.com/',
|
||||
text='True')
|
||||
action = "example:get_http"
|
||||
target = {}
|
||||
result = policy.enforce(self.context, action, target)
|
||||
result = policy.authorize(self.context, action, target)
|
||||
self.assertTrue(result)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_enforce_http_false(self, req_mock):
|
||||
def test_authorize_http_false(self, req_mock):
|
||||
req_mock.post('http://www.example.com/',
|
||||
text='False')
|
||||
action = "example:get_http"
|
||||
target = {}
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.enforce,
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
|
||||
self.context, action, target)
|
||||
|
||||
def test_templatized_authorization(self):
|
||||
target_mine = {'project_id': 'fake'}
|
||||
target_not_mine = {'project_id': 'another'}
|
||||
action = "example:my_file"
|
||||
policy.authorize(self.context, action, target_mine)
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
|
||||
self.context, action, target_not_mine)
|
||||
|
||||
def test_early_AND_authorization(self):
|
||||
action = "example:early_and_fail"
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
|
||||
self.context, action, self.target)
|
||||
|
||||
def test_early_OR_authorization(self):
|
||||
action = "example:early_or_success"
|
||||
policy.authorize(self.context, action, self.target)
|
||||
|
||||
def test_ignore_case_role_check(self):
|
||||
lowercase_action = "example:lowercase_admin"
|
||||
uppercase_action = "example:uppercase_admin"
|
||||
admin_context = context.RequestContext('admin',
|
||||
'fake',
|
||||
roles=['AdMiN'])
|
||||
policy.authorize(admin_context, lowercase_action, self.target)
|
||||
policy.authorize(admin_context, uppercase_action, self.target)
|
||||
|
||||
|
||||
class IsAdminCheckTestCase(test.NoDBTestCase):
|
||||
def setUp(self):
|
||||
@ -160,6 +191,23 @@ class IsAdminCheckTestCase(test.NoDBTestCase):
|
||||
policy._ENFORCER), True)
|
||||
|
||||
|
||||
class AdminRolePolicyTestCase(test.NoDBTestCase):
|
||||
def setUp(self):
|
||||
super(AdminRolePolicyTestCase, self).setUp()
|
||||
self.policy = self.useFixture(policy_fixture.RoleBasedPolicyFixture())
|
||||
self.context = context.RequestContext('fake', 'fake', roles=['member'])
|
||||
self.actions = policy.get_rules().keys()
|
||||
self.target = {}
|
||||
|
||||
def test_authorize_admin_actions_with_nonadmin_context_throws(self):
|
||||
"""Check if non-admin context passed to admin actions throws
|
||||
Policy not authorized exception
|
||||
"""
|
||||
for action in self.actions:
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
|
||||
self.context, action, self.target)
|
||||
|
||||
|
||||
class RealRolePolicyTestCase(test.NoDBTestCase):
|
||||
def setUp(self):
|
||||
super(RealRolePolicyTestCase, self).setUp()
|
||||
@ -172,10 +220,21 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
|
||||
self.fake_policy = jsonutils.loads(fake_policy.policy_data)
|
||||
|
||||
self.admin_only_rules = (
|
||||
"os_masakari_api:extensions",
|
||||
"os_masakari_api:os-hosts",
|
||||
"os_masakari_api:segments",
|
||||
"os_masakari_api:notifications"
|
||||
"os_masakari_api:extensions:index",
|
||||
"os_masakari_api:extensions:detail",
|
||||
"os_masakari_api:os-hosts:index",
|
||||
"os_masakari_api:os-hosts:detail",
|
||||
"os_masakari_api:os-hosts:create",
|
||||
"os_masakari_api:os-hosts:update",
|
||||
"os_masakari_api:os-hosts:delete",
|
||||
"os_masakari_api:segments:index",
|
||||
"os_masakari_api:segments:detail",
|
||||
"os_masakari_api:segments:create",
|
||||
"os_masakari_api:segments:update",
|
||||
"os_masakari_api:segments:delete",
|
||||
"os_masakari_api:notifications:index",
|
||||
"os_masakari_api:notifications:detail",
|
||||
"os_masakari_api:notifications:create"
|
||||
)
|
||||
|
||||
def test_all_rules_in_sample_file(self):
|
||||
@ -187,7 +246,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
|
||||
|
||||
def test_admin_only_rules(self):
|
||||
for rule in self.admin_only_rules:
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.enforce,
|
||||
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
|
||||
self.non_admin_context, rule,
|
||||
{'project_id': 'fake', 'user_id': 'fake'})
|
||||
policy.enforce(self.admin_context, rule, self.target)
|
||||
policy.authorize(self.admin_context, rule, self.target)
|
||||
|
22
releasenotes/notes/policy-in-code-8740d51624055044.yaml
Normal file
22
releasenotes/notes/policy-in-code-8740d51624055044.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Masakari now support policy in code, which means if operators doesn't need to
|
||||
modify any of the default policy rules, they do not need a policy file.
|
||||
Operators can modify/generate a ``policy.yaml.sample`` file which will override
|
||||
specific policy rules from their defaults.
|
||||
|
||||
Masakari is now configured to work with two oslo.policy CLI scripts that
|
||||
have been added:
|
||||
|
||||
- The first of these can be called like
|
||||
``oslopolicy-list-redundant --namespace masakari`` and will output a list of
|
||||
policy rules in policy.[json|yaml] that match the project defaults. These
|
||||
rules can be removed from the policy file as they have no effect there.
|
||||
- The second script can be called like
|
||||
``oslopolicy-policy-generator --namespace masakari --output-file policy-merged.yaml``
|
||||
and will populate the policy-merged.yaml file with the effective policy.
|
||||
This is the merged results of project defaults and config file overrides.
|
||||
|
||||
NOTE: Default `policy.json` file is now removed as Masakari now uses default
|
||||
policies. A policy file is only needed if overriding one of the defaults.
|
10
setup.cfg
10
setup.cfg
@ -33,6 +33,16 @@ oslo.config.opts =
|
||||
oslo.config.opts.defaults =
|
||||
masakari.api = masakari.common.config:set_middleware_defaults
|
||||
|
||||
oslo.policy.enforcer =
|
||||
masakari = masakari.policy:get_enforcer
|
||||
|
||||
oslo.policy.policies =
|
||||
# The sample policies will be ordered by entry point and then by list
|
||||
# returned from that entry point. If more control is desired split out each
|
||||
# list_rules method into a separate entry point rather than using the
|
||||
# aggregate method.
|
||||
masakari = masakari.policies:list_rules
|
||||
|
||||
console_scripts =
|
||||
masakari-api = masakari.cmd.api:main
|
||||
masakari-engine = masakari.cmd.engine:main
|
||||
|
3
tox.ini
3
tox.ini
@ -38,6 +38,9 @@ commands =
|
||||
basepython = python3
|
||||
commands = oslo-config-generator --config-file=etc/masakari/masakari-config-generator.conf
|
||||
|
||||
[testenv:genpolicy]
|
||||
commands = oslopolicy-sample-generator --config-file=etc/masakari/masakari-policy-generator.conf
|
||||
|
||||
[testenv:pep8]
|
||||
basepython = python3
|
||||
commands = flake8 {posargs}
|
||||
|
Loading…
Reference in New Issue
Block a user