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:
shilpa.devharakar 2018-04-24 15:50:03 +05:30
parent b6a6e3cbf3
commit d7592cbe25
33 changed files with 906 additions and 152 deletions

4
.gitignore vendored
View File

@ -60,3 +60,7 @@ releasenotes/build
# PyCharm IDE # PyCharm IDE
.idea/ .idea/
# policy sample generation
etc/masakari/policy.yaml.sample

View File

@ -43,3 +43,5 @@ Masakari Specific Commandments
- [M329] Deprecated library function os.popen() - [M329] Deprecated library function os.popen()
- [M331] LOG.warn is deprecated. Enforce use of LOG.warning. - [M331] LOG.warn is deprecated. Enforce use of LOG.warning.
- [M332] Yield must always be followed by a space when yielding a value. - [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.

View File

@ -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"
}

View 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": "@"

View File

@ -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 as per his requirement. It is recommended that all api's of masakari should
be allowed to admin user only. be allowed to admin user only.
.. literalinclude:: _static/masakari.policy.json.sample .. literalinclude:: _static/masakari.policy.yaml.sample

View File

@ -0,0 +1,3 @@
[DEFAULT]
output_file = etc/masakari/policy.yaml.sample
namespace = masakari

View File

@ -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"
}

View File

@ -22,11 +22,9 @@ import six
import webob.dec import webob.dec
import webob.exc import webob.exc
import masakari.api.openstack
from masakari.api.openstack import wsgi from masakari.api.openstack import wsgi
from masakari import exception from masakari import exception
from masakari.i18n import _ from masakari.i18n import _
import masakari.policy
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -316,45 +314,6 @@ def load_standard_extensions(ext_mgr, logger, path, package, ext_list=None):
dirnames[:] = subdirs 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) @six.add_metaclass(abc.ABCMeta)
class V1APIExtensionBase(object): class V1APIExtensionBase(object):
"""Abstract base class for all v1 API extensions. """Abstract base class for all v1 API extensions.

View File

