Merge "Implement granular policy rules for placement"
This commit is contained in:
commit
07d2b69bd3
1
.gitignore
vendored
1
.gitignore
vendored
@ -48,6 +48,7 @@ nova/vcsversion.py
|
|||||||
tools/conf/nova.conf*
|
tools/conf/nova.conf*
|
||||||
doc/source/_static/nova.conf.sample
|
doc/source/_static/nova.conf.sample
|
||||||
doc/source/_static/nova.policy.yaml.sample
|
doc/source/_static/nova.policy.yaml.sample
|
||||||
|
doc/source/_static/placement.policy.yaml.sample
|
||||||
|
|
||||||
# Files created by releasenotes build
|
# Files created by releasenotes build
|
||||||
releasenotes/build
|
releasenotes/build
|
||||||
|
@ -57,8 +57,10 @@ bug_tag = ''
|
|||||||
config_generator_config_file = '../../etc/nova/nova-config-generator.conf'
|
config_generator_config_file = '../../etc/nova/nova-config-generator.conf'
|
||||||
sample_config_basename = '_static/nova'
|
sample_config_basename = '_static/nova'
|
||||||
|
|
||||||
policy_generator_config_file = '../../etc/nova/nova-policy-generator.conf'
|
policy_generator_config_file = [
|
||||||
sample_policy_basename = '_static/nova'
|
('../../etc/nova/nova-policy-generator.conf', '_static/nova'),
|
||||||
|
('../../etc/nova/placement-policy-generator.conf', '_static/placement')
|
||||||
|
]
|
||||||
|
|
||||||
actdiag_html_image_format = 'SVG'
|
actdiag_html_image_format = 'SVG'
|
||||||
actdiag_antialias = True
|
actdiag_antialias = True
|
||||||
|
@ -20,8 +20,8 @@ Configuration
|
|||||||
* :doc:`Sample Config File <sample-config>`: A sample config
|
* :doc:`Sample Config File <sample-config>`: A sample config
|
||||||
file with inline documentation.
|
file with inline documentation.
|
||||||
|
|
||||||
Policy
|
Nova Policy
|
||||||
------
|
-----------
|
||||||
|
|
||||||
Nova, like most OpenStack projects, uses a policy language to restrict
|
Nova, like most OpenStack projects, uses a policy language to restrict
|
||||||
permissions on REST API actions.
|
permissions on REST API actions.
|
||||||
@ -29,8 +29,20 @@ permissions on REST API actions.
|
|||||||
* :doc:`Policy Reference <policy>`: A complete reference of all
|
* :doc:`Policy Reference <policy>`: A complete reference of all
|
||||||
policy points in nova and what they impact.
|
policy points in nova and what they impact.
|
||||||
|
|
||||||
* :doc:`Sample Policy File <sample-policy>`: A sample policy
|
* :doc:`Sample Policy File <sample-policy>`: A sample nova
|
||||||
file with inline documentation.
|
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
|
.. # NOTE(mriedem): This is the section where we hide things that we don't
|
||||||
@ -43,3 +55,5 @@ permissions on REST API actions.
|
|||||||
sample-config
|
sample-config
|
||||||
policy
|
policy
|
||||||
sample-policy
|
sample-policy
|
||||||
|
placement-policy
|
||||||
|
sample-placement-policy
|
||||||
|
10
doc/source/configuration/placement-policy.rst
Normal file
10
doc/source/configuration/placement-policy.rst
Normal file
@ -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
|
16
doc/source/configuration/sample-placement-policy.rst
Normal file
16
doc/source/configuration/sample-placement-policy.rst
Normal file
@ -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
|
@ -1,8 +1,24 @@
|
|||||||
To generate the sample policy.yaml file, run the following command from the top
|
Nova
|
||||||
level of the nova directory:
|
====
|
||||||
|
|
||||||
|
To generate the sample nova policy.yaml file, run the following command from
|
||||||
|
the top level of the nova directory:
|
||||||
|
|
||||||
tox -egenpolicy
|
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
|
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
etc/nova/placement-policy-generator.conf
Normal file
5
etc/nova/placement-policy-generator.conf
Normal file
@ -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
|
@ -83,7 +83,7 @@ oslo.i18n==3.15.3
|
|||||||
oslo.log==3.36.0
|
oslo.log==3.36.0
|
||||||
oslo.messaging==5.29.0
|
oslo.messaging==5.29.0
|
||||||
oslo.middleware==3.31.0
|
oslo.middleware==3.31.0
|
||||||
oslo.policy==1.30.0
|
oslo.policy==1.35.0
|
||||||
oslo.privsep==1.23.0
|
oslo.privsep==1.23.0
|
||||||
oslo.reports==1.18.0
|
oslo.reports==1.18.0
|
||||||
oslo.rootwrap==5.8.0
|
oslo.rootwrap==5.8.0
|
||||||
|
@ -12,13 +12,12 @@
|
|||||||
|
|
||||||
|
|
||||||
from keystonemiddleware import auth_token
|
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_log import log as logging
|
||||||
from oslo_middleware import request_id
|
from oslo_middleware import request_id
|
||||||
import webob.dec
|
import webob.dec
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
|
from nova.api.openstack.placement import context
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -57,11 +56,6 @@ class NoAuthMiddleware(Middleware):
|
|||||||
return self.application
|
return self.application
|
||||||
|
|
||||||
|
|
||||||
@enginefacade.transaction_context_provider
|
|
||||||
class RequestContext(context.RequestContext):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PlacementKeystoneContext(Middleware):
|
class PlacementKeystoneContext(Middleware):
|
||||||
"""Make a request context from keystone headers."""
|
"""Make a request context from keystone headers."""
|
||||||
|
|
||||||
@ -69,7 +63,7 @@ class PlacementKeystoneContext(Middleware):
|
|||||||
def __call__(self, req):
|
def __call__(self, req):
|
||||||
req_id = req.environ.get(request_id.ENV_REQUEST_ID)
|
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)
|
req.environ, request_id=req_id)
|
||||||
|
|
||||||
if ctx.user_id is None and req.environ['PATH_INFO'] != '/':
|
if ctx.user_id is None and req.environ['PATH_INFO'] != '/':
|
||||||
|
52
nova/api/openstack/placement/context.py
Normal file
52
nova/api/openstack/placement/context.py
Normal file
@ -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
|
@ -127,6 +127,10 @@ class ObjectActionError(_BaseException):
|
|||||||
msg_fmt = _('Object action %(action)s failed because: %(reason)s')
|
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):
|
class ResourceClassCannotDeleteStandard(_BaseException):
|
||||||
msg_fmt = _("Cannot delete standard resource class %(resource_class)s.")
|
msg_fmt = _("Cannot delete standard resource class %(resource_class)s.")
|
||||||
|
|
||||||
|
@ -23,10 +23,13 @@ Routes.Mapper, including automatic handlers to respond with a
|
|||||||
method.
|
method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
import routes
|
import routes
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
|
|
||||||
from nova.api.openstack.placement import exception
|
from nova.api.openstack.placement import exception
|
||||||
from nova.api.openstack.placement.handlers import aggregate
|
from nova.api.openstack.placement.handlers import aggregate
|
||||||
@ -38,7 +41,6 @@ from nova.api.openstack.placement.handlers import resource_provider
|
|||||||
from nova.api.openstack.placement.handlers import root
|
from nova.api.openstack.placement.handlers import root
|
||||||
from nova.api.openstack.placement.handlers import trait
|
from nova.api.openstack.placement.handlers import trait
|
||||||
from nova.api.openstack.placement.handlers import usage
|
from nova.api.openstack.placement.handlers import usage
|
||||||
from nova.api.openstack.placement import policy
|
|
||||||
from nova.api.openstack.placement import util
|
from nova.api.openstack.placement import util
|
||||||
from nova.i18n import _
|
from nova.i18n import _
|
||||||
|
|
||||||
@ -129,6 +131,19 @@ ROUTE_DECLARATIONS = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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):
|
def dispatch(environ, start_response, mapper):
|
||||||
"""Find a matching route for the current request.
|
"""Find a matching route for the current request.
|
||||||
@ -192,17 +207,29 @@ class PlacementHandler(object):
|
|||||||
# NOTE(cdent): Local config currently unused.
|
# NOTE(cdent): Local config currently unused.
|
||||||
self._map = make_map(ROUTE_DECLARATIONS)
|
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):
|
def __call__(self, environ, start_response):
|
||||||
# All requests but '/' require admin.
|
# Any routes that do not yet have a granular policy check default
|
||||||
if environ['PATH_INFO'] != '/':
|
# to admin-only.
|
||||||
|
if not self._is_granular_policy_check(environ['PATH_INFO']):
|
||||||
context = environ['placement.context']
|
context = environ['placement.context']
|
||||||
# TODO(cdent): Using is_admin everywhere (except /) is
|
try:
|
||||||
# insufficiently flexible for future use case but is
|
if not context.can('placement', fatal=False):
|
||||||
# convenient for initial exploration.
|
raise webob.exc.HTTPForbidden(
|
||||||
if not policy.placement_authorize(context, 'placement'):
|
_('admin required'),
|
||||||
raise webob.exc.HTTPForbidden(
|
json_formatter=util.json_error_formatter)
|
||||||
_('admin required'),
|
except Exception:
|
||||||
json_formatter=util.json_error_formatter)
|
# 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
|
# Check that an incoming request with a content-length header
|
||||||
# that is an integer > 0 and not empty, also has a content-type
|
# that is an integer > 0 and not empty, also has a content-type
|
||||||
# header that is not empty. If not raise a 400.
|
# header that is not empty. If not raise a 400.
|
||||||
@ -223,6 +250,10 @@ class PlacementHandler(object):
|
|||||||
except exception.NotFound as exc:
|
except exception.NotFound as exc:
|
||||||
raise webob.exc.HTTPNotFound(
|
raise webob.exc.HTTPNotFound(
|
||||||
exc, json_formatter=util.json_error_formatter)
|
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
|
# Remaining uncaught exceptions will rise first to the Microversion
|
||||||
# middleware, where any WebOb generated exceptions will be caught and
|
# middleware, where any WebOb generated exceptions will be caught and
|
||||||
# transformed into legit HTTP error responses (with microversion
|
# transformed into legit HTTP error responses (with microversion
|
||||||
|
@ -21,6 +21,7 @@ import webob
|
|||||||
from nova.api.openstack.placement import exception
|
from nova.api.openstack.placement import exception
|
||||||
from nova.api.openstack.placement import microversion
|
from nova.api.openstack.placement import microversion
|
||||||
from nova.api.openstack.placement.objects import resource_provider as rp_obj
|
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.schemas import resource_provider as rp_schema
|
||||||
from nova.api.openstack.placement import util
|
from nova.api.openstack.placement import util
|
||||||
from nova.api.openstack.placement import wsgi_wrapper
|
from nova.api.openstack.placement import wsgi_wrapper
|
||||||
@ -78,6 +79,7 @@ def create_resource_provider(req):
|
|||||||
header pointing to the newly created resource provider.
|
header pointing to the newly created resource provider.
|
||||||
"""
|
"""
|
||||||
context = req.environ['placement.context']
|
context = req.environ['placement.context']
|
||||||
|
context.can(policies.CREATE)
|
||||||
schema = rp_schema.POST_RESOURCE_PROVIDER_SCHEMA
|
schema = rp_schema.POST_RESOURCE_PROVIDER_SCHEMA
|
||||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||||
if want_version.matches((1, 14)):
|
if want_version.matches((1, 14)):
|
||||||
@ -126,6 +128,7 @@ def delete_resource_provider(req):
|
|||||||
"""
|
"""
|
||||||
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
||||||
context = req.environ['placement.context']
|
context = req.environ['placement.context']
|
||||||
|
context.can(policies.DELETE)
|
||||||
# The containing application will catch a not found here.
|
# The containing application will catch a not found here.
|
||||||
try:
|
try:
|
||||||
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
||||||
@ -153,9 +156,10 @@ def get_resource_provider(req):
|
|||||||
"""
|
"""
|
||||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||||
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
||||||
# The containing application will catch a not found here.
|
|
||||||
context = req.environ['placement.context']
|
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(
|
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
||||||
context, uuid)
|
context, uuid)
|
||||||
|
|
||||||
@ -179,6 +183,7 @@ def list_resource_providers(req):
|
|||||||
a collection of resource providers.
|
a collection of resource providers.
|
||||||
"""
|
"""
|
||||||
context = req.environ['placement.context']
|
context = req.environ['placement.context']
|
||||||
|
context.can(policies.LIST)
|
||||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||||
|
|
||||||
schema = rp_schema.GET_RPS_SCHEMA_1_0
|
schema = rp_schema.GET_RPS_SCHEMA_1_0
|
||||||
@ -244,6 +249,7 @@ def update_resource_provider(req):
|
|||||||
"""
|
"""
|
||||||
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
||||||
context = req.environ['placement.context']
|
context = req.environ['placement.context']
|
||||||
|
context.can(policies.UPDATE)
|
||||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||||
|
|
||||||
# The containing application will catch a not found here.
|
# The containing application will catch a not found here.
|
||||||
|
23
nova/api/openstack/placement/policies/__init__.py
Normal file
23
nova/api/openstack/placement/policies/__init__.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# 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 nova.api.openstack.placement.policies import base
|
||||||
|
from nova.api.openstack.placement.policies import resource_provider
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return itertools.chain(
|
||||||
|
base.list_rules(),
|
||||||
|
resource_provider.list_rules(),
|
||||||
|
)
|
41
nova/api/openstack/placement/policies/base.py
Normal file
41
nova/api/openstack/placement/policies/base.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
RULE_ADMIN_API = 'rule:admin_api'
|
||||||
|
|
||||||
|
rules = [
|
||||||
|
# "placement" is the default rule (action) used for all routes that do
|
||||||
|
# not yet have granular policy rules. It is used in
|
||||||
|
# PlacementHandler.__call__ and can be dropped once all routes have
|
||||||
|
# granular policy handling.
|
||||||
|
policy.RuleDefault(
|
||||||
|
"placement",
|
||||||
|
"role:admin",
|
||||||
|
description="This rule is used for all routes that do not yet "
|
||||||
|
"have granular policy rules. It will be replaced "
|
||||||
|
"with rule:admin_api.",
|
||||||
|
deprecated_for_removal=True,
|
||||||
|
deprecated_reason="This was a catch-all rule hard-coded into "
|
||||||
|
"the placement service and has been superseded by "
|
||||||
|
"granular policy rules per operation.",
|
||||||
|
deprecated_since="18.0.0"),
|
||||||
|
policy.RuleDefault(
|
||||||
|
"admin_api",
|
||||||
|
"role:admin",
|
||||||
|
description="Default rule for most placement APIs."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return rules
|
81
nova/api/openstack/placement/policies/resource_provider.py
Normal file
81
nova/api/openstack/placement/policies/resource_provider.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# 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 nova.api.openstack.placement.policies import base
|
||||||
|
|
||||||
|
|
||||||
|
PREFIX = 'placement:resource_providers:%s'
|
||||||
|
LIST = PREFIX % 'list'
|
||||||
|
CREATE = PREFIX % 'create'
|
||||||
|
SHOW = PREFIX % 'show'
|
||||||
|
UPDATE = PREFIX % 'update'
|
||||||
|
DELETE = PREFIX % 'delete'
|
||||||
|
|
||||||
|
rules = [
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
LIST,
|
||||||
|
base.RULE_ADMIN_API,
|
||||||
|
"List resource providers.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/resource_providers'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
CREATE,
|
||||||
|
base.RULE_ADMIN_API,
|
||||||
|
"Create resource provider.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'method': 'POST',
|
||||||
|
'path': '/resource_providers'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
SHOW,
|
||||||
|
base.RULE_ADMIN_API,
|
||||||
|
"Show resource provider.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/resource_providers/{uuid}'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
UPDATE,
|
||||||
|
base.RULE_ADMIN_API,
|
||||||
|
"Update resource provider.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'method': 'PUT',
|
||||||
|
'path': '/resource_providers/{uuid}'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
DELETE,
|
||||||
|
base.RULE_ADMIN_API,
|
||||||
|
"Delete resource provider.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'method': 'DELETE',
|
||||||
|
'path': '/resource_providers/{uuid}'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return rules
|
@ -14,7 +14,10 @@
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_policy import policy
|
from oslo_policy import policy
|
||||||
from oslo_serialization import jsonutils
|
from oslo_utils import excutils
|
||||||
|
|
||||||
|
from nova.api.openstack.placement import exception
|
||||||
|
from nova.api.openstack.placement import policies
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -22,54 +25,68 @@ LOG = logging.getLogger(__name__)
|
|||||||
_ENFORCER_PLACEMENT = None
|
_ENFORCER_PLACEMENT = None
|
||||||
|
|
||||||
|
|
||||||
def placement_init():
|
def reset():
|
||||||
"""Init an Enforcer class for placement policy.
|
"""Used to reset the global _ENFORCER_PLACEMENT between test runs."""
|
||||||
|
global _ENFORCER_PLACEMENT
|
||||||
|
if _ENFORCER_PLACEMENT:
|
||||||
|
_ENFORCER_PLACEMENT.clear()
|
||||||
|
_ENFORCER_PLACEMENT = None
|
||||||
|
|
||||||
This method uses a different list of policies than other parts of Nova.
|
|
||||||
This is done to facilitate a split out of the placement service later.
|
def init():
|
||||||
"""
|
"""Init an Enforcer class. Sets the _ENFORCER_PLACEMENT global."""
|
||||||
global _ENFORCER_PLACEMENT
|
global _ENFORCER_PLACEMENT
|
||||||
if not _ENFORCER_PLACEMENT:
|
if not _ENFORCER_PLACEMENT:
|
||||||
# TODO(cdent): Using is_admin everywhere (except /) is
|
# NOTE(mriedem): We have to explicitly pass in the
|
||||||
# insufficiently flexible for future use case but is
|
# [placement]/policy_file path because otherwise oslo_policy defaults
|
||||||
# convenient for initial exploration. We will need to
|
# to read the policy file from config option [oslo_policy]/policy_file
|
||||||
# determine how to manage authorization/policy and
|
# which is used by nova. In other words, to have separate policy files
|
||||||
# implement that, probably per handler.
|
# for placement and nova, we have to use separate policy_file options.
|
||||||
rules = policy.Rules.load(jsonutils.dumps({'placement': 'role:admin'}))
|
_ENFORCER_PLACEMENT = policy.Enforcer(
|
||||||
# Enforcer is initialized so that the above rule is loaded in and no
|
CONF, policy_file=CONF.placement.policy_file)
|
||||||
# policy file is read.
|
_ENFORCER_PLACEMENT.register_defaults(policies.list_rules())
|
||||||
# TODO(alaski): Register a default rule rather than loading it in like
|
_ENFORCER_PLACEMENT.load_rules()
|
||||||
# this. That requires that a policy file is specified to be read. When
|
|
||||||
# this is split out such that a placement policy file makes sense then
|
|
||||||
# change to rule registration.
|
|
||||||
_ENFORCER_PLACEMENT = policy.Enforcer(CONF, rules=rules,
|
|
||||||
use_conf=False)
|
|
||||||
|
|
||||||
|
|
||||||
def placement_authorize(context, action, target=None):
|
def get_enforcer():
|
||||||
|
# This method is used by oslopolicy CLI scripts in order to generate policy
|
||||||
|
# files from overrides on disk and defaults in code. We can just pass an
|
||||||
|
# empty list and let oslo do the config lifting for us.
|
||||||
|
cfg.CONF([], project='nova')
|
||||||
|
init()
|
||||||
|
return _ENFORCER_PLACEMENT
|
||||||
|
|
||||||
|
|
||||||
|
def authorize(context, action, target, do_raise=True):
|
||||||
"""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: RequestContext object
|
:param context: instance of
|
||||||
:param action: string representing the action to be checked
|
nova.api.openstack.placement.context.RequestContext
|
||||||
:param target: dictionary representing the object of the action
|
:param action: string representing the action to be checked
|
||||||
for object creation this should be a dictionary representing the
|
this should be colon separated for clarity, i.e.
|
||||||
location of the object e.g. ``{'project_id': context.project_id}``
|
``placement:resource_providers:list``
|
||||||
|
:param target: dictionary representing the object of the action;
|
||||||
:return: returns a non-False value (not necessarily "True") if
|
for object creation this should be a dictionary representing the
|
||||||
authorized, and the exact value False if not authorized.
|
owner of the object e.g. ``{'project_id': context.project_id}``.
|
||||||
|
:param do_raise: if True (the default), raises PolicyNotAuthorized;
|
||||||
|
if False, returns False
|
||||||
|
:raises nova.api.openstack.placement.exception.PolicyNotAuthorized: if
|
||||||
|
verification fails and do_raise is True.
|
||||||
|
:returns: non-False value (not necessarily "True") if authorized, and the
|
||||||
|
exact value False if not authorized and do_raise is False.
|
||||||
"""
|
"""
|
||||||
placement_init()
|
init()
|
||||||
if target is None:
|
|
||||||
target = {'project_id': context.project_id,
|
|
||||||
'user_id': context.user_id}
|
|
||||||
credentials = context.to_policy_values()
|
credentials = context.to_policy_values()
|
||||||
# TODO(alaski): Change this to use authorize() when rules are registered.
|
try:
|
||||||
# noqa the following line because a hacking check disallows using enforce.
|
# NOTE(mriedem): The "action" kwarg is for the PolicyNotAuthorized exc.
|
||||||
result = _ENFORCER_PLACEMENT.enforce(action, target, credentials,
|
return _ENFORCER_PLACEMENT.authorize(
|
||||||
do_raise=False, exc=None,
|
action, target, credentials, do_raise=do_raise,
|
||||||
action=action)
|
exc=exception.PolicyNotAuthorized, action=action)
|
||||||
if result is False:
|
except policy.PolicyNotRegistered:
|
||||||
LOG.debug('Policy check for %(action)s failed with credentials '
|
with excutils.save_and_reraise_exception():
|
||||||
'%(credentials)s',
|
LOG.exception('Policy not registered')
|
||||||
{'action': action, 'credentials': credentials})
|
except Exception:
|
||||||
return result
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.debug('Policy check for %(action)s failed with credentials '
|
||||||
|
'%(credentials)s',
|
||||||
|
{'action': action, 'credentials': credentials})
|
||||||
|
@ -35,6 +35,15 @@ being equal, two requests for allocation candidates will return the same
|
|||||||
results in the same order; but no guarantees are made as to how that order
|
results in the same order; but no guarantees are made as to how that order
|
||||||
is determined.
|
is determined.
|
||||||
"""),
|
"""),
|
||||||
|
# TODO(mriedem): When placement is split out of nova, this should be
|
||||||
|
# deprecated since then [oslo_policy]/policy_file can be used.
|
||||||
|
cfg.StrOpt(
|
||||||
|
'policy_file',
|
||||||
|
# This default matches what is in
|
||||||
|
# etc/nova/placement-policy-generator.conf
|
||||||
|
default='placement-policy.yaml',
|
||||||
|
help='The file that defines placement policies. This can be an '
|
||||||
|
'absolute path or relative to the configuration file.'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -322,6 +322,8 @@ class TestCase(testtools.TestCase):
|
|||||||
self.addCleanup(self._clear_attrs)
|
self.addCleanup(self._clear_attrs)
|
||||||
self.useFixture(fixtures.EnvironmentVariable('http_proxy'))
|
self.useFixture(fixtures.EnvironmentVariable('http_proxy'))
|
||||||
self.policy = self.useFixture(policy_fixture.PolicyFixture())
|
self.policy = self.useFixture(policy_fixture.PolicyFixture())
|
||||||
|
self.placement_policy = self.useFixture(
|
||||||
|
policy_fixture.PlacementPolicyFixture())
|
||||||
|
|
||||||
self.useFixture(nova_fixtures.PoisonFunctions())
|
self.useFixture(nova_fixtures.PoisonFunctions())
|
||||||
|
|
||||||
|
@ -19,10 +19,12 @@ from oslo_utils import uuidutils
|
|||||||
from nova.api.openstack.placement import deploy
|
from nova.api.openstack.placement import deploy
|
||||||
from nova.api.openstack.placement import exception
|
from nova.api.openstack.placement import exception
|
||||||
from nova.api.openstack.placement.objects import resource_provider as rp_obj
|
from nova.api.openstack.placement.objects import resource_provider as rp_obj
|
||||||
|
from nova.api.openstack.placement import policies
|
||||||
from nova import conf
|
from nova import conf
|
||||||
from nova import config
|
from nova import config
|
||||||
from nova import context
|
from nova import context
|
||||||
from nova.tests import fixtures
|
from nova.tests import fixtures
|
||||||
|
from nova.tests.unit import policy_fixture
|
||||||
from nova.tests import uuidsentinel as uuids
|
from nova.tests import uuidsentinel as uuids
|
||||||
|
|
||||||
|
|
||||||
@ -514,3 +516,28 @@ class GranularFixture(APIFixture):
|
|||||||
_add_inventory(shr_net, 'SRIOV_NET_VF', 16)
|
_add_inventory(shr_net, 'SRIOV_NET_VF', 16)
|
||||||
_add_inventory(shr_net, 'CUSTOM_NET_MBPS', 40000)
|
_add_inventory(shr_net, 'CUSTOM_NET_MBPS', 40000)
|
||||||
_set_traits(shr_net, 'MISC_SHARES_VIA_AGGREGATE')
|
_set_traits(shr_net, 'MISC_SHARES_VIA_AGGREGATE')
|
||||||
|
|
||||||
|
|
||||||
|
class OpenPolicyFixture(APIFixture):
|
||||||
|
"""An APIFixture that changes all policy rules to allow non-admins."""
|
||||||
|
|
||||||
|
def start_fixture(self):
|
||||||
|
super(OpenPolicyFixture, self).start_fixture()
|
||||||
|
self.placement_policy_fixture = policy_fixture.PlacementPolicyFixture()
|
||||||
|
self.placement_policy_fixture.setUp()
|
||||||
|
# Get all of the registered rules and set them to '@' to allow any
|
||||||
|
# user to have access. The nova policy "admin_or_owner" concept does
|
||||||
|
# not really apply to most of placement resources since they do not
|
||||||
|
# have a user_id/project_id attribute.
|
||||||
|
rules = {}
|
||||||
|
for rule in policies.list_rules():
|
||||||
|
name = rule.name
|
||||||
|
# Ignore "base" rules for role:admin.
|
||||||
|
if name in ['placement', 'admin_api']:
|
||||||
|
continue
|
||||||
|
rules[name] = '@'
|
||||||
|
self.placement_policy_fixture.set_rules(rules)
|
||||||
|
|
||||||
|
def stop_fixture(self):
|
||||||
|
super(OpenPolicyFixture, self).stop_fixture()
|
||||||
|
self.placement_policy_fixture.cleanUp()
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
# This tests the individual CRUD operations on /resource_providers
|
||||||
|
# using a non-admin user with an open policy configuration. The
|
||||||
|
# response validation is intentionally minimal.
|
||||||
|
fixtures:
|
||||||
|
- OpenPolicyFixture
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
request_headers:
|
||||||
|
x-auth-token: user
|
||||||
|
accept: application/json
|
||||||
|
content-type: application/json
|
||||||
|
openstack-api-version: placement latest
|
||||||
|
|
||||||
|
tests:
|
||||||
|
|
||||||
|
- name: list resource providers
|
||||||
|
GET: /resource_providers
|
||||||
|
response_json_paths:
|
||||||
|
$.resource_providers: []
|
||||||
|
|
||||||
|
- name: create resource provider
|
||||||
|
POST: /resource_providers
|
||||||
|
request_headers:
|
||||||
|
content-type: application/json
|
||||||
|
data:
|
||||||
|
name: $ENVIRON['RP_NAME']
|
||||||
|
uuid: $ENVIRON['RP_UUID']
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
$.uuid: $ENVIRON['RP_UUID']
|
||||||
|
|
||||||
|
- name: show resource provider
|
||||||
|
GET: /resource_providers/$ENVIRON['RP_UUID']
|
||||||
|
response_json_paths:
|
||||||
|
$.uuid: $ENVIRON['RP_UUID']
|
||||||
|
|
||||||
|
- name: update resource provider
|
||||||
|
PUT: /resource_providers/$ENVIRON['RP_UUID']
|
||||||
|
data:
|
||||||
|
name: new name
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
$.name: new name
|
||||||
|
$.uuid: $ENVIRON['RP_UUID']
|
||||||
|
|
||||||
|
- name: delete resource provider
|
||||||
|
DELETE: /resource_providers/$ENVIRON['RP_UUID']
|
||||||
|
status: 204
|
@ -31,14 +31,13 @@ tests:
|
|||||||
response_json_paths:
|
response_json_paths:
|
||||||
$.errors[0].title: Forbidden
|
$.errors[0].title: Forbidden
|
||||||
|
|
||||||
- name: non admin forbidden non json
|
- name: route not found non json
|
||||||
GET: /resource_providers
|
GET: /moo
|
||||||
request_headers:
|
request_headers:
|
||||||
x-auth-token: user
|
|
||||||
accept: text/plain
|
accept: text/plain
|
||||||
status: 403
|
status: 404
|
||||||
response_strings:
|
response_strings:
|
||||||
- admin required
|
- The resource could not be found
|
||||||
|
|
||||||
- name: post new resource provider - old microversion
|
- name: post new resource provider - old microversion
|
||||||
POST: /resource_providers
|
POST: /resource_providers
|
||||||
|
68
nova/tests/unit/api/openstack/placement/test_context.py
Normal file
68
nova/tests/unit/api/openstack/placement/test_context.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# 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 mock
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from nova.api.openstack.placement import context
|
||||||
|
from nova.api.openstack.placement import exception
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlacementRequestContext(testtools.TestCase):
|
||||||
|
"""Test cases for PlacementRequestContext."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPlacementRequestContext, self).setUp()
|
||||||
|
self.ctxt = context.RequestContext(user_id='fake', project_id='fake')
|
||||||
|
self.default_target = {'user_id': self.ctxt.user_id,
|
||||||
|
'project_id': self.ctxt.project_id}
|
||||||
|
|
||||||
|
@mock.patch('nova.api.openstack.placement.policy.authorize',
|
||||||
|
return_value=True)
|
||||||
|
def test_can_target_none_fatal_true_accept(self, mock_authorize):
|
||||||
|
self.assertTrue(self.ctxt.can('placement:resource_providers:list'))
|
||||||
|
mock_authorize.assert_called_once_with(
|
||||||
|
self.ctxt, 'placement:resource_providers:list',
|
||||||
|
self.default_target)
|
||||||
|
|
||||||
|
@mock.patch('nova.api.openstack.placement.policy.authorize',
|
||||||
|
side_effect=exception.PolicyNotAuthorized(
|
||||||
|
action='placement:resource_providers:list'))
|
||||||
|
def test_can_target_none_fatal_true_reject(self, mock_authorize):
|
||||||
|
self.assertRaises(exception.PolicyNotAuthorized,
|
||||||
|
self.ctxt.can, 'placement:resource_providers:list')
|
||||||
|
mock_authorize.assert_called_once_with(
|
||||||
|
self.ctxt, 'placement:resource_providers:list',
|
||||||
|
self.default_target)
|
||||||
|
|
||||||
|
@mock.patch('nova.api.openstack.placement.policy.authorize',
|
||||||
|
side_effect=exception.PolicyNotAuthorized(
|
||||||
|
action='placement:resource_providers:list'))
|
||||||
|
def test_can_target_none_fatal_false_reject(self, mock_authorize):
|
||||||
|
self.assertFalse(self.ctxt.can('placement:resource_providers:list',
|
||||||
|
fatal=False))
|
||||||
|
mock_authorize.assert_called_once_with(
|
||||||
|
self.ctxt, 'placement:resource_providers:list',
|
||||||
|
self.default_target)
|
||||||
|
|
||||||
|
@mock.patch('nova.api.openstack.placement.policy.authorize',
|
||||||
|
return_value=True)
|
||||||
|
def test_can_target_none_fatal_true_accept_custom_target(
|
||||||
|
self, mock_authorize):
|
||||||
|
class MyObj(object):
|
||||||
|
user_id = project_id = 'fake2'
|
||||||
|
|
||||||
|
target = MyObj()
|
||||||
|
self.assertTrue(self.ctxt.can('placement:resource_providers:list',
|
||||||
|
target=target))
|
||||||
|
mock_authorize.assert_called_once_with(
|
||||||
|
self.ctxt, 'placement:resource_providers:list', target)
|
80
nova/tests/unit/api/openstack/placement/test_policy.py
Normal file
80
nova/tests/unit/api/openstack/placement/test_policy.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from oslo_policy import policy as oslo_policy
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from nova.api.openstack.placement import context
|
||||||
|
from nova.api.openstack.placement import exception
|
||||||
|
from nova.api.openstack.placement import policy
|
||||||
|
from nova.tests.unit import conf_fixture
|
||||||
|
from nova.tests.unit import policy_fixture
|
||||||
|
from nova import utils
|
||||||
|
|
||||||
|
|
||||||
|
class PlacementPolicyTestCase(testtools.TestCase):
|
||||||
|
"""Tests interactions with placement policy.
|
||||||
|
|
||||||
|
These tests do not rely on the base nova.test.TestCase to avoid
|
||||||
|
interference from the PlacementPolicyFixture which is not used in all
|
||||||
|
test cases.
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
super(PlacementPolicyTestCase, self).setUp()
|
||||||
|
self.conf = self.useFixture(conf_fixture.ConfFixture()).conf
|
||||||
|
self.ctxt = context.RequestContext(user_id='fake', project_id='fake')
|
||||||
|
self.target = {'user_id': 'fake', 'project_id': 'fake'}
|
||||||
|
|
||||||
|
def test_modified_policy_reloads(self):
|
||||||
|
"""Creates a temporary placement-policy.yaml file and tests
|
||||||
|
authorizations against a fake rule between updates to the physical
|
||||||
|
policy file.
|
||||||
|
"""
|
||||||
|
with utils.tempdir() as tmpdir:
|
||||||
|
tmpfilename = os.path.join(tmpdir, 'placement-policy.yaml')
|
||||||
|
|
||||||
|
self.conf.set_default(
|
||||||
|
'policy_file', tmpfilename, group='placement')
|
||||||
|
|
||||||
|
action = 'placement:test'
|
||||||
|
# Expect PolicyNotRegistered since defaults are not yet loaded.
|
||||||
|
self.assertRaises(oslo_policy.PolicyNotRegistered,
|
||||||
|
policy.authorize, self.ctxt, action, self.target)
|
||||||
|
|
||||||
|
# Load the default action and rule (defaults to "any").
|
||||||
|
enforcer = policy.get_enforcer()
|
||||||
|
rule = oslo_policy.RuleDefault(action, '')
|
||||||
|
enforcer.register_default(rule)
|
||||||
|
|
||||||
|
# Now auth should work because the action is registered and anyone
|
||||||
|
# can perform the action.
|
||||||
|
policy.authorize(self.ctxt, action, self.target)
|
||||||
|
|
||||||
|
# Now update the policy file and reload it to disable the action
|
||||||
|
# from all users.
|
||||||
|
with open(tmpfilename, "w") as policyfile:
|
||||||
|
policyfile.write('"%s": "!"' % action)
|
||||||
|
enforcer.load_rules(force_reload=True)
|
||||||
|
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
|
||||||
|
self.ctxt, action, self.target)
|
||||||
|
|
||||||
|
def test_authorize_do_raise_false(self):
|
||||||
|
"""Tests that authorize does not raise an exception when the check
|
||||||
|
fails.
|
||||||
|
"""
|
||||||
|
fixture = self.useFixture(policy_fixture.PlacementPolicyFixture())
|
||||||
|
fixture.set_rules({'placement': '!'})
|
||||||
|
self.assertFalse(
|
||||||
|
policy.authorize(
|
||||||
|
self.ctxt, 'placement', self.target, do_raise=False))
|
@ -18,6 +18,7 @@ import fixtures
|
|||||||
from oslo_policy import policy as oslo_policy
|
from oslo_policy import policy as oslo_policy
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
|
|
||||||
|
from nova.api.openstack.placement import policy as placement_policy
|
||||||
import nova.conf
|
import nova.conf
|
||||||
from nova.conf import paths
|
from nova.conf import paths
|
||||||
from nova import policies
|
from nova import policies
|
||||||
@ -126,3 +127,32 @@ class RoleBasedPolicyFixture(RealPolicyFixture):
|
|||||||
self.policy_file = os.path.join(self.policy_dir.path, 'policy.json')
|
self.policy_file = os.path.join(self.policy_dir.path, 'policy.json')
|
||||||
with open(self.policy_file, 'w') as f:
|
with open(self.policy_file, 'w') as f:
|
||||||
jsonutils.dump(policy, f)
|
jsonutils.dump(policy, f)
|
||||||
|
|
||||||
|
|
||||||
|
class PlacementPolicyFixture(fixtures.Fixture):
|
||||||
|
"""Load the default placement policy for tests.
|
||||||
|
|
||||||
|
This fixture requires nova.tests.unit.conf_fixture.ConfFixture.
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
super(PlacementPolicyFixture, self).setUp()
|
||||||
|
policy_file = paths.state_path_def('etc/nova/placement-policy.yaml')
|
||||||
|
CONF.set_override('policy_file', policy_file, group='placement')
|
||||||
|
placement_policy.reset()
|
||||||
|
placement_policy.init()
|
||||||
|
self.addCleanup(placement_policy.reset)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_rules(rules, overwrite=True):
|
||||||
|
"""Set placement policy rules.
|
||||||
|
|
||||||
|
.. note:: The rules must first be registered via the
|
||||||
|
Enforcer.register_defaults method.
|
||||||
|
|
||||||
|
:param rules: dict of action=rule mappings to set
|
||||||
|
:param overwrite: Whether to overwrite current rules or update them
|
||||||
|
with the new rules.
|
||||||
|
"""
|
||||||
|
enforcer = placement_policy.get_enforcer()
|
||||||
|
enforcer.set_rules(oslo_policy.Rules.from_dict(rules),
|
||||||
|
overwrite=overwrite)
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
It is now possible to configure granular policy rules for placement
|
||||||
|
REST API operations.
|
||||||
|
|
||||||
|
By default, all operations continue to use the ``role:admin`` check string
|
||||||
|
so there is no upgrade impact.
|
||||||
|
|
||||||
|
A new configuration option is introduced, ``[placement]/policy_file``,
|
||||||
|
which is used to configure the location of the placement policy file.
|
||||||
|
By default, the ``placement-policy.yaml`` file may live alongside the
|
||||||
|
nova policy file, e.g.:
|
||||||
|
|
||||||
|
* /etc/nova/policy.yaml
|
||||||
|
* /etc/nova/placement-policy.yaml
|
||||||
|
|
||||||
|
However, if desired, ``[placement]/policy_file`` makes it possible to
|
||||||
|
package and deploy the placement policy file separately to make the future
|
||||||
|
split of placement and nova packages easier, e.g.:
|
||||||
|
|
||||||
|
* /etc/placement/policy.yaml
|
||||||
|
|
||||||
|
All placement policy rules are defined in code so by default no extra
|
||||||
|
configuration is required and the default rules will be used on start of
|
||||||
|
the placement service.
|
||||||
|
|
||||||
|
For more information about placement policy including a sample file, see
|
||||||
|
the configuration reference documentation:
|
||||||
|
|
||||||
|
https://docs.openstack.org/nova/latest/configuration/index.html#placement-policy
|
@ -44,7 +44,7 @@ oslo.utils>=3.33.0 # Apache-2.0
|
|||||||
oslo.db>=4.27.0 # Apache-2.0
|
oslo.db>=4.27.0 # Apache-2.0
|
||||||
oslo.rootwrap>=5.8.0 # Apache-2.0
|
oslo.rootwrap>=5.8.0 # Apache-2.0
|
||||||
oslo.messaging>=5.29.0 # Apache-2.0
|
oslo.messaging>=5.29.0 # Apache-2.0
|
||||||
oslo.policy>=1.30.0 # Apache-2.0
|
oslo.policy>=1.35.0 # Apache-2.0
|
||||||
oslo.privsep>=1.23.0 # Apache-2.0
|
oslo.privsep>=1.23.0 # Apache-2.0
|
||||||
oslo.i18n>=3.15.3 # Apache-2.0
|
oslo.i18n>=3.15.3 # Apache-2.0
|
||||||
oslo.service!=1.28.1,>=1.24.0 # Apache-2.0
|
oslo.service!=1.28.1,>=1.24.0 # Apache-2.0
|
||||||
|
@ -40,6 +40,7 @@ oslo.config.opts.defaults =
|
|||||||
|
|
||||||
oslo.policy.enforcer =
|
oslo.policy.enforcer =
|
||||||
nova = nova.policy:get_enforcer
|
nova = nova.policy:get_enforcer
|
||||||
|
placement = nova.api.openstack.placement.policy:get_enforcer
|
||||||
|
|
||||||
oslo.policy.policies =
|
oslo.policy.policies =
|
||||||
# The sample policies will be ordered by entry point and then by list
|
# The sample policies will be ordered by entry point and then by list
|
||||||
@ -47,6 +48,7 @@ oslo.policy.policies =
|
|||||||
# list_rules method into a separate entry point rather than using the
|
# list_rules method into a separate entry point rather than using the
|
||||||
# aggregate method.
|
# aggregate method.
|
||||||
nova = nova.policies:list_rules
|
nova = nova.policies:list_rules
|
||||||
|
placement = nova.api.openstack.placement.policies:list_rules
|
||||||
|
|
||||||
nova.compute.monitors.cpu =
|
nova.compute.monitors.cpu =
|
||||||
virt_driver = nova.compute.monitors.cpu.virt_driver:Monitor
|
virt_driver = nova.compute.monitors.cpu.virt_driver:Monitor
|
||||||
|
3
tox.ini
3
tox.ini
@ -116,6 +116,9 @@ commands = oslo-config-generator --config-file=etc/nova/nova-config-generator.co
|
|||||||
[testenv:genpolicy]
|
[testenv:genpolicy]
|
||||||
commands = oslopolicy-sample-generator --config-file=etc/nova/nova-policy-generator.conf
|
commands = oslopolicy-sample-generator --config-file=etc/nova/nova-policy-generator.conf
|
||||||
|
|
||||||
|
[testenv:genplacementpolicy]
|
||||||
|
commands = oslopolicy-sample-generator --config-file=etc/nova/placement-policy-generator.conf
|
||||||
|
|
||||||
[testenv:cover]
|
[testenv:cover]
|
||||||
# Also do not run test_coverage_ext tests while gathering coverage as those
|
# Also do not run test_coverage_ext tests while gathering coverage as those
|
||||||
# tests conflict with coverage.
|
# tests conflict with coverage.
|
||||||
|
Loading…
Reference in New Issue
Block a user