Skip to content

Commit

Permalink
Implement granular policy rules for placement
Browse files Browse the repository at this point in the history
This adds a granular policy checking framework for
placement based on nova.policy but with a lot of
the legacy cruft removed, like the is_admin and
context_is_admin rules.

A new PlacementPolicyFixture is added along with
a new configuration option, [placement]/policy_file,
which is needed because the default policy file
that gets used in config is from [oslo_policy]/policy_file
which is being used as the nova policy file. As
far as I can tell, oslo.policy doesn't allow for
multiple policy files with different names unless
I'm misunderstanding how the policy_dirs option works.

With these changes, we can have something like:

  /etc/nova/policy.json - for nova policy rules
  /etc/nova/placement-policy.yaml - for placement rules

The docs are also updated to include the placement
policy sample along with a tox builder for the sample.

This starts by adding granular rules for CRUD operations
on the /resource_providers and /resource_providers/{uuid}
routes which use the same descriptions from the placement
API reference. Subsequent patches will add new granular
rules for the other routes.

Part of blueprint granular-placement-policy

Change-Id: I17573f5210314341c332fdcb1ce462a989c21940
  • Loading branch information
mriedem committed May 17, 2018
1 parent ccc02de commit 0a46197
Show file tree
Hide file tree
Showing 33 changed files with 711 additions and 88 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ nova/vcsversion.py
tools/conf/nova.conf*
doc/source/_static/nova.conf.sample
doc/source/_static/nova.policy.yaml.sample
doc/source/_static/placement.policy.yaml.sample

# Files created by releasenotes build
releasenotes/build
6 changes: 4 additions & 2 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@
config_generator_config_file = '../../etc/nova/nova-config-generator.conf'
sample_config_basename = '_static/nova'

policy_generator_config_file = '../../etc/nova/nova-policy-generator.conf'
sample_policy_basename = '_static/nova'
policy_generator_config_file = [
('../../etc/nova/nova-policy-generator.conf', '_static/nova'),
('../../etc/nova/placement-policy-generator.conf', '_static/placement')
]

actdiag_html_image_format = 'SVG'
actdiag_antialias = True
Expand Down
22 changes: 18 additions & 4 deletions doc/source/configuration/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,29 @@ Configuration
* :doc:`Sample Config File <sample-config>`: A sample config
file with inline documentation.

Policy
------
Nova Policy
-----------

Nova, like most OpenStack projects, uses a policy language to restrict
permissions on REST API actions.

* :doc:`Policy Reference <policy>`: A complete reference of all
policy points in nova and what they impact.

* :doc:`Sample Policy File <sample-policy>`: A sample policy
file with inline documentation.
* :doc:`Sample Policy File <sample-policy>`: A sample nova
policy file with inline documentation.

Placement Policy
----------------

Placement, like most OpenStack projects, uses a policy language to restrict
permissions on REST API actions.

* :doc:`Policy Reference <placement-policy>`: A complete
reference of all policy points in placement and what they impact.

* :doc:`Sample Policy File <sample-placement-policy>`: A sample
placement policy file with inline documentation.


.. # NOTE(mriedem): This is the section where we hide things that we don't
Expand All @@ -43,3 +55,5 @@ permissions on REST API actions.
sample-config
policy
sample-policy
placement-policy
sample-placement-policy
10 changes: 10 additions & 0 deletions doc/source/configuration/placement-policy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
==================
Placement Policies
==================

The following is an overview of all available policies in Placement.
For a sample configuration file, refer to
:doc:`/configuration/sample-placement-policy`.

.. show-policy::
:config-file: etc/nova/placement-policy-generator.conf
6 changes: 3 additions & 3 deletions doc/source/configuration/policy.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
========
Policies
========
=============
Nova Policies
=============

The following is an overview of all available policies in Nova. For a sample
configuration file, refer to :doc:`/configuration/sample-policy`.
Expand Down
16 changes: 16 additions & 0 deletions doc/source/configuration/sample-placement-policy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
============================
Sample Placement Policy File
============================

The following is a sample placement policy file for adaptation and use.

The sample policy can also be viewed in :download:`file form
</_static/placement.policy.yaml.sample>`.

.. important::

The sample policy file is auto-generated from placement when this
documentation is built. You must ensure your version of placement matches
the version of this documentation.

.. literalinclude:: /_static/placement.policy.yaml.sample
6 changes: 3 additions & 3 deletions doc/source/configuration/sample-policy.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
==================
Sample Policy File
==================
=======================
Sample Nova Policy File
=======================

