Project Scoping Node endpoint

* Adds additional policies:
  * baremetal:node_get:last_error
  * baremetal:node:get:reservation
  * baremetal:node:get:driver_internal_info
  * baremetal:node:get:driver_info
  * baremetal:node:update:driver_info
  * baremetal:node:update:properties
  * baremetal:node:update:chassis_uuid
  * baremetal:node:update:instance_uuid
  * baremetal:node:update:lessee
  * baremetal:node:update:driver_interfaces
  * baremetal:node:update:network_data
  * baremetal:node:update:conductor_group
  * baremetal:node:update:name

* With new policies, responses of filtering and posted data is
  performed. Testing has been added to the RBAC testing files
  to align with this and the defaults where pertinant.

* Adds another variation of the common policy check method
  which may be useful in the long term. This is too soon to
  tell, but the overall purpose is to allow similar logic
  patterns to the authorize behavior. This is because the
  standard policies are, at present, also used to control
  behavior of response, and node response sanitization needs
  to be carefully navigated.

This change excludes linked resources such as /nodes/<uuid>/ports,
portgroups, volumes/[targets|connectors]. Those will be in later
changes, as the node itself is quite a bit.

Special note:
* The indicator endpoint code in the API appears to be broken
  and given that should be fixed in a separate patch.

Change-Id: I2869bf21f761cfc543798cf1f7d97c5500cd3681
This commit is contained in:
Julia Kreger 2021-02-03 07:10:08 -08:00
parent 5857fa802d
commit f1641468bb
13 changed files with 1057 additions and 400 deletions

View File

@ -54,6 +54,7 @@ Advanced Topics
Deploying without BMC Credentials <agent-power>
Layer 3 or DHCP-less Ramdisk Booting <dhcp-less>
Tuning Ironic <tuning>
Role Based Access Control <secure-rbac>
.. toctree::
:hidden:

View File

@ -0,0 +1,119 @@
===========
Secure RBAC
===========
System Scoped
=============
.. todo: Need to be filled out in an earlier patch most likely.
Project Scoped
==============
Project scoped authentication is when a request token and associated records
indicate an associated ``project_id`` value.
Legacy Behavior
---------------
The legacy behavior of API service is that all requests are treated as
project scoped requests where access is governed using an "admin project".
This behavior is *deprecated*. The new behavior is a delineation of
access through ``system`` scoped and ``project`` scoped requests.
In essence, what would have served as an "admin project", is now ``system``
scoped usage.
Previously, Ironic API, by default, responded with access denied or permitted
based upon the admin project and associated role. These responses would
generate an HTTP 403 if the project was incorrect or if a user role.
.. NOTE:: While Ironic has had the concept of an ``owner`` and a
``lessee``, they are *NOT* used by default. They require
custom policy configuration files to be used in the legacy
operating mode.
Supported Endpoints
-------------------
* /nodes
How Project Scoped Works
------------------------
Ironic has two project use models where access is generally more delagative
to an ``owner`` where access to a ``lessee`` is generally more utilitarian.
The purpose of an owner, is more to enable the System Operator to delegate
much of the administrative activity of a Node to the owner.
This may be because they physically own the hardware, or they are in charge
of the node. Regardless of the use model that the fields and mechanics
support, these fields are to support humans, and possibly services where
applicable.
.. todo: Add link to owner spec.
The purpose of a lessee is more for a *tenant* in their *project* to
be able to have access to perform basic actions with the API. In some cases
that may be to reprovision or rebuild a node. Ultimately that is the lessee's
progative, but by default there are actions and field updates that cannot
be performed by default. This is also governed by access level within
a project.
.. todo: Add link to lessee spec.
These policies are applied in the way data is viewed and how data can be
updated. Generally, an inability to view a node is an access permission issue
in term of the project ID being correct for owner/lessee.
The ironic project has attempted to generally codify what we believe is
reasonable, however operators may wish to override these policy settings.
For details general policy setting details, please see
:doc:`/configuration/policy`.
Field value visibility restrictions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ironic's API, by default has a concept of filtering node values to prevent
sensitive data from being leaked. System scoped users are subjected to basic
restrictions, where as project scoped users are, by default, examined further
and against additional policies. This threshold is controlled with the
``baremetal:node:get:filter_threshold``.
By default, the following fields are masked on Nodes and are controlled by the
associated policies. By default, owner's are able to see insight into the
infrastucture, where as lessee users *CANNOT* view these fields by default.
* ``last_error`` - ``baremetal:node:get:last_error``
* ``reservation`` - ``baremetal:node:get:reservation``
* ``driver_internal_info`` - ``baremetal:node:get:driver_internal_info``
* ``driver_info`` - ``baremetal:node:get:driver_info``
Field update restrictions
~~~~~~~~~~~~~~~~~~~~~~~~~
Some of the fields in this list are restricted to System scoped users,
or even only System Administrators. Some of these default restrictions
are likely obvious. Owners can't change the owner. Lessee's can't
change the owner.
* ``driver_info`` - ``baremetal:node:update:driver_info``
* ``properties`` - ``baremetal:node:update:properties``
* ``chassis_uuid`` - ``baremetal:node:update:chassis_uuid``
* ``instance_uuid`` - ``baremetal:node:update:instance_uuid``
* ``lessee`` - ``baremetal:node:update:lessee``
* ``owner`` - ``baremetal:node:update:owner``
* ``driver`` - ``baremetal:node:update:driver_interfaces``
* ``*_interface`` - ``baremetal:node:update:driver_interfaces``
* ``network_data`` - ``baremetal:node:update:network_data``
* ``conductor_group`` - ``baremetal:node:update:conductor_group``
* ``name`` - ``baremetal:node:update:name``
* ``retired`` - ``baremetal:node:update:driver_info``
* ``retired_reason`` - ``baremetal:node:update:retired``
.. WARNING:: The ``chassis_uuid`` field is a write-once-only field. As such
it is restricted to system scoped administrators.
More information is available on these fields in :doc:`/configuration/policy`.