@ -19,10 +19,11 @@ import webob.exc
from masakari.api.openstack import extensions from masakari.api.openstack import extensions
from masakari.api.openstack import wsgi from masakari.api.openstack import wsgi
from masakari import exception from masakari import exception
from masakari.policies import base as base_policies
from masakari.policies import extension_info as extension_policies
ALIAS = 'extensions' ALIAS = 'extensions'
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
authorize = extensions.os_masakari_authorizer(ALIAS)
class FakeExtension(object): class FakeExtension(object):
@ -53,10 +54,10 @@ class ExtensionInfoController(wsgi.Controller):
"""Filter extensions list based on policy.""" """Filter extensions list based on policy."""
discoverable_extensions = dict() discoverable_extensions = dict()
for alias, ext in self.extension_info.get_extensions().items(): for alias, ext in self.extension_info.get_extensions().items():
authorize = extensions.os_masakari_soft_authorizer(alias) action = ':'.join([
if authorize(context, action='discoverable'): base_policies.MASAKARI_API, alias, 'discoverable'])
if context.can(action, fatal=False):
discoverable_extensions[alias] = ext discoverable_extensions[alias] = ext
else: else:
LOG.debug("Filter out extension %s from discover list", LOG.debug("Filter out extension %s from discover list",
@ -67,7 +68,7 @@ class ExtensionInfoController(wsgi.Controller):
@extensions.expected_errors(()) @extensions.expected_errors(())
def index(self, req): def index(self, req):
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(extension_policies.EXTENSIONS % 'index')
discoverable_extensions = self._get_extensions(context) discoverable_extensions = self._get_extensions(context)
sorted_ext_list = sorted(discoverable_extensions.items()) sorted_ext_list = sorted(discoverable_extensions.items())
@ -80,7 +81,7 @@ class ExtensionInfoController(wsgi.Controller):
@extensions.expected_errors(http_client.NOT_FOUND) @extensions.expected_errors(http_client.NOT_FOUND)
def show(self, req, id): def show(self, req, id):
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(extension_policies.EXTENSIONS % 'detail')
try: try:
ext = self._get_extensions(context)[id] ext = self._get_extensions(context)[id]
except KeyError: except KeyError:

View File

@ -29,9 +29,9 @@ from masakari import exception
from masakari.ha import api as host_api from masakari.ha import api as host_api
from masakari.i18n import _ from masakari.i18n import _
from masakari import objects from masakari import objects
from masakari.policies import hosts as host_policies
ALIAS = "os-hosts" ALIAS = "os-hosts"
authorize = extensions.os_masakari_authorizer(ALIAS)
class HostsController(wsgi.Controller): class HostsController(wsgi.Controller):
@ -45,7 +45,7 @@ class HostsController(wsgi.Controller):
def index(self, req, segment_id): def index(self, req, segment_id):
"""Returns a list a hosts.""" """Returns a list a hosts."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(host_policies.HOSTS % 'index')
try: try:
filters = {} filters = {}
@ -103,7 +103,7 @@ class HostsController(wsgi.Controller):
def create(self, req, segment_id, body): def create(self, req, segment_id, body):
"""Creates a host.""" """Creates a host."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(host_policies.HOSTS % 'create')
host_data = body.get('host') host_data = body.get('host')
try: try:
host = self.api.create_host(context, segment_id, host_data) 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): def show(self, req, segment_id, id):
"""Shows the details of a host.""" """Shows the details of a host."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(host_policies.HOSTS % 'detail')
try: try:
host = self.api.get_host(context, segment_id, id) host = self.api.get_host(context, segment_id, id)
except exception.HostNotFound as e: except exception.HostNotFound as e:
@ -133,7 +133,7 @@ class HostsController(wsgi.Controller):
def update(self, req, segment_id, id, body): def update(self, req, segment_id, id, body):
"""Updates the existing host.""" """Updates the existing host."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(host_policies.HOSTS % 'update')
host_data = body.get('host') host_data = body.get('host')
try: try:
host = self.api.update_host(context, segment_id, id, host_data) 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): def delete(self, req, segment_id, id):
"""Removes a host by id.""" """Removes a host by id."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(host_policies.HOSTS % 'delete')
try: try:
self.api.delete_host(context, segment_id, id) self.api.delete_host(context, segment_id, id)
except exception.FailoverSegmentNotFound as e: except exception.FailoverSegmentNotFound as e:

View File

@ -25,9 +25,9 @@ from masakari.api import validation
from masakari import exception from masakari import exception
from masakari.ha import api as notification_api from masakari.ha import api as notification_api
from masakari.i18n import _ from masakari.i18n import _
from masakari.policies import notifications as notifications_policies
ALIAS = 'notifications' ALIAS = 'notifications'
authorize = extensions.os_masakari_authorizer(ALIAS)
class NotificationsController(wsgi.Controller): class NotificationsController(wsgi.Controller):
@ -43,7 +43,7 @@ class NotificationsController(wsgi.Controller):
def create(self, req, body): def create(self, req, body):
"""Creates a new notification.""" """Creates a new notification."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(notifications_policies.NOTIFICATIONS % 'create')
notification_data = body['notification'] notification_data = body['notification']
try: try:
@ -61,7 +61,7 @@ class NotificationsController(wsgi.Controller):
def index(self, req): def index(self, req):
"""Returns a summary list of notifications.""" """Returns a summary list of notifications."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(notifications_policies.NOTIFICATIONS % 'index')
try: try:
limit, marker = common.get_limit_and_marker(req) limit, marker = common.get_limit_and_marker(req)
sort_keys, sort_dirs = common.get_sort_params(req.params) sort_keys, sort_dirs = common.get_sort_params(req.params)
@ -95,7 +95,7 @@ class NotificationsController(wsgi.Controller):
def show(self, req, id): def show(self, req, id):
"""Return data about the given notification id.""" """Return data about the given notification id."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(notifications_policies.NOTIFICATIONS % 'detail')
try: try:
notification = self.api.get_notification(context, id) notification = self.api.get_notification(context, id)

View File