The following is a sample nova policy file for adaptation and use.

Expand Down
22 changes: 19 additions & 3 deletions etc/nova/README-policy.yaml.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
To generate the sample policy.yaml file, run the following command from the top
level of the nova directory:
Nova
====

To generate the sample nova policy.yaml file, run the following command from
the top level of the nova directory:

tox -egenpolicy

For a pre-generated example of the latest policy.yaml, see:
For a pre-generated example of the latest nova policy.yaml, see:

https://docs.openstack.org/nova/latest/configuration/sample-policy.html


Placement
=========

To generate the sample placement policy.yaml file, run the following command
from the top level of the nova directory:

tox -e genplacementpolicy

For a pre-generated example of the latest placement policy.yaml, see:

https://docs.openstack.org/nova/latest/configuration/sample-placement-policy.html
5 changes: 5 additions & 0 deletions etc/nova/placement-policy-generator.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[DEFAULT]
# TODO: When placement is split out of the nova repo, this can change to
# etc/placement/policy.yaml.sample.
output_file = etc/nova/placement-policy.yaml.sample
namespace = placement
2 changes: 1 addition & 1 deletion lower-constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ oslo.i18n==3.15.3
oslo.log==3.36.0
oslo.messaging==5.29.0
oslo.middleware==3.31.0
oslo.policy==1.30.0
oslo.policy==1.35.0
oslo.privsep==1.23.0
oslo.reports==1.18.0
oslo.rootwrap==5.8.0
Expand Down
10 changes: 2 additions & 8 deletions nova/api/openstack/placement/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@


from keystonemiddleware import auth_token
from oslo_context import context
from oslo_db.sqlalchemy import enginefacade
from oslo_log import log as logging
from oslo_middleware import request_id
import webob.dec
import webob.exc

from nova.api.openstack.placement import context

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -57,19 +56,14 @@ def __call__(self, req):
return self.application


@enginefacade.transaction_context_provider
class RequestContext(context.RequestContext):
pass


class PlacementKeystoneContext(Middleware):
"""Make a request context from keystone headers."""

@webob.dec.wsgify
def __call__(self, req):
req_id = req.environ.get(request_id.ENV_REQUEST_ID)