View File

@ -23,6 +23,13 @@ OpenStack deployment.
REST API: user roles and policy settings
========================================
.. WARNING::
This information is presently in flux as of the Wallaby release with the
implementation of ``Secure RBAC`` where ``system`` and ``project``
scoped requests are able to be parsed and default access controls support
a delineation of roles and responsibilities through the roles.
Please see :doc:`/admin/secure-rbac`.
Beginning with the Newton (6.1.0) release, the Bare Metal service allows
operators significant control over API access:

View File

@ -56,9 +56,9 @@ class NodeBiosController(rest.RestController):
@method.expose()
def get_all(self):
"""List node bios settings."""
api_utils.check_policy('baremetal:node:bios:get')
node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:bios:get', self.node_ident)
node = api_utils.get_rpc_node(self.node_ident)
settings = objects.BIOSSettingList.get_by_node_id(
api.request.context, node.id)
return collection_from_list(self.node_ident, settings)
@ -71,9 +71,9 @@ class NodeBiosController(rest.RestController):
:param setting_name: Logical name of the setting to retrieve.
"""
api_utils.check_policy('baremetal:node:bios:get')
node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:bios:get', self.node_ident)
node = api_utils.get_rpc_node(self.node_ident)
try:
setting = objects.BIOSSetting.get(api.request.context, node.id,
setting_name)

View File

@ -47,6 +47,7 @@ from ironic.common import policy
from ironic.common import states as ir_states
from ironic.conductor import steps as conductor_steps
import ironic.conf
from ironic.drivers import base as driver_base
from ironic import objects
@ -513,9 +514,9 @@ class IndicatorController(rest.RestController):
mod:`ironic.common.indicator_states`.
"""
api_utils.check_policy('baremetal:node:set_indicator_state')
rpc_node = api_utils.get_rpc_node(node_ident)
rpc_node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:set_indicator_state',
node_ident)
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
indicator_at_component = IndicatorAtComponent(unique_name=indicator)
pecan.request.rpcapi.set_indicator_state(
@ -535,9 +536,9 @@ class IndicatorController(rest.RestController):
:returns: a dict with the "state" key and one of
mod:`ironic.common.indicator_states` as a value.
"""
api_utils.check_policy('baremetal:node:get_indicator_state')
rpc_node = api_utils.get_rpc_node(node_ident)
rpc_node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:get_indicator_state',
node_ident)
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
indicator_at_component = IndicatorAtComponent(unique_name=indicator)
state = pecan.request.rpcapi.get_indicator_state(
@ -558,9 +559,9 @@ class IndicatorController(rest.RestController):
(from `get_supported_indicators`) as values.
"""
api_utils.check_policy('baremetal:node:get_indicator_state')
rpc_node = api_utils.get_rpc_node(node_ident)
rpc_node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:get_indicator_state',
node_ident)
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
indicators = pecan.request.rpcapi.get_supported_indicators(
pecan.request.context, rpc_node.uuid, topic=topic)
@ -1324,13 +1325,64 @@ def node_sanitize(node, fields):
:type fields: list of str
"""
cdict = api.request.context.to_policy_values()
target_dict = dict(cdict)
owner = node.get('owner')
lessee = node.get('lessee')
if owner:
target_dict['node.owner'] = owner
if lessee:
target_dict['node.lessee'] = lessee
# NOTE(tenbrae): the 'show_password' policy setting name exists for
# legacy purposes and can not be changed. Changing it will
# cause upgrade problems for any operators who have
# customized the value of this field
show_driver_secrets = policy.check("show_password", cdict, cdict)
# NOTE(TheJulia): These methods use policy.check and normally return
# False in a noauth or password auth based situation, because the
# effective caller doesn't match the policy check rule.
show_driver_secrets = policy.check("show_password", cdict, target_dict)
show_instance_secrets = policy.check("show_instance_secrets",
cdict, cdict)
cdict, target_dict)
# TODO(TheJulia): The above checks need to be migrated in some direction,
# but until we have auditing clarity, it might not be a big deal.
# Determine if we need to do the additional checks. Keep in mind
# nova integrated with ironic is API read heavy, so it is ideal
# to keep the policy checks for say system-member based roles to
# a minimum as they are likely the regular API users as well.
# Also, the default for the filter_threshold is system-member.
evaluate_additional_policies = not policy.check_policy(
"baremetal:node:get:filter_threshold",
target_dict, cdict)
if evaluate_additional_policies:
# NOTE(TheJulia): The net effect of this is that by default,
# at least matching common/policy.py defaults. is these should
# be stripped out.
if not policy.check("baremetal:node:get:last_error",
target_dict, cdict):
# Guard the last error from being visible as it can contain
# hostnames revealing infrastucture internal details.
node['last_error'] = ('** Value Redacted - Requires '
'baremetal:node:get:last_error '
'permission. **')
if not policy.check("baremetal:node:get:reservation",
target_dict, cdict):
# Guard conductor names from being visible.
node['reservation'] = ('** Redacted - requires baremetal:'
'node:get:reservation permission. **')
if not policy.check("baremetal:node:get:driver_internal_info",
target_dict, cdict):
# Guard conductor names from being visible.
node['driver_internal_info'] = {
'content': '** Redacted - Requires baremetal:node:get:'
'driver_internal_info permission. **'}
if not policy.check("baremetal:node:get:driver_info",
target_dict, cdict):
# Guard infrastructure intenral details from being visible.
node['driver_info'] = {
'content': '** Redacted - requires baremetal:node:get:'
'driver_info permission. **'}
if not show_driver_secrets and node.get('driver_info'):
node['driver_info'] = strutils.mask_dict_password(
@ -2148,6 +2200,36 @@ class NodesController(rest.RestController):
and strutils.bool_from_string(p['value'], default=None)
is False):
policy_checks.append('baremetal:node:disable_cleaning')
elif p['path'].startswith('/driver_info'):
policy_checks.append('baremetal:node:update:driver_info')
elif p['path'].startswith('/properties'):
policy_checks.append('baremetal:node:update:properties')
elif p['path'].startswith('/chassis_uuid'):
policy_checks.append('baremetal:node:update:chassis_uuid')
elif p['path'].startswith('/instance_uuid'):
policy_checks.append('baremetal:node:update:instance_uuid')
elif p['path'].startswith('/lessee'):
policy_checks.append('baremetal:node:update:lessee')
elif p['path'].startswith('/owner'):
policy_checks.append('baremetal:node:update:owner')
elif p['path'].startswith('/driver'):
policy_checks.append('baremetal:node:update:driver_interfaces')
elif ((p['path'].lstrip('/').rsplit(sep="_", maxsplit=1)[0]
in driver_base.ALL_INTERFACES)
and (p['path'].lstrip('/').rsplit(sep="_", maxsplit=1)[-1]
== "interface")):
# TODO(TheJulia): Replace the above check with something like
# elif (p['path'].lstrip('/').removesuffix('_interface')
# when the minimum supported version is Python 3.9.
policy_checks.append('baremetal:node:update:driver_interfaces')
elif p['path'].startswith('/network_data'):
policy_checks.append('baremetal:node:update:network_data')
elif p['path'].startswith('/conductor_group'):
policy_checks.append('baremetal:node:update:conductor_group')
elif p['path'].startswith('/name'):
policy_checks.append('baremetal:node:update:name')
elif p['path'].startswith('/retired'):
policy_checks.append('baremetal:node:update:retired')
else:
generic_update = True

View File

@ -25,6 +25,7 @@ import jsonschema
from jsonschema import exceptions as json_schema_exc
import os_traits
from oslo_config import cfg
from oslo_policy import policy as oslo_policy
from oslo_utils import uuidutils
from pecan import rest
@ -1502,23 +1503,34 @@ def check_policy(policy_name):
policy.authorize(policy_name, cdict, api.request.context)
def check_owner_policy(object_type, policy_name, owner, lessee=None):
def check_owner_policy(object_type, policy_name, owner, lessee=None,
conceal_node=False):
"""Check if the policy authorizes this request on an object.
:param: object_type: type of object being checked
:param: policy_name: Name of the policy to check.
:param: owner: the owner
:param: lessee: the lessee
:param: conceal_node: the UUID of the node IF we should
conceal the existence of the node with a
404 Error instead of a 403 Error.
:raises: HTTPForbidden if the policy forbids access.
"""
cdict = api.request.context.to_policy_values()
target_dict = dict(cdict)
target_dict[object_type + '.owner'] = owner
if lessee:
target_dict[object_type + '.lessee'] = lessee
policy.authorize(policy_name, target_dict, api.request.context)
try:
policy.authorize(policy_name, target_dict, api.request.context)
except exception.HTTPForbidden:
if conceal_node:
# The caller does NOT have access to the node and we've been told
# we should return a 404 instead of HTTPForbidden.
raise exception.NodeNotFound(node=conceal_node)
else:
raise
def check_node_policy_and_retrieve(policy_name, node_ident,
@ -1533,20 +1545,30 @@ def check_node_policy_and_retrieve(policy_name, node_ident,
:raises: NodeNotFound if the node is not found.
:return: RPC node identified by node_ident
"""
conceal_node = False
try:
if with_suffix:
rpc_node = get_rpc_node_with_suffix(node_ident)
else:
rpc_node = get_rpc_node(node_ident)
except exception.NodeNotFound:
# don't expose non-existence of node unless requester
# has generic access to policy
cdict = api.request.context.to_policy_values()
policy.authorize(policy_name, cdict, api.request.context)
raise
# Project scoped users will get a 404 where as system
# scoped should get a 403
cdict = api.request.context.to_policy_values()
if cdict.get('project_id', False):
conceal_node = node_ident
try:
# Always check the ability to see the node BEFORE anything else.
check_owner_policy('node', 'baremetal:node:get', rpc_node['owner'],
rpc_node['lessee'], conceal_node=conceal_node)
except exception.NotAuthorized:
raise exception.NodeNotFound(node=node_ident)
# If we've reached here, we can see the node and we have
# access to view it.
check_owner_policy('node', policy_name,
rpc_node['owner'], rpc_node['lessee'])
rpc_node['owner'], rpc_node['lessee'],
conceal_node=False)
return rpc_node
@ -1612,7 +1634,9 @@ def check_list_policy(object_type, owner=None):
try:
policy.authorize('baremetal:%s:list_all' % object_type,
cdict, api.request.context)
except exception.HTTPForbidden:
except (exception.HTTPForbidden, oslo_policy.InvalidScope):
# In the event the scoped policy fails, falling back to the
# policy governing a filtered view.
project_owner = cdict.get('project_id')
if (not project_owner or (owner and owner != project_owner)):
raise