@ -23,10 +23,10 @@ from masakari.api.openstack import wsgi
from masakari.api import validation from masakari.api import validation
from masakari import exception from masakari import exception
from masakari.ha import api as segment_api from masakari.ha import api as segment_api
from masakari.policies import segments as segment_policies
ALIAS = 'segments' ALIAS = 'segments'
authorize = extensions.os_masakari_authorizer(ALIAS)
class SegmentsController(wsgi.Controller): class SegmentsController(wsgi.Controller):
@ -39,7 +39,7 @@ class SegmentsController(wsgi.Controller):
def index(self, req): def index(self, req):
"""Returns a summary list of failover segments.""" """Returns a summary list of failover segments."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(segment_policies.SEGMENTS % 'index')
try: try:
limit, marker = common.get_limit_and_marker(req) limit, marker = common.get_limit_and_marker(req)
@ -66,7 +66,7 @@ class SegmentsController(wsgi.Controller):
def show(self, req, id): def show(self, req, id):
"""Return data about the given segment id.""" """Return data about the given segment id."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(segment_policies.SEGMENTS % 'detail')
try: try:
segment = self.api.get_segment(context, id) segment = self.api.get_segment(context, id)
@ -80,7 +80,7 @@ class SegmentsController(wsgi.Controller):
def create(self, req, body): def create(self, req, body):
"""Creates a new failover segment.""" """Creates a new failover segment."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(segment_policies.SEGMENTS % 'create')
segment_data = body['segment'] segment_data = body['segment']
try: try:
@ -95,7 +95,7 @@ class SegmentsController(wsgi.Controller):
def update(self, req, id, body): def update(self, req, id, body):
"""Updates the existing segment.""" """Updates the existing segment."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(segment_policies.SEGMENTS % 'update')
segment_data = body['segment'] segment_data = body['segment']
try: try:
@ -113,7 +113,7 @@ class SegmentsController(wsgi.Controller):
def delete(self, req, id): def delete(self, req, id):
"""Removes a segment by uuid.""" """Removes a segment by uuid."""
context = req.environ['masakari.context'] context = req.environ['masakari.context']
authorize(context) context.can(segment_policies.SEGMENTS % 'delete')
try: try:
self.api.delete_segment(context, id) self.api.delete_segment(context, id)

View File

@ -27,6 +27,7 @@ from oslo_log import log as logging
from oslo_utils import timeutils from oslo_utils import timeutils
import six import six
from masakari import exception
from masakari.i18n import _ from masakari.i18n import _
from masakari import policy from masakari import policy
from masakari import utils from masakari import utils
@ -202,6 +203,39 @@ class RequestContext(context.RequestContext):
return context 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): def __str__(self):
return "<Context %s>" % self.to_dict() return "<Context %s>" % self.to_dict()

View File