ctx = RequestContext.from_environ(
ctx = context.RequestContext.from_environ(
req.environ, request_id=req_id)

if ctx.user_id is None and req.environ['PATH_INFO'] != '/':
Expand Down
52 changes: 52 additions & 0 deletions nova/api/openstack/placement/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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_context import context
from oslo_db.sqlalchemy import enginefacade

from nova.api.openstack.placement import exception
from nova.api.openstack.placement import policy


@enginefacade.transaction_context_provider
class RequestContext(context.RequestContext):

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: As much information about the object being operated on
as possible. The target argument should be a dict instance or an
instance of a class that fully supports the Mapping abstract base
class and deep copying. 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.PolicyNotAuthorized occurs.
:raises nova.exception.PolicyNotAuthorized: 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.PolicyNotAuthorized:
if fatal:
raise
return False
4 changes: 4 additions & 0 deletions nova/api/openstack/placement/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ class ObjectActionError(_BaseException):
msg_fmt = _('Object action %(action)s failed because: %(reason)s')


class PolicyNotAuthorized(_BaseException):
msg_fmt = _("Policy does not allow %(action)s to be performed.")


class ResourceClassCannotDeleteStandard(_BaseException):
msg_fmt = _("Cannot delete standard resource class %(resource_class)s.")

Expand Down
51 changes: 41 additions & 10 deletions nova/api/openstack/placement/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
method.
"""

import re

import routes
import webob

from oslo_log import log as logging
from oslo_utils import excutils

from nova.api.openstack.placement import exception
from nova.api.openstack.placement.handlers import aggregate
Expand All @@ -38,7 +41,6 @@
from nova.api.openstack.placement.handlers import root
from nova.api.openstack.placement.handlers import trait
from nova.api.openstack.placement.handlers import usage
from nova.api.openstack.placement import policy
from nova.api.openstack.placement import util
from nova.i18n import _

Expand Down Expand Up @@ -129,6 +131,19 @@
},
}

# This is a temporary list (of regexes) of the route handlers that will do
# their own granular policy check. Once all handlers are doing their own
# policy checks we can remove this along with the generic policy check in
# PlacementHandler. All entries are checked against re.match() so must
# match the start of the path.
PER_ROUTE_POLICY = [
# The root is special in that it does not require auth.
'/$',
# /resource_providers
# /resource_providers/{uuid}
'/resource_providers(/[A-Za-z0-9-]+)?$'
]


def dispatch(environ, start_response, mapper):
"""Find a matching route for the current request.
Expand Down Expand Up @@ -192,17 +207,29 @@ def __init__(self, **local_config):
# NOTE(cdent): Local config currently unused.
self._map = make_map(ROUTE_DECLARATIONS)

@staticmethod
def _is_granular_policy_check(path):
for policy in PER_ROUTE_POLICY:
if re.match(policy, path):
return True
return False

def __call__(self, environ, start_response):
# All requests but '/' require admin.
if environ['PATH_INFO'] != '/':
# Any routes that do not yet have a granular policy check default
# to admin-only.
if not self._is_granular_policy_check(environ['PATH_INFO']):
context = environ['placement.context']
# TODO(cdent): Using is_admin everywhere (except /) is
# insufficiently flexible for future use case but is
# convenient for initial exploration.
if not policy.placement_authorize(context, 'placement'):
raise webob.exc.HTTPForbidden(
_('admin required'),
json_formatter=util.json_error_formatter)
try:
if not context.can('placement', fatal=False):
raise webob.exc.HTTPForbidden(
_('admin required'),
json_formatter=util.json_error_formatter)
except Exception:
# This is here mostly for help in debugging problems with
# busted test setup.
with excutils.save_and_reraise_exception():
LOG.exception('policy check failed for path: %s',
environ['PATH_INFO'])
# Check that an incoming request with a content-length header
# that is an integer > 0 and not empty, also has a content-type
# header that is not empty. If not raise a 400.
Expand All @@ -223,6 +250,10 @@ def __call__(self, environ, start_response):
except exception.NotFound as exc:
raise webob.exc.HTTPNotFound(
exc, json_formatter=util.json_error_formatter)
except exception.PolicyNotAuthorized as exc:
raise webob.exc.HTTPForbidden(
exc.format_message(),
json_formatter=util.json_error_formatter)
# Remaining uncaught exceptions will rise first to the Microversion
# middleware, where any WebOb generated exceptions will be caught and
# transformed into legit HTTP error responses (with microversion
Expand Down
8 changes: 7 additions & 1 deletion nova/api/openstack/placement/handlers/resource_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from nova.api.openstack.placement import exception
from nova.api.openstack.placement import microversion
from nova.api.openstack.placement.objects import resource_provider as rp_obj
from nova.api.openstack.placement.policies import resource_provider as policies
from nova.api.openstack.placement.schemas import resource_provider as rp_schema
from nova.api.openstack.placement import util
from nova.api.openstack.placement import wsgi_wrapper
Expand Down Expand Up @@ -78,6 +79,7 @@ def create_resource_provider(req):
header pointing to the newly created resource provider.
"""
context = req.environ['placement.context']
context.can(policies.CREATE)
schema = rp_schema.POST_RESOURCE_PROVIDER_SCHEMA
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
if want_version.matches((1, 14)):
Expand Down Expand Up @@ -126,6 +128,7 @@ def delete_resource_provider(req):
"""
uuid = util.wsgi_path_item(req.environ, 'uuid')
context = req.environ['placement.context']
context.can(policies.DELETE)
# The containing application will catch a not found here.
try:
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
Expand Down Expand Up @@ -153,9 +156,10 @@ def get_resource_provider(req):
"""
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
uuid = util.wsgi_path_item(req.environ, 'uuid')
# The containing application will catch a not found here.
context = req.environ['placement.context']
context.can(policies.SHOW)

# The containing application will catch a not found here.
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
context, uuid)

Expand All @@ -179,6 +183,7 @@ def list_resource_providers(req):
a collection of resource providers.
"""
context = req.environ['placement.context']
context.can(policies.LIST)
want_version = req.environ[microversion.MICROVERSION_ENVIRON]

schema = rp_schema.GET_RPS_SCHEMA_1_0
Expand Down Expand Up @@ -244,6 +249,7 @@ def update_resource_provider(req):
"""
uuid = util.wsgi_path_item(req.environ, 'uuid')
context = req.environ['placement.context']
context.can(policies.UPDATE)
want_version = req.environ[microversion.MICROVERSION_ENVIRON]

# The containing application will catch a not found here.
Expand Down
Loading

0 comments on commit 0a46197

Please sign in to comment.