View File

@ -80,13 +80,36 @@ PROJECT_READER = ('role:reader and '
# protecting APIs designed to operate with multiple scopes (e.g., a system
# administrator should be able to delete any baremetal host in the deployment,
# a project member should only be able to delete hosts in their project).
SYSTEM_ADMIN_OR_PROJECT_MEMBER = (
'(' + SYSTEM_ADMIN + ') or (' + PROJECT_MEMBER + ')'
SYSTEM_OR_PROJECT_MEMBER = (
'(' + SYSTEM_MEMBER + ') or (' + PROJECT_MEMBER + ')'
)
SYSTEM_OR_PROJECT_READER = (
'(' + SYSTEM_READER + ') or (' + PROJECT_READER + ')'
)
PROJECT_OWNER_ADMIN = ('role:admin and project_id:%(node.owner)s')
PROJECT_OWNER_MEMBER = ('role:member and project_id:%(node.owner)s')
PROJECT_OWNER_READER = ('role:reader and project_id:%(node.owner)s')
PROJECT_LESSEE_ADMIN = ('role:admin and project_id:%(node.lessee)s')
SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN = (
'(' + SYSTEM_MEMBER + ') or (' + PROJECT_OWNER_MEMBER + ') or (' + PROJECT_LESSEE_ADMIN + ')' # noqa
)
SYSTEM_MEMBER_OR_OWNER_ADMIN = (
'(' + SYSTEM_MEMBER + ') or (' + PROJECT_OWNER_ADMIN + ')'
)
SYSTEM_MEMBER_OR_OWNER_MEMBER = (
'(' + SYSTEM_MEMBER + ') or (' + PROJECT_OWNER_MEMBER + ')'
)
SYSTEM_OR_OWNER_READER = (
'(' + SYSTEM_READER + ') or (' + PROJECT_OWNER_READER + ')'
)
API_READER = ('role:reader')
default_policies = [
# Legacy setting, don't remove. Likely to be overridden by operators who
# forget to update their policy.json configuration file.
@ -282,21 +305,12 @@ node_policies = [
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:get',
check_str=SYSTEM_READER,
scope_types=['system'],
description='Retrieve a single Node record',
operations=[{'path': '/nodes/{node_ident}', 'method': 'GET'}],
deprecated_rule=deprecated_node_get,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:list',
check_str=SYSTEM_READER,
scope_types=['system'],
description='Retrieve multiple Node records, filtered by owner',
check_str=API_READER,
scope_types=['system', 'project'],
description='Retrieve multiple Node records, filtered by '
'an explicit owner or the client project_id',
operations=[{'path': '/nodes', 'method': 'GET'},
{'path': '/nodes/detail', 'method': 'GET'}],
deprecated_rule=deprecated_node_list,
@ -315,38 +329,246 @@ node_policies = [
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
description='Update Node records',
name='baremetal:node:get',
check_str=SYSTEM_OR_PROJECT_READER,
scope_types=['system', 'project'],
description='Retrieve a single Node record',
operations=[{'path': '/nodes/{node_ident}', 'method': 'GET'}],
deprecated_rule=deprecated_node_get,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:get:filter_threshold',
check_str=SYSTEM_READER,
scope_types=['system', 'project'],
description='Filter to allow operators to govern the threshold '
'where information should be filtered. Non-authorized '
'users will be subjected to additional API policy '
'checks for API content response bodies.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'GET'}],
# This rule fallsback to deprecated_node_get in order to provide a
# mechanism so the additional policies only engage in an updated
# operating context.
deprecated_rule=deprecated_node_get,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY,
),
policy.DocumentedRuleDefault(
name='baremetal:node:get:last_error',
check_str=SYSTEM_OR_OWNER_READER,
scope_types=['system', 'project'],
description='Governs if the node last_error field is masked from API'
'clients with insufficent privileges.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'GET'}],
deprecated_rule=deprecated_node_get,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:get:reservation',
check_str=SYSTEM_OR_OWNER_READER,
scope_types=['system', 'project'],
description='Governs if the node reservation field is masked from API'
'clients with insufficent privileges.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'GET'}],
deprecated_rule=deprecated_node_get,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:get:driver_internal_info',
check_str=SYSTEM_OR_OWNER_READER,
scope_types=['system', 'project'],
description='Governs if the node driver_internal_info field is '
'masked from API clients with insufficent privileges.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'GET'}],
deprecated_rule=deprecated_node_get,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:get:driver_info',
check_str=SYSTEM_OR_OWNER_READER,
scope_types=['system', 'project'],
description='Governs if the driver_info field is masked from API'
'clients with insufficent privileges.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'GET'}],
deprecated_rule=deprecated_node_get,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:driver_info',
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Governs if node driver_info field can be updated via '
'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:properties',
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Governs if node properties field can be updated via '
'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:chassis_uuid',
check_str=SYSTEM_ADMIN,
scope_types=['system', 'project'],
description='Governs if node chassis_uuid field can be updated via '
'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:instance_uuid',
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Governs if node instance_uuid field can be updated via '
'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:lessee',
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Governs if node lessee field can be updated via '
'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:owner',
check_str=SYSTEM_MEMBER,
scope_types=['system', 'project'],
description='Governs if node owner field can be updated via '
'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:driver_interfaces',
check_str=SYSTEM_MEMBER_OR_OWNER_ADMIN,
scope_types=['system', 'project'],
description='Governs if node driver and driver interfaces field '
'can be updated via the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:network_data',
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Governs if node driver_info field can be updated via '
'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:conductor_group',
check_str=SYSTEM_MEMBER,
scope_types=['system', 'project'],
description='Governs if node conductor_group field can be updated '
'via the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:name',
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Governs if node name field can be updated via '
'the API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:update:retired',
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Governs if node retired and retired reason '
'can be updated by API clients.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
# If this role is denied we should likely roll into the other rules
# Like, this rule could match "SYSTEM_MEMBER" by default and then drill
# further into each field, which would maintain what we do today, and
# enable further testing.
policy.DocumentedRuleDefault(
name='baremetal:node:update',
check_str=SYSTEM_OR_PROJECT_MEMBER,
scope_types=['system', 'project'],
description='Generalized update of node records',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
# TODO(TheJulia): Explicit RBAC testing needed for this.
policy.DocumentedRuleDefault(
name='baremetal:node:update_extra',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_OR_PROJECT_MEMBER,
scope_types=['system', 'project'],
description='Update Node extra field',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update_extra,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
# TODO(TheJulia): Explicit RBAC testing needed for this.
# TODO(TheJulia): So multiple additional fields need policies. This needs
# to be reviewed/audited/addressed.
# * Get ability on last_error - policy added
# * Get ability on reservation (conductor names) - policy added
# * get ability on driver_internal_info (internal addressing) added
# * ability to get driver_info - policy added
# * ability to set driver_info - policy added
# * ability to set properties. - added
# * ability to set chassis_uuid - added
# * ability to set instance_uuid - added
# * ability to set a lessee - default only to admin or owner. added
# * ability to set driver/*_interface - added
# * ability to set network_data - added
# * ability to set conductor_group -added
# * ability to set name -added
policy.DocumentedRuleDefault(
name='baremetal:node:update_instance_info',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,
scope_types=['system', 'project'],
description='Update Node instance_info field',
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
deprecated_rule=deprecated_node_update_instance_info,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
# TODO(TheJulia): Explicit RBAC testing needed for this.
policy.DocumentedRuleDefault(
name='baremetal:node:update_owner_provisioned',
check_str=SYSTEM_MEMBER,
@ -357,11 +579,10 @@ node_policies = [
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
# TODO(TheJulia): Explicit RBAC testing needed for this... Maybe?
policy.DocumentedRuleDefault(
name='baremetal:node:delete',
check_str=SYSTEM_ADMIN,
scope_types=['system'],
scope_types=['system', 'project'],
description='Delete Node records',
operations=[{'path': '/nodes/{node_ident}', 'method': 'DELETE'}],
deprecated_rule=deprecated_node_delete,
@ -371,8 +592,8 @@ node_policies = [
policy.DocumentedRuleDefault(
name='baremetal:node:validate',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,
scope_types=['system', 'project'],
description='Request active validation of Nodes',
operations=[
{'path': '/nodes/{node_ident}/validate', 'method': 'GET'}
@ -384,8 +605,8 @@ node_policies = [
policy.DocumentedRuleDefault(
name='baremetal:node:set_maintenance',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,
scope_types=['system', 'project'],
description='Set maintenance flag, taking a Node out of service',
operations=[
{'path': '/nodes/{node_ident}/maintenance', 'method': 'PUT'}
@ -396,8 +617,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:clear_maintenance',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,
scope_types=['system', 'project'],
description=(
'Clear maintenance flag, placing the Node into service again'
),
@ -413,8 +634,8 @@ node_policies = [
# a cached object.
policy.DocumentedRuleDefault(
name='baremetal:node:get_boot_device',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_ADMIN,
scope_types=['system', 'project'],
description='Retrieve Node boot device metadata',
operations=[
{'path': '/nodes/{node_ident}/management/boot_device',
@ -428,8 +649,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:set_boot_device',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_ADMIN,
scope_types=['system', 'project'],
description='Change Node boot device',
operations=[
{'path': '/nodes/{node_ident}/management/boot_device',
@ -442,8 +663,8 @@ node_policies = [
policy.DocumentedRuleDefault(
name='baremetal:node:get_indicator_state',
check_str=SYSTEM_READER,
scope_types=['system'],
check_str=SYSTEM_OR_PROJECT_READER,
scope_types=['system', 'project'],
description='Retrieve Node indicators and their states',
operations=[
{'path': '/nodes/{node_ident}/management/indicators/'
@ -458,8 +679,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:set_indicator_state',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Change Node indicator state',
operations=[
{'path': '/nodes/{node_ident}/management/indicators/'
@ -473,8 +694,8 @@ node_policies = [
policy.DocumentedRuleDefault(
name='baremetal:node:inject_nmi',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_ADMIN,
scope_types=['system', 'project'],
description='Inject NMI for a node',
operations=[
{'path': '/nodes/{node_ident}/management/inject_nmi',
@ -487,8 +708,8 @@ node_policies = [
policy.DocumentedRuleDefault(
name='baremetal:node:get_states',
check_str=SYSTEM_READER,
scope_types=['system'],
check_str=SYSTEM_OR_PROJECT_READER,
scope_types=['system', 'project'],
description='View Node power and provision state',
operations=[{'path': '/nodes/{node_ident}/states', 'method': 'GET'}],
deprecated_rule=deprecated_node_get_states,
@ -497,8 +718,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:set_power_state',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_OR_PROJECT_MEMBER,
scope_types=['system', 'project'],
description='Change Node power status',
operations=[
{'path': '/nodes/{node_ident}/states/power', 'method': 'PUT'}
@ -509,8 +730,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:set_provision_state',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,
scope_types=['system', 'project'],
description='Change Node provision status',
operations=[
{'path': '/nodes/{node_ident}/states/provision', 'method': 'PUT'}
@ -521,8 +742,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:set_raid_state',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Change Node RAID status',
operations=[
{'path': '/nodes/{node_ident}/states/raid', 'method': 'PUT'}
@ -533,8 +754,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:get_console',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Get Node console connection information',
operations=[
{'path': '/nodes/{node_ident}/states/console', 'method': 'GET'}
@ -545,8 +766,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:set_console_state',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_MEMBER,
scope_types=['system', 'project'],
description='Change Node console status',
operations=[
{'path': '/nodes/{node_ident}/states/console', 'method': 'PUT'}
@ -558,8 +779,8 @@ node_policies = [
policy.DocumentedRuleDefault(
name='baremetal:node:vif:list',
check_str=SYSTEM_READER,
scope_types=['system'],
check_str=SYSTEM_OR_PROJECT_READER,
scope_types=['system', 'project'],
description='List VIFs attached to node',
operations=[{'path': '/nodes/{node_ident}/vifs', 'method': 'GET'}],
deprecated_rule=deprecated_node_vif_list,
@ -568,8 +789,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:vif:attach',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,
scope_types=['system', 'project'],
description='Attach a VIF to a node',
operations=[{'path': '/nodes/{node_ident}/vifs', 'method': 'POST'}],
deprecated_rule=deprecated_node_vif_attach,
@ -578,8 +799,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:vif:detach',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,
scope_types=['system', 'project'],
description='Detach a VIF from a node',
operations=[
{'path': '/nodes/{node_ident}/vifs/{node_vif_ident}',
@ -589,11 +810,10 @@ node_policies = [
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:node:traits:list',
check_str=SYSTEM_READER,
scope_types=['system'],
check_str=SYSTEM_OR_PROJECT_READER,
scope_types=['system', 'project'],
description='List node traits',
operations=[{'path': '/nodes/{node_ident}/traits', 'method': 'GET'}],
deprecated_rule=deprecated_node_traits_list,
@ -602,8 +822,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:traits:set',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_ADMIN,
scope_types=['system', 'project'],
description='Add a trait to, or replace all traits of, a node',
operations=[
{'path': '/nodes/{node_ident}/traits', 'method': 'PUT'},
@ -615,8 +835,8 @@ node_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:node:traits:delete',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_ADMIN,
scope_types=['system', 'project'],
description='Remove one or all traits from a node',
operations=[
{'path': '/nodes/{node_ident}/traits', 'method': 'DELETE'},
@ -630,8 +850,8 @@ node_policies = [
policy.DocumentedRuleDefault(
name='baremetal:node:bios:get',
check_str=SYSTEM_READER,
scope_types=['system'],
check_str=SYSTEM_OR_PROJECT_READER,
scope_types=['system', 'project'],
description='Retrieve Node BIOS information',
operations=[
{'path': '/nodes/{node_ident}/bios', 'method': 'GET'},
@ -975,7 +1195,10 @@ vendor_passthru_policies = [
policy.DocumentedRuleDefault(
name='baremetal:node:vendor_passthru',
check_str=SYSTEM_ADMIN,
scope_types=['system'],
# NOTE(TheJulia): Project scope listed, but not a project scoped role
# as some operators may find it useful to provide access to say owner
# admins.
scope_types=['system', 'project'],
description='Access vendor-specific Node functions',
operations=[
{'path': 'nodes/{node_ident}/vendor_passthru/methods',
@ -1503,3 +1726,16 @@ def check(rule, target, creds, *args, **kwargs):
"""
enforcer = get_enforcer()
return enforcer.enforce(rule, target, creds, *args, **kwargs)
def check_policy(rule, target, creds, *args, **kwargs):
"""Configuration aware role policy check wrapper.
Checks authorization of a rule against the target and credentials
and returns True or False.
Always returns true if CONF.auth_strategy is not keystone.
"""
if CONF.auth_strategy != 'keystone':
return True
enforcer = get_enforcer()
return enforcer.enforce(rule, target, creds, *args, **kwargs)

View File

@ -1136,10 +1136,13 @@ class TestCheckNodePolicyAndRetrieve(base.TestCase):
rpc_node = utils.check_node_policy_and_retrieve(
'fake_policy', self.valid_node_uuid
)
authorize_calls = [
mock.call('baremetal:node:get', expected_target, fake_context),
mock.call('fake_policy', expected_target, fake_context)]
mock_grn.assert_called_once_with(self.valid_node_uuid)
mock_grnws.assert_not_called()
mock_authorize.assert_called_once_with(
'fake_policy', expected_target, fake_context)
mock_authorize.assert_has_calls(authorize_calls)
self.assertEqual(self.node, rpc_node)
@mock.patch.object(api, 'request', spec_set=["context", "version"])
@ -1162,14 +1165,16 @@ class TestCheckNodePolicyAndRetrieve(base.TestCase):
)
mock_grn.assert_not_called()
mock_grnws.assert_called_once_with(self.valid_node_uuid)
mock_authorize.assert_called_once_with(
'fake_policy', expected_target, fake_context)
authorize_calls = [
mock.call('baremetal:node:get', expected_target, fake_context),
mock.call('fake_policy', expected_target, fake_context)]
mock_authorize.assert_has_calls(authorize_calls)
self.assertEqual(self.node, rpc_node)
@mock.patch.object(api, 'request', spec_set=["context"])
@mock.patch.object(policy, 'authorize', spec=True)
@mock.patch.object(utils, 'get_rpc_node', autospec=True)
def test_check_node_policy_and_retrieve_no_node_policy_forbidden(
def test_check_node_policy_and_retrieve_no_node_policy_notfound(
self, mock_grn, mock_authorize, mock_pr
):
mock_pr.context.to_policy_values.return_value = {}
@ -1178,7 +1183,7 @@ class TestCheckNodePolicyAndRetrieve(base.TestCase):
node=self.valid_node_uuid)
self.assertRaises(
exception.HTTPForbidden,
exception.NodeNotFound,
utils.check_node_policy_and_retrieve,
'fake-policy',
self.valid_node_uuid
@ -1213,7 +1218,7 @@ class TestCheckNodePolicyAndRetrieve(base.TestCase):
mock_grn.return_value = self.node
self.assertRaises(
exception.HTTPForbidden,
exception.NodeNotFound,
utils.check_node_policy_and_retrieve,
'fake-policy',
self.valid_node_uuid

View File

@ -142,9 +142,22 @@ class TestACLBase(base.BaseApiTest):
)
else:
assert False, 'Unimplemented test method: %s' % method
# Once miggrated:
# Items will return:
# 403 - Trying to access something that is generally denied.
# Example: PATCH /v1/nodes/<uuid> as a reader.
# 404 - Trying to access something where we don't have permissions
# in a project scope. This is particularly true where implied
# permissions or assocation exists. Ports are attempted to be
# accessed when the underlying node is inaccessible as owner
# nor node matches.
# Example: GET /v1/portgroups or /v1/nodes/<uuid>/ports
# 500 - Attempting to access something such an system scoped endpoint
# with a project scoped request. Example: /v1/conductors.
if not (bool(deprecated)
and ('403' in response.status or '500' in response.status)
and ('404' in response.status
or '500' in response.status
or '403' in response.status)
and cfg.CONF.oslo_policy.enforce_scope
and cfg.CONF.oslo_policy.enforce_new_defaults):
# NOTE(TheJulia): Everything, once migrated, should
@ -152,7 +165,9 @@ class TestACLBase(base.BaseApiTest):
self.assertEqual(assert_status, response.status_int)
else:
self.assertTrue(
'403' in response.status or '500' in response.status)
'404' in response.status
or '500' in response.status
or '403' in response.status)
# We can't check the contents of the response if there is no
# response.
return
@ -163,8 +178,23 @@ class TestACLBase(base.BaseApiTest):
if assert_dict_contains:
for k, v in assert_dict_contains.items():
self.assertIn(k, response)
self.assertEqual(v.format(**self.format_data),
response.json[k])
print(k)
print(v)
if str(v) == "None":
# Compare since the variable loaded from the
# json ends up being null in json or None.
self.assertIsNone(response.json[k])
elif str(v) == "{}":
# Special match for signifying a dictonary.
self.assertEqual({}, response.json[k])
elif isinstance(v, dict):
# The value from the YAML can be a dictionary,
# which cannot be formatted, so we're likely doing
# direct matching.
self.assertEqual(str(v), str(response.json[k]))
else:
self.assertEqual(v.format(**self.format_data),
response.json[k])
if assert_list_length:
for root, length in assert_list_length.items():
@ -173,7 +203,14 @@ class TestACLBase(base.BaseApiTest):
# important for owner/lessee testing.
items = response.json[root]
self.assertIsInstance(items, list)
self.assertEqual(length, len(items))
if not (bool(deprecated)
and cfg.CONF.oslo_policy.enforce_scope):
self.assertEqual(length, len(items))
else:
# If we have scope enforcement, we likely have different
# views, such as "other" admins being subjected to
# a filtered view in these cases.
self.assertEqual(0, len(items))
# NOTE(TheJulia): API tests in Ironic tend to have a pattern
# to print request and response data to aid in development
@ -207,13 +244,15 @@ class TestRBACModelBeforeScopesBase(TestACLBase):
resource_class="CUSTOM_TEST")
fake_db_node = db_utils.create_test_node(
chassis_id=None,
driver='fake-driverz')
driver='fake-driverz',
owner='z')
fake_db_node_alloced = db_utils.create_test_node(
id=allocated_node_id,
chassis_id=None,
allocation_id=fake_db_allocation['id'],
uuid='22e26c0b-03f2-4d2e-ae87-c02d7f33c000',
driver='fake-driverz')
driver='fake-driverz',
owner='z')
fake_vif_port_id = "ee21d58f-5de2-4956-85ff-33935ea1ca00"
fake_db_port = db_utils.create_test_port(
node_id=fake_db_node['id'],
@ -242,7 +281,6 @@ class TestRBACModelBeforeScopesBase(TestACLBase):
# false positives with test runners.
db_utils.create_test_node(
uuid='18a552fb-dcd2-43bf-9302-e4c93287be11')
self.format_data.update({
'node_ident': fake_db_node['uuid'],
'allocated_node_ident': fake_db_node_alloced['uuid'],
@ -337,11 +375,15 @@ class TestRBACProjectScoped(TestACLBase):
# owner/lesse checks
db_utils.create_test_node(
uuid=owner_node_ident,
owner=owner_node_ident)
owner=owner_project_id,
last_error='meow',
reservation='lolcats')
leased_node = db_utils.create_test_node(
uuid=lessee_node_ident,
owner=owner_project_id,
lessee=lessee_project_id)
lessee=lessee_project_id,
last_error='meow',
reservation='lolcats')
fake_db_volume_target = db_utils.create_test_volume_target(
node_id=leased_node['id'])
fake_db_volume_connector = db_utils.create_test_volume_connector(
@ -350,6 +392,8 @@ class TestRBACProjectScoped(TestACLBase):
node_id=leased_node['id'])
fake_db_portgroup = db_utils.create_test_portgroup(
node_id=leased_node['id'])
fake_trait = 'CUSTOM_MEOW'
fake_vif_port_id = "0e21d58f-5de2-4956-85ff-33935ea1ca01"
self.format_data.update({
'node_ident': unowned_node['uuid'],
@ -359,7 +403,11 @@ class TestRBACProjectScoped(TestACLBase):
'volume_target_ident': fake_db_volume_target['uuid'],
'volume_connector_ident': fake_db_volume_connector['uuid'],
'port_ident': fake_db_port['uuid'],
'portgroup_ident': fake_db_portgroup['uuid']})
'portgroup_ident': fake_db_portgroup['uuid'],
'trait': fake_trait,
'vif_ident': fake_vif_port_id,
'ind_component': 'component',
'ind_ident': 'magic_light'})
@ddt.file_data('test_rbac_project_scoped.yaml')
@ddt.unpack

View File

@ -11,7 +11,7 @@ values:
unauthenticated_user_cannot_get_node:
path: &node_path '/v1/nodes/{node_uuid}'
assert_status: 403
assert_status: 404
project_admin_can_get_node:
path: *node_path
@ -24,7 +24,7 @@ project_admin_can_get_node:
project_member_cannot_get_node:
path: *node_path
headers: *project_member_headers
assert_status: 403
assert_status: 404
public_api:
path: /

View File

@ -78,7 +78,7 @@ nodes_get_node_member:
path: '/v1/nodes/{node_ident}'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_get_node_observer:
@ -152,7 +152,7 @@ nodes_node_ident_get_member:
path: '/v1/nodes/{node_ident}'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_node_ident_get_observer:
@ -178,7 +178,7 @@ nodes_node_ident_patch_member:
method: patch
headers: *member_headers
body: *extra_patch
assert_status: 403
assert_status: 404
deprecated: true
nodes_node_ident_patch_observer:
@ -200,7 +200,7 @@ nodes_node_ident_delete_member:
path: '/v1/nodes/{node_ident}'
method: delete
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_node_ident_delete_observer:
@ -223,7 +223,7 @@ nodes_validate_get_member:
path: '/v1/nodes/{node_ident}/validate'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_validate_get_observer:
@ -244,7 +244,7 @@ nodes_maintenance_put_member:
path: '/v1/nodes/{node_ident}/maintenance'
method: put
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_maintenance_put_observer:
@ -265,7 +265,7 @@ nodes_maintenance_delete_member:
path: '/v1/nodes/{node_ident}/maintenance'
method: delete
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_maintenance_delete_observer:
@ -289,7 +289,7 @@ nodes_management_boot_device_put_member:
method: put
headers: *member_headers
body: *boot_device_body
assert_status: 403
assert_status: 404
deprecated: true
nodes_management_boot_device_put_observer:
@ -311,7 +311,7 @@ nodes_management_boot_device_get_member:
path: '/v1/nodes/{node_ident}/management/boot_device'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_management_boot_device_get_observer:
@ -332,7 +332,7 @@ nodes_management_boot_device_supported_get_member:
path: '/v1/nodes/{node_ident}/management/boot_device/supported'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_management_boot_device_supported_get_observer:
@ -355,7 +355,7 @@ nodes_management_inject_nmi_put_member:
method: put
headers: *member_headers
body: {}
assert_status: 403
assert_status: 404
deprecated: true
nodes_management_inject_nmi_put_observer:
@ -377,7 +377,7 @@ nodes_states_get_member:
path: '/v1/nodes/{node_ident}/states'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_states_get_observer:
@ -401,7 +401,7 @@ nodes_states_power_put_member:
method: put
headers: *member_headers
body: *power_body
assert_status: 403
assert_status: 404
deprecated: true
nodes_states_power_put_observer:
@ -426,7 +426,7 @@ nodes_states_provision_put_member:
method: put
headers: *member_headers
body: *provision_body
assert_status: 403
assert_status: 404
deprecated: true
nodes_states_provision_put_observer:
@ -455,7 +455,7 @@ nodes_states_raid_put_member:
method: put
headers: *member_headers
body: *raid_body
assert_status: 403
assert_status: 404
deprecated: true
nodes_states_raid_put_observer:
@ -477,7 +477,7 @@ nodes_states_console_get_member:
path: '/v1/nodes/{node_ident}/states/console'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_states_console_get_admin:
@ -501,7 +501,7 @@ nodes_states_console_put_member:
method: put
headers: *member_headers
body: *console_body_put
assert_status: 403
assert_status: 404
deprecated: true
nodes_states_console_put_observer:
@ -526,7 +526,7 @@ nodes_vendor_passthru_methods_get_member:
path: '/v1/nodes/{node_ident}/vendor_passthru/methods'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_vendor_passthru_methods_get_observer:
@ -547,7 +547,7 @@ nodes_vendor_passthru_get_member:
path: '/v1/nodes/{node_ident}/vendor_passthru?method=test'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_vendor_passthru_get_observer:
@ -568,7 +568,7 @@ nodes_vendor_passthru_post_member:
path: '/v1/nodes/{node_ident}/vendor_passthru?method=test'
method: post
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_vendor_passthru_post_observer:
@ -589,7 +589,7 @@ nodes_vendor_passthru_put_member:
path: '/v1/nodes/{node_ident}/vendor_passthru?method=test'
method: put
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_vendor_passthru_put_observer:
@ -610,7 +610,7 @@ nodes_vendor_passthru_delete_member:
path: '/v1/nodes/{node_ident}/vendor_passthru?method=test'
method: delete
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_vendor_passthru_delete_observer:
@ -633,7 +633,7 @@ nodes_traits_get_member:
path: '/v1/nodes/{node_ident}/traits'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_traits_get_observer:
@ -658,7 +658,7 @@ nodes_traits_put_member:
path: '/v1/nodes/{node_ident}/traits'
method: put
headers: *member_headers
assert_status: 403
assert_status: 404
body: *traits_body
deprecated: true
@ -681,7 +681,7 @@ nodes_traits_delete_member:
path: '/v1/nodes/{node_ident}/traits/{trait}'
method: delete
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_traits_delete_observer:
@ -702,7 +702,7 @@ nodes_traits_trait_put_member:
path: '/v1/nodes/{node_ident}/traits/CUSTOM_TRAIT2'
method: put
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_traits_trait_put_observer:
@ -723,7 +723,7 @@ nodes_traits_trait_delete_member:
path: '/v1/nodes/{node_ident}/traits/{trait}'
method: delete
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_traits_trait_delete_observer:
@ -750,7 +750,7 @@ nodes_vifs_get_member:
path: '/v1/nodes/{node_ident}/vifs'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_vifs_get_observer:
@ -773,7 +773,7 @@ nodes_vifs_post_member:
path: '/v1/nodes/{node_ident}/vifs'
method: post
headers: *member_headers
assert_status: 403
assert_status: 404
body: *vif_body
deprecated: true
@ -797,7 +797,7 @@ nodes_vifs_node_vif_ident_delete_member:
path: '/v1/nodes/{node_ident}/vifs/{vif_ident}'
method: delete
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_vifs_node_vif_ident_delete_observer:
@ -820,7 +820,7 @@ nodes_management_indicators_get_member:
path: '/v1/nodes/{node_ident}/management/indicators'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_management_indicators_get_observer:
@ -1804,7 +1804,7 @@ nodes_bios_get_member:
path: '/v1/nodes/{node_ident}/bios'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_bios_get_observer:
@ -1825,7 +1825,7 @@ nodes_bios_bios_setting_get_member:
path: '/v1/nodes/{node_ident}/bios/{bios_setting}'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_bios_bios_setting_get_observer:

File diff suppressed because it is too large Load Diff

View File

@ -147,6 +147,26 @@ nodes_node_ident_patch_admin:
value: {'test': 'testing'}
assert_status: 503
system_admin_can_patch_chassis:
path: '/v1/nodes/{node_ident}'
method: patch
headers: *admin_headers
body:
- op: replace
path: /chassis_uuid
value: 'e74c40e0-d825-11e2-a28f-0800200c9a66'
assert_status: 503
system_member_can_patch_conductor_group:
path: '/v1/nodes/{node_ident}'
method: patch
headers: *scoped_member_headers
body:
- op: replace
path: /conductor_group
value: "DC04-ROW39"
assert_status: 503
nodes_node_ident_patch_member:
path: '/v1/nodes/{node_ident}'
method: patch