@ -35,6 +35,8 @@ UNDERSCORE_IMPORT_FILES = []
session_check = re.compile(r"\w*def [a-zA-Z0-9].*[(].*session.*[)]") session_check = re.compile(r"\w*def [a-zA-Z0-9].*[(].*session.*[)]")
cfg_re = re.compile(r".*\scfg\.") cfg_re = re.compile(r".*\scfg\.")
cfg_opt_re = re.compile(r".*[\s\[]cfg\.[a-zA-Z]*Opt\(") 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( asse_trueinst_re = re.compile(
r"(.)*assertTrue\(isinstance\((\w|\.|\'|\"|\[|\])+, " r"(.)*assertTrue\(isinstance\((\w|\.|\'|\"|\[|\])+, "
"(\w|\.|\'|\"|\[|\])+\)\)") "(\w|\.|\'|\"|\[|\])+\)\)")
@ -412,6 +414,38 @@ def yield_followed_by_space(logical_line):
"M332: Yield keyword should be followed by a space.") "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): def factory(register):
register(no_db_session_in_public_api) register(no_db_session_in_public_api)
register(use_timeutils_utcnow) register(use_timeutils_utcnow)
@ -438,3 +472,5 @@ def factory(register):
register(no_os_popen) register(no_os_popen)
register(no_log_warn) register(no_log_warn)
register(yield_followed_by_space) register(yield_followed_by_space)
register(check_policy_registration_in_central_place)
register(check_policy_enforce)

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -15,18 +15,27 @@
"""Policy Engine For Masakari.""" """Policy Engine For Masakari."""
import copy
import logging import logging
import re
import sys
from oslo_config import cfg from oslo_config import cfg
from oslo_policy import policy from oslo_policy import policy
from oslo_utils import excutils from oslo_utils import excutils
from masakari import exception from masakari import exception
# from masakari.i18n import _LE, _LW
from masakari import policies
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
_ENFORCER = None _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(): 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. :param use_conf: Whether to load rules from config file.
""" """
global _ENFORCER global _ENFORCER
global saved_file_rules
if not _ENFORCER: if not _ENFORCER:
_ENFORCER = policy.Enforcer(CONF, _ENFORCER = policy.Enforcer(CONF,
policy_file=policy_file, policy_file=policy_file,
rules=rules, rules=rules,
default_rule=default_rule, default_rule=default_rule,
use_conf=use_conf) 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): 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) _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. """Verifies that the action is valid on the target in this context.
:param context: masakari context :param context: masakari context
:param action: string representing the action to be checked :param action: string representing the action to be checked
this should be colon separated for clarity. 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 :param target: dictionary representing the object of the action
for object creation this should be a dictionary representing the for object creation this should be a dictionary representing the
location of the object e.g. ``{'project_id': context.project_id}`` location of the object e.g. ``{'project_id': context.project_id}``
:param do_raise: if True (the default), raises PolicyNotAuthorized; :param do_raise: if True (the default), raises PolicyNotAuthorized;
if False, returns False 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 :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 :return: returns a non-False value (not necessarily "True") if
authorized, and the exact value False if not authorized and authorized, and the exact value False if not authorized and
do_raise is False. do_raise is False.
""" """
init() init()
credentials = context.to_dict() credentials = context.to_policy_values()
if not exc: if not exc:
exc = exception.PolicyNotAuthorized exc = exception.PolicyNotAuthorized
try: try:
result = _ENFORCER.enforce(action, target, credentials, result = _ENFORCER.authorize(action, target, credentials,
do_raise=do_raise, exc=exc, action=action) do_raise=do_raise, exc=exc, action=action)
except policy.PolicyNotRegistered:
with excutils.save_and_reraise_exception():
LOG.debug('Policy not registered')
except Exception: except Exception:
credentials.pop('auth_token', None)
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
LOG.debug('Policy check for %(action)s failed with credentials ' LOG.debug('Policy check for %(action)s failed with credentials '
'%(credentials)s', '%(credentials)s',
@ -111,9 +164,9 @@ def check_is_admin(context):
init() init()
# the target is user-self # the target is user-self
credentials = context.to_dict() credentials = context.to_policy_values()
target = credentials target = credentials
return _ENFORCER.enforce('context_is_admin', target, credentials) return _ENFORCER.authorize('context_is_admin', target, credentials)
@policy.register('is_admin') @policy.register('is_admin')
@ -136,3 +189,27 @@ class IsAdminCheck(policy.Check):
def get_rules(): def get_rules():
if _ENFORCER: if _ENFORCER:
return _ENFORCER.rules 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

View File

@ -64,14 +64,14 @@ class ExtensionInfoTest(test.NoDBTestCase):
self.assertEqual(e['links'], []) self.assertEqual(e['links'], [])
self.assertEqual(6, len(e)) 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): def test_extension_info_list(self):
req = fakes.HTTPRequest.blank('/extensions') req = fakes.HTTPRequest.blank('/extensions')
res_dict = self.controller.index(req) res_dict = self.controller.index(req)
self.assertGreaterEqual(len(res_dict['extensions']), 3) self.assertGreaterEqual(len(res_dict['extensions']), 3)
self._filter_extensions(res_dict) 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): def test_extension_info_show(self):
req = fakes.HTTPRequest.blank('/extensions/ext1-alias') req = fakes.HTTPRequest.blank('/extensions/ext1-alias')
res_dict = self.controller.show(req, '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(res_dict['extension']['links'], [])
self.assertEqual(6, len(res_dict['extension'])) 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): def test_extension_info_list_not_all_discoverable(self, mock_authorize):
mock_authorize.side_effect = fake_policy_authorize_selective mock_authorize.side_effect = fake_policy_authorize_selective
req = fakes.HTTPRequest.blank('/extensions') req = fakes.HTTPRequest.blank('/extensions')

View File

@ -480,25 +480,27 @@ class HostTestCasePolicyNotAuthorized(test.NoDBTestCase):
self.req = fakes.HTTPRequest.blank( self.req = fakes.HTTPRequest.blank(
'/v1/segments/%s/hosts' % uuidsentinel.fake_segment1) '/v1/segments/%s/hosts' % uuidsentinel.fake_segment1)
self.context = self.req.environ['masakari.context'] 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): def setUp(self):
super(HostTestCasePolicyNotAuthorized, self).setUp() super(HostTestCasePolicyNotAuthorized, self).setUp()
self._set_up() self._set_up()
def _check_rule(self, exc): def _check_rule(self, exc, rule_name):
self.assertEqual( 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()) exc.format_message())
def test_index_no_admin(self): 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, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.index, self.controller.index,
self.req, uuidsentinel.fake_segment1) self.req, uuidsentinel.fake_segment1)
self._check_rule(exc) self._check_rule(exc, rule_name)
def test_create_no_admin(self): def test_create_no_admin(self):
rule_name = "os_masakari_api:os-hosts:create"
self.policy.set_rules({rule_name: "project:non_fake"})
body = { body = {
"host": { "host": {
"name": "host-1", "type": "fake", "reserved": False, "name": "host-1", "type": "fake", "reserved": False,
@ -510,16 +512,20 @@ class HostTestCasePolicyNotAuthorized(test.NoDBTestCase):
self.controller.create, self.controller.create,
self.req, uuidsentinel.fake_segment1, self.req, uuidsentinel.fake_segment1,
body=body) body=body)
self._check_rule(exc) self._check_rule(exc, rule_name)
def test_show_no_admin(self): 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, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.show, self.controller.show,
self.req, uuidsentinel.fake_segment1, self.req, uuidsentinel.fake_segment1,
uuidsentinel.fake_host_1) uuidsentinel.fake_host_1)
self._check_rule(exc) self._check_rule(exc, rule_name)
def test_update_no_admin(self): def test_update_no_admin(self):
rule_name = "os_masakari_api:os-hosts:update"
self.policy.set_rules({rule_name: "project:non_fake"})
body = { body = {
"host": { "host": {
"name": "host-1", "type": "fake", "reserved": False, "name": "host-1", "type": "fake", "reserved": False,
@ -531,11 +537,13 @@ class HostTestCasePolicyNotAuthorized(test.NoDBTestCase):
self.controller.update, self.controller.update,
self.req, uuidsentinel.fake_segment1, self.req, uuidsentinel.fake_segment1,
uuidsentinel.fake_host_1, body=body) uuidsentinel.fake_host_1, body=body)
self._check_rule(exc) self._check_rule(exc, rule_name)
def test_delete_no_admin(self): 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, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.delete, self.controller.delete,
self.req, uuidsentinel.fake_segment1, self.req, uuidsentinel.fake_segment1,
uuidsentinel.fake_host_1) uuidsentinel.fake_host_1)
self._check_rule(exc) self._check_rule(exc, rule_name)

View File

@ -338,15 +338,15 @@ class NotificationCasePolicyNotAuthorized(test.NoDBTestCase):
self.controller = notifications.NotificationsController() self.controller = notifications.NotificationsController()
self.req = fakes.HTTPRequest.blank('/v1/notifications') self.req = fakes.HTTPRequest.blank('/v1/notifications')
self.context = self.req.environ['masakari.context'] 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( 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()) exc.format_message())
def test_create_no_admin(self): def test_create_no_admin(self):
rule_name = "os_masakari_api:notifications:create"
self.policy.set_rules({rule_name: "project:non_fake"})
body = { body = {
"notification": {"hostname": "fake_host", "notification": {"hostname": "fake_host",
"payload": {"event": "STOPPED", "payload": {"event": "STOPPED",
@ -357,16 +357,20 @@ class NotificationCasePolicyNotAuthorized(test.NoDBTestCase):
exc = self.assertRaises(exception.PolicyNotAuthorized, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.create, self.controller.create,
self.req, body=body) self.req, body=body)
self._check_rule(exc) self._check_rule(exc, rule_name)
def test_show_no_admin(self): 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, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.show, self.controller.show,
self.req, uuidsentinel.fake_notification) self.req, uuidsentinel.fake_notification)
self._check_rule(exc) self._check_rule(exc, rule_name)
def test_index_no_admin(self): 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, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.index, self.controller.index,
self.req) self.req)
self._check_rule(exc) self._check_rule(exc, rule_name)

View File

@ -322,21 +322,23 @@ class FailoverSegmentTestCasePolicyNotAuthorized(test.NoDBTestCase):
self.controller = segments.SegmentsController() self.controller = segments.SegmentsController()
self.req = fakes.HTTPRequest.blank('/v1/segments') self.req = fakes.HTTPRequest.blank('/v1/segments')
self.context = self.req.environ['masakari.context'] 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( 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()) exc.format_message())
def test_index_no_admin(self): 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, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.index, self.controller.index,
self.req) self.req)
self._check_rule(exc) self._check_rule(exc, rule_name)
def test_create_no_admin(self): def test_create_no_admin(self):
rule_name = "os_masakari_api:segments:create"
self.policy.set_rules({rule_name: "project:non_fake"})
body = { body = {
"segment": { "segment": {
"name": "segment1", "name": "segment1",
@ -348,15 +350,19 @@ class FailoverSegmentTestCasePolicyNotAuthorized(test.NoDBTestCase):
exc = self.assertRaises(exception.PolicyNotAuthorized, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.create, self.controller.create,
self.req, body=body) self.req, body=body)
self._check_rule(exc) self._check_rule(exc, rule_name)
def test_show_no_admin(self): 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, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.show, self.controller.show,
self.req, uuidsentinel.fake_segment) self.req, uuidsentinel.fake_segment)
self._check_rule(exc) self._check_rule(exc, rule_name)
def test_update_no_admin(self): def test_update_no_admin(self):
rule_name = "os_masakari_api:segments:update"
self.policy.set_rules({rule_name: "project:non_fake"})
body = { body = {
"segment": { "segment": {
"name": "segment1", "name": "segment1",
@ -368,10 +374,12 @@ class FailoverSegmentTestCasePolicyNotAuthorized(test.NoDBTestCase):
exc = self.assertRaises(exception.PolicyNotAuthorized, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.update, self.controller.update,
self.req, uuidsentinel.fake_segment, body=body) self.req, uuidsentinel.fake_segment, body=body)
self._check_rule(exc) self._check_rule(exc, rule_name)
def test_delete_no_admin(self): 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, exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.delete, self.controller.delete,
self.req, uuidsentinel.fake_segment) self.req, uuidsentinel.fake_segment)
self._check_rule(exc) self._check_rule(exc, rule_name)

View File

@ -17,9 +17,20 @@ policy_data = """
{ {
"context_is_admin": "role:admin or role:administrator", "context_is_admin": "role:admin or role:administrator",
"os_masakari_api:extensions": "", "os_masakari_api:extensions:index": "",
"os_masakari_api:segments": "", "os_masakari_api:extensions:detail": "",
"os_masakari_api:os-hosts": "", "os_masakari_api:segments:index": "",
"os_masakari_api:notifications": "" "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": ""
} }
""" """

View File

@ -20,6 +20,7 @@ from oslo_serialization import jsonutils
import masakari.conf import masakari.conf
from masakari.conf import paths from masakari.conf import paths
from masakari import policies
import masakari.policy import masakari.policy
from masakari.tests.unit import fake_policy from masakari.tests.unit import fake_policy
@ -52,9 +53,10 @@ class RealPolicyFixture(fixtures.Fixture):
masakari.policy.init() masakari.policy.init()
self.addCleanup(masakari.policy.reset) self.addCleanup(masakari.policy.reset)
def set_rules(self, rules): def set_rules(self, rules, overwrite=True):
policy = masakari.policy._ENFORCER 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): class PolicyFixture(RealPolicyFixture):
@ -91,13 +93,10 @@ class RoleBasedPolicyFixture(RealPolicyFixture):
self.role = role self.role = role
def _prepare_policy(self): def _prepare_policy(self):
with open(CONF.oslo_policy.policy_file) as fp: # Convert all actions to require the specified role
policy = fp.read() policy = {}
policy = jsonutils.loads(policy) for rule in policies.list_rules():
policy[rule.name] = 'role:%s' % self.role
# Convert all actions to require specified role
for action, rule in policy.items():
policy[action] = 'role:%s' % self.role
self.policy_dir = self.useFixture(fixtures.TempDir()) self.policy_dir = self.useFixture(fixtures.TempDir())
self.policy_file = os.path.join(self.policy_dir.path, 'policy.json') self.policy_file = os.path.join(self.policy_dir.path, 'policy.json')

View File

@ -457,3 +457,39 @@ class HackingTestCase(test.NoDBTestCase):
yieldx_func(a, b) yieldx_func(a, b)
""" """
self._assert_has_no_errors(code, checks.yield_followed_by_space) 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)

View File

@ -56,11 +56,11 @@ class PolicyFileTestCase(test.NoDBTestCase):
action = "example:test" action = "example:test"
with open(tmpfilename, "w") as policyfile: with open(tmpfilename, "w") as policyfile:
policyfile.write('{"example:test": ""}') policyfile.write('{"example:test": ""}')
policy.enforce(self.context, action, self.target) policy.authorize(self.context, action, self.target)
with open(tmpfilename, "w") as policyfile: with open(tmpfilename, "w") as policyfile:
policyfile.write('{"example:test": "!"}') policyfile.write('{"example:test": "!"}')
policy._ENFORCER.load_rules(True) policy._ENFORCER.load_rules(True)
self.assertRaises(exception.PolicyNotAuthorized, policy.enforce, self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
self.context, action, self.target) self.context, action, self.target)
@ -90,39 +90,70 @@ class PolicyTestCase(test.NoDBTestCase):
self.context = context.RequestContext('fake', 'fake', roles=['member']) self.context = context.RequestContext('fake', 'fake', roles=['member'])
self.target = {} self.target = {}
def test_enforce_bad_action_throws(self): def test_authorize_nonexistent_action_throws(self):
action = "example:denied" action = "example:noexist"
self.assertRaises(exception.PolicyNotAuthorized, policy.enforce, self.assertRaises(oslo_policy.PolicyNotRegistered, policy.authorize,
self.context, action, self.target) self.context, action, self.target)
def test_enforce_bad_action_noraise(self): def test_authorize_bad_action_throws(self):
action = "example:denied" 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) self.assertFalse(result)
def test_enforce_good_action(self): def test_authorize_good_action(self):
action = "example:allowed" action = "example:allowed"
result = policy.enforce(self.context, action, self.target) result = policy.authorize(self.context, action, self.target)
self.assertTrue(result) self.assertTrue(result)
@requests_mock.mock() @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/', req_mock.post('http://www.example.com/',
text='True') text='True')
action = "example:get_http" action = "example:get_http"
target = {} target = {}
result = policy.enforce(self.context, action, target) result = policy.authorize(self.context, action, target)
self.assertTrue(result) self.assertTrue(result)
@requests_mock.mock() @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/', req_mock.post('http://www.example.com/',
text='False') text='False')
action = "example:get_http" action = "example:get_http"
target = {} target = {}
self.assertRaises(exception.PolicyNotAuthorized, policy.enforce, self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
self.context, action, target) 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): class IsAdminCheckTestCase(test.NoDBTestCase):
def setUp(self): def setUp(self):
@ -160,6 +191,23 @@ class IsAdminCheckTestCase(test.NoDBTestCase):
policy._ENFORCER), True) 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): class RealRolePolicyTestCase(test.NoDBTestCase):
def setUp(self): def setUp(self):
super(RealRolePolicyTestCase, self).setUp() super(RealRolePolicyTestCase, self).setUp()
@ -172,10 +220,21 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
self.fake_policy = jsonutils.loads(fake_policy.policy_data) self.fake_policy = jsonutils.loads(fake_policy.policy_data)
self.admin_only_rules = ( self.admin_only_rules = (
"os_masakari_api:extensions", "os_masakari_api:extensions:index",
"os_masakari_api:os-hosts", "os_masakari_api:extensions:detail",
"os_masakari_api:segments", "os_masakari_api:os-hosts:index",
"os_masakari_api:notifications" "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): def test_all_rules_in_sample_file(self):
@ -187,7 +246,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
def test_admin_only_rules(self): def test_admin_only_rules(self):
for rule in self.admin_only_rules: for rule in self.admin_only_rules:
self.assertRaises(exception.PolicyNotAuthorized, policy.enforce, self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
self.non_admin_context, rule, self.non_admin_context, rule,
{'project_id': 'fake', 'user_id': 'fake'}) {'project_id': 'fake', 'user_id': 'fake'})
policy.enforce(self.admin_context, rule, self.target) policy.authorize(self.admin_context, rule, self.target)

View 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.

View File

@ -33,6 +33,16 @@ oslo.config.opts =
oslo.config.opts.defaults = oslo.config.opts.defaults =
masakari.api = masakari.common.config:set_middleware_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 = console_scripts =
masakari-api = masakari.cmd.api:main masakari-api = masakari.cmd.api:main
masakari-engine = masakari.cmd.engine:main masakari-engine = masakari.cmd.engine:main

View File

@ -38,6 +38,9 @@ commands =
basepython = python3 basepython = python3
commands = oslo-config-generator --config-file=etc/masakari/masakari-config-generator.conf 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] [testenv:pep8]
basepython = python3 basepython = python3
commands = flake8 {posargs} commands = flake8 {posargs}