Browse Source

Volume targets/connectors Project Scoped RBAC

This patch adds project scoped access, as part of the work
to delineate system and project scope access.

Adds policies:
* baremetal:volume:list_all
* baremetal:volume:list
* baremetal:volume:view_target_properties

Change-Id: I898310b515195b7065a3b1c7998ef3f29f5e8747
changes/14/776314/7
Julia Kreger 7 months ago
parent
commit
e870bd34d0
  1. 8
      doc/source/admin/secure-rbac.rst
  2. 104
      ironic/api/controllers/v1/utils.py
  3. 61
      ironic/api/controllers/v1/volume_connector.py
  4. 87
      ironic/api/controllers/v1/volume_target.py
  5. 73
      ironic/common/policy.py
  6. 20
      ironic/db/api.py
  7. 41
      ironic/db/sqlalchemy/api.py
  8. 13
      ironic/objects/volume_connector.py
  9. 19
      ironic/objects/volume_target.py
  10. 7
      ironic/tests/unit/api/test_acl.py
  11. 28
      ironic/tests/unit/api/test_rbac_legacy.yaml
  12. 110
      ironic/tests/unit/api/test_rbac_project_scoped.yaml
  13. 20
      ironic/tests/unit/api/test_rbac_system_scoped.yaml
  14. 9
      ironic/tests/unit/objects/test_volume_connector.py
  15. 11
      ironic/tests/unit/objects/test_volume_target.py

8
doc/source/admin/secure-rbac.rst

@ -66,8 +66,12 @@ Supported Endpoints
* /nodes
* /nodes/<uuid>/ports
* /nodes/<uuid>/portgroups
* /nodes/<uuid>/volume/connectors
* /nodes/<uuid>/volume/targets
* /ports
* /portgroups
* /volume/connectors
* /volume/targets
How Project Scoped Works
------------------------
@ -146,7 +150,7 @@ More information is available on these fields in :doc:`/configuration/policy`.
Pratical differences
--------------------
Most users, upon implementing the use of ``system`` scoped authenticaiton,
Most users, upon implementing the use of ``system`` scoped authentication
should not notice a difference as long as their authentication token is
properly scoped to ``system`` and with the appropriate role for their
access level. For most users who used a ``baremetal`` project,
@ -154,7 +158,7 @@ or other custom project via a custom policy file, along with a custom
role name such as ``baremetal_admin``, this will require changing
the user to be a ``system`` scoped user with ``admin`` privilges.
The most noticable difference for API consumers is the HTTP 403 access
The most noticeable difference for API consumers is the HTTP 403 access
code is now mainly a HTTP 404 access code. The access concept has changed
from "Does the user user broadly has access to the API?" to
"Does user have access to the node, and then do they have access

104
ironic/api/controllers/v1/utils.py

@ -1787,6 +1787,110 @@ def check_port_list_policy(portgroup=False, parent_node=None,
return owner
def check_volume_list_policy(parent_node=None):
"""Check if the specified policy authorizes this request on a port.
:param parent_node: The UUID of a node, if any, to apply a policy
check to as well before applying other policy
check operations.
:raises: HTTPForbidden if the policy forbids access.
:return: owner that should be used for list query, if needed
"""
cdict = api.request.context.to_policy_values()
# No node is associated with this request, yet.
rpc_node = None
conceal_linked_node = None
if parent_node:
try:
rpc_node = objects.Node.get_by_uuid(api.request.context,
parent_node)
conceal_linked_node = rpc_node.uuid
except exception.NotFound:
raise exception.NodeNotFound(node=parent_node)
if parent_node:
try:
check_owner_policy(
'node', 'baremetal:node:get',
rpc_node.owner, rpc_node.lessee,
conceal_node=conceal_linked_node)
except exception.NotAuthorized:
if parent_node:
# This should likely never be hit, because
# the existence of a parent node should
# trigger the node not found exception to be
# explicitly raised.
raise exception.NodeNotFound(
node=parent_node)
raise
try:
policy.authorize('baremetal:volume:list_all',
cdict, api.request.context)
except exception.HTTPForbidden:
owner = cdict.get('project_id')
if not owner:
raise
policy.authorize('baremetal:volume:list',
cdict, api.request.context)
return owner
def check_volume_policy_and_retrieve(policy_name, vol_ident, target=False):
"""Check if the specified policy authorizes this request on a port.
:param: policy_name: Name of the policy to check.
:param: vol_ident: The name, uuid, or other valid ID value to find
a port or portgroup by.
:param: target: Boolean value to indicate if the check is for a volume
target or connector. Default value is False, implying
connector.
:raises: HTTPForbidden if the policy forbids access.
:raises: VolumeConnectorNotFound if the node is not found.
:raises: VolumeTargetNotFound if the node is not found.
:return: RPC port identified by port_ident associated node
"""
context = api.request.context
cdict = context.to_policy_values()
owner = None
lessee = None
try:
if not target:
rpc_vol = objects.VolumeConnector.get(context, vol_ident)
else:
rpc_vol = objects.VolumeTarget.get(context, vol_ident)
except (exception.VolumeConnectorNotFound, exception.VolumeTargetNotFound):
# don't expose non-existence of port unless requester
# has generic access to policy
raise
target_dict = dict(cdict)
try:
rpc_node = objects.Node.get_by_id(context, rpc_vol.node_id)
owner = rpc_node['owner']
lessee = rpc_node['lessee']
except exception.NodeNotFound:
pass
target_dict = dict(cdict)
target_dict['node.owner'] = owner
target_dict['node.lessee'] = lessee
try:
policy.authorize('baremetal:node:get', target_dict, context)
except exception.NotAuthorized:
if not target:
raise exception.VolumeConnectorNotFound(connector=vol_ident)
else:
raise exception.VolumeTargetNotFound(target=vol_ident)
policy.authorize(policy_name, target_dict, context)
return rpc_vol, rpc_node
def allow_build_configdrive():
"""Check if building configdrive is allowed.

61
ironic/api/controllers/v1/volume_connector.py

@ -111,7 +111,8 @@ class VolumeConnectorsController(rest.RestController):
def _get_volume_connectors_collection(self, node_ident, marker, limit,
sort_key, sort_dir,
resource_url=None,
fields=None, detail=None):
fields=None, detail=None,
project=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
@ -135,13 +136,15 @@ class VolumeConnectorsController(rest.RestController):
node = api_utils.get_rpc_node(node_ident)
connectors = objects.VolumeConnector.list_by_node_id(
api.request.context, node.id, limit, marker_obj,
sort_key=sort_key, sort_dir=sort_dir)
sort_key=sort_key, sort_dir=sort_dir,
project=project)
else:
connectors = objects.VolumeConnector.list(api.request.context,
limit,
marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
sort_dir=sort_dir,
project=project)
return list_convert_with_links(connectors, limit,
url=resource_url,
fields=fields,
@ -156,7 +159,7 @@ class VolumeConnectorsController(rest.RestController):
sort_dir=args.string, fields=args.string_list,
detail=args.boolean)
def get_all(self, node=None, marker=None, limit=None, sort_key='id',
sort_dir='asc', fields=None, detail=None):
sort_dir='asc', fields=None, detail=None, project=None):
"""Retrieve a list of volume connectors.
:param node: UUID or name of a node, to get only volume connectors
@ -179,7 +182,8 @@ class VolumeConnectorsController(rest.RestController):
:raises: InvalidParameterValue if sort key is invalid for sorting.
:raises: InvalidParameterValue if both fields and detail are specified.
"""
api_utils.check_policy('baremetal:volume:get')
project = api_utils.check_volume_list_policy(
parent_node=self.parent_node_ident)
if fields is None and not detail:
fields = _DEFAULT_RETURN_FIELDS
@ -191,7 +195,7 @@ class VolumeConnectorsController(rest.RestController):
resource_url = 'volume/connectors'
return self._get_volume_connectors_collection(
node, marker, limit, sort_key, sort_dir, resource_url=resource_url,
fields=fields, detail=detail)
fields=fields, detail=detail, project=project)
@METRICS.timer('VolumeConnectorsController.get_one')
@method.expose()
@ -210,13 +214,15 @@ class VolumeConnectorsController(rest.RestController):
:raises: VolumeConnectorNotFound if no volume connector exists with
the specified UUID.
"""
api_utils.check_policy('baremetal:volume:get')
rpc_connector, _ = api_utils.check_volume_policy_and_retrieve(
'baremetal:volume:get',
connector_uuid,
target=False)
if self.parent_node_ident:
raise exception.OperationNotPermitted()
rpc_connector = objects.VolumeConnector.get_by_uuid(
api.request.context, connector_uuid)
return convert_with_links(rpc_connector, fields=fields)
@METRICS.timer('VolumeConnectorsController.post')
@ -238,7 +244,23 @@ class VolumeConnectorsController(rest.RestController):
same UUID already exists
"""
context = api.request.context
api_utils.check_policy('baremetal:volume:create')
owner = None
lessee = None
raise_node_not_found = False
node_uuid = connector.get('node_uuid')
try:
node = api_utils.replace_node_uuid_with_id(connector)
owner = node.owner
lessee = node.lessee
except exception.NotFound:
raise_node_not_found = True
api_utils.check_owner_policy('node', 'baremetal:volume:create',
owner, lessee=lessee, conceal_node=False)
if raise_node_not_found:
raise exception.InvalidInput(fieldname='node_uuid',
value=node_uuid)
if self.parent_node_ident:
raise exception.OperationNotPermitted()
@ -247,8 +269,6 @@ class VolumeConnectorsController(rest.RestController):
if not connector.get('uuid'):
connector['uuid'] = uuidutils.generate_uuid()
node = api_utils.replace_node_uuid_with_id(connector)
new_connector = objects.VolumeConnector(context, **connector)
notify.emit_start_notification(context, new_connector, 'create',
@ -294,7 +314,11 @@ class VolumeConnectorsController(rest.RestController):
volume connector is not powered off.
"""
context = api.request.context
api_utils.check_policy('baremetal:volume:update')
rpc_connector, rpc_node = api_utils.check_volume_policy_and_retrieve(
'baremetal:volume:update',
connector_uuid,
target=False)
if self.parent_node_ident:
raise exception.OperationNotPermitted()
@ -307,9 +331,6 @@ class VolumeConnectorsController(rest.RestController):
"%(uuid)s.") % {'uuid': str(value)}
raise exception.InvalidUUID(message=message)
rpc_connector = objects.VolumeConnector.get_by_uuid(context,
connector_uuid)
connector_dict = rpc_connector.as_dict()
# NOTE(smoriya):
# 1) Remove node_id because it's an internal value and
@ -370,14 +391,14 @@ class VolumeConnectorsController(rest.RestController):
volume connector is not powered off.
"""
context = api.request.context
api_utils.check_policy('baremetal:volume:delete')
rpc_connector, rpc_node = api_utils.check_volume_policy_and_retrieve(
'baremetal:volume:delete',
connector_uuid,
target=False)
if self.parent_node_ident:
raise exception.OperationNotPermitted()
rpc_connector = objects.VolumeConnector.get_by_uuid(context,
connector_uuid)
rpc_node = objects.Node.get_by_id(context, rpc_connector.node_id)
notify.emit_start_notification(context, rpc_connector, 'delete',
node_uuid=rpc_node.uuid)
with notify.handle_error_notification(context, rpc_connector,

87
ironic/api/controllers/v1/volume_target.py

@ -27,6 +27,7 @@ from ironic.api import method
from ironic.common import args
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import policy
from ironic import objects
METRICS = metrics_utils.get_metrics_logger(__name__)
@ -119,9 +120,22 @@ class VolumeTargetsController(rest.RestController):
super(VolumeTargetsController, self).__init__()
self.parent_node_ident = node_ident
def _redact_target_properties(self, target):
# Filters what could contain sensitive information. For iSCSI
# volumes this can include iscsi connection details which may
# be sensitive.
redacted = ('** Value redacted: Requires permission '
'baremetal:volume:view_target_properties '
'access. Permission denied. **')
redacted_message = {
'redacted_contents': redacted
}
target.properties = redacted_message
def _get_volume_targets_collection(self, node_ident, marker, limit,
sort_key, sort_dir, resource_url=None,
fields=None, detail=None):
fields=None, detail=None,
project=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
@ -134,7 +148,6 @@ class VolumeTargetsController(rest.RestController):
raise exception.InvalidParameterValue(
_("The sort_key value %(key)s is an invalid field for "
"sorting") % {'key': sort_key})
node_ident = self.parent_node_ident or node_ident
if node_ident:
@ -145,12 +158,19 @@ class VolumeTargetsController(rest.RestController):
node = api_utils.get_rpc_node(node_ident)
targets = objects.VolumeTarget.list_by_node_id(
api.request.context, node.id, limit, marker_obj,
sort_key=sort_key, sort_dir=sort_dir)
sort_key=sort_key, sort_dir=sort_dir, project=project)
else:
targets = objects.VolumeTarget.list(api.request.context,
limit, marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
sort_dir=sort_dir,
project=project)
cdict = api.request.context.to_policy_values()
if not policy.check_policy('baremetal:volume:view_target_properties',
cdict, cdict):
for target in targets:
self._redact_target_properties(target)
return list_convert_with_links(targets, limit,
url=resource_url,
fields=fields,
@ -165,7 +185,7 @@ class VolumeTargetsController(rest.RestController):
sort_dir=args.string, fields=args.string_list,
detail=args.boolean)
def get_all(self, node=None, marker=None, limit=None, sort_key='id',
sort_dir='asc', fields=None, detail=None):
sort_dir='asc', fields=None, detail=None, project=None):
"""Retrieve a list of volume targets.
:param node: UUID or name of a node, to get only volume targets
@ -180,6 +200,8 @@ class VolumeTargetsController(rest.RestController):
:param fields: Optional, a list with a specified set of fields
of the resource to be returned.
:param detail: Optional, whether to retrieve with detail.
:param project: Optional, an associated node project (owner,
or lessee) to filter the query upon.
:returns: a list of volume targets, or an empty list if no volume
target is found.
@ -188,8 +210,8 @@ class VolumeTargetsController(rest.RestController):
:raises: InvalidParameterValue if sort key is invalid for sorting.
:raises: InvalidParameterValue if both fields and detail are specified.
"""
api_utils.check_policy('baremetal:volume:get')
project = api_utils.check_volume_list_policy(
parent_node=self.parent_node_ident)
if fields is None and not detail:
fields = _DEFAULT_RETURN_FIELDS
@ -202,7 +224,8 @@ class VolumeTargetsController(rest.RestController):
sort_key, sort_dir,
resource_url=resource_url,
fields=fields,
detail=detail)
detail=detail,
project=project)
@METRICS.timer('VolumeTargetsController.get_one')
@method.expose()
@ -220,13 +243,20 @@ class VolumeTargetsController(rest.RestController):
node.
:raises: VolumeTargetNotFound if no volume target with this UUID exists
"""
api_utils.check_policy('baremetal:volume:get')
rpc_target, _ = api_utils.check_volume_policy_and_retrieve(
'baremetal:volume:get',
target_uuid,
target=True)
if self.parent_node_ident:
raise exception.OperationNotPermitted()
rpc_target = objects.VolumeTarget.get_by_uuid(
api.request.context, target_uuid)
cdict = api.request.context.to_policy_values()
if not policy.check_policy('baremetal:volume:view_target_properties',
cdict, cdict):
self._redact_target_properties(rpc_target)
return convert_with_links(rpc_target, fields=fields)
@METRICS.timer('VolumeTargetsController.post')
@ -248,7 +278,23 @@ class VolumeTargetsController(rest.RestController):
UUID exists
"""
context = api.request.context
api_utils.check_policy('baremetal:volume:create')
raise_node_not_found = False
node = None
owner = None
lessee = None
node_uuid = target.get('node_uuid')
try:
node = api_utils.replace_node_uuid_with_id(target)
owner = node.owner
lessee = node.lessee
except exception.NotFound:
raise_node_not_found = True
api_utils.check_owner_policy('node', 'baremetal:volume:create',
owner, lessee=lessee,
conceal_node=False)
if raise_node_not_found:
raise exception.InvalidInput(fieldname='node_uuid',
value=node_uuid)
if self.parent_node_ident:
raise exception.OperationNotPermitted()
@ -256,9 +302,6 @@ class VolumeTargetsController(rest.RestController):
# NOTE(hshiina): UUID is mandatory for notification payload
if not target.get('uuid'):
target['uuid'] = uuidutils.generate_uuid()
node = api_utils.replace_node_uuid_with_id(target)
new_target = objects.VolumeTarget(context, **target)
notify.emit_start_notification(context, new_target, 'create',
@ -301,7 +344,10 @@ class VolumeTargetsController(rest.RestController):
volume target is not powered off.
"""
context = api.request.context
api_utils.check_policy('baremetal:volume:update')
api_utils.check_volume_policy_and_retrieve('baremetal:volume:update',
target_uuid,
target=True)
if self.parent_node_ident:
raise exception.OperationNotPermitted()
@ -327,6 +373,10 @@ class VolumeTargetsController(rest.RestController):
try:
if target_dict['node_uuid'] != rpc_node.uuid:
# TODO(TheJulia): I guess the intention is to
# permit the mapping to be changed
# should we even allow this at all?
rpc_node = objects.Node.get(
api.request.context, target_dict['node_uuid'])
except exception.NodeNotFound as e:
@ -374,7 +424,10 @@ class VolumeTargetsController(rest.RestController):
volume target is not powered off.
"""
context = api.request.context
api_utils.check_policy('baremetal:volume:delete')
api_utils.check_volume_policy_and_retrieve('baremetal:volume:delete',
target_uuid,
target=True)
if self.parent_node_ident:
raise exception.OperationNotPermitted()

73
ironic/common/policy.py

@ -112,7 +112,18 @@ SYSTEM_OR_OWNER_READER = (
'(' + SYSTEM_READER + ') or (' + PROJECT_OWNER_READER + ')'
)
SYSTEM_MEMBER_OR_OWNER_LESSEE_ADMIN = (
'(' + SYSTEM_MEMBER + ') or (' + PROJECT_OWNER_ADMIN + ') or (' + PROJECT_LESSEE_ADMIN + ')' # noqa
)
# Special purpose aliases for things like "ability to access the API
# as a reader, or permission checking that does not require node
# owner relationship checking
API_READER = ('role:reader')
TARGET_PROPERTIES_READER = (
'(' + SYSTEM_READER + ') or (role:admin)'
)
default_policies = [
# Legacy setting, don't remove. Likely to be overridden by operators who
@ -1339,9 +1350,40 @@ roles.
volume_policies = [
policy.DocumentedRuleDefault(
name='baremetal:volume:get',
name='baremetal:volume:list_all',
check_str=SYSTEM_READER,
scope_types=['system'],
scope_types=['system', 'project'],
description=('Retrieve a list of all Volume connector and target '
'records'),
operations=[
{'path': '/volume/connectors', 'method': 'GET'},
{'path': '/volume/targets', 'method': 'GET'},
{'path': '/nodes/{node_ident}/volume/connectors', 'method': 'GET'},
{'path': '/nodes/{node_ident}/volume/targets', 'method': 'GET'}
],
deprecated_rule=deprecated_volume_get,
deprecated_reason=deprecated_volume_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:volume:list',
check_str=API_READER,
scope_types=['system', 'project'],
description='Retrieve a list of Volume connector and target records',
operations=[
{'path': '/volume/connectors', 'method': 'GET'},
{'path': '/volume/targets', 'method': 'GET'},
{'path': '/nodes/{node_ident}/volume/connectors', 'method': 'GET'},
{'path': '/nodes/{node_ident}/volume/targets', 'method': 'GET'}
],
deprecated_rule=deprecated_volume_get,
deprecated_reason=deprecated_volume_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:volume:get',
check_str=SYSTEM_OR_PROJECT_READER,
scope_types=['system', 'project'],
description='Retrieve Volume connector and target records',
operations=[
{'path': '/volume', 'method': 'GET'},
@ -1360,8 +1402,8 @@ volume_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:volume:create',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_LESSEE_ADMIN,
scope_types=['system', 'project'],
description='Create Volume connector and target records',
operations=[
{'path': '/volume/connectors', 'method': 'POST'},
@ -1373,8 +1415,8 @@ volume_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:volume:delete',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_MEMBER_OR_OWNER_LESSEE_ADMIN,
scope_types=['system', 'project'],
description='Delete Volume connector and target records',
operations=[
{'path': '/volume/connectors/{volume_connector_id}',
@ -1388,8 +1430,8 @@ volume_policies = [
),
policy.DocumentedRuleDefault(
name='baremetal:volume:update',
check_str=SYSTEM_MEMBER,
scope_types=['system'],
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,
scope_types=['system', 'project'],
description='Update Volume connector and target records',
operations=[
{'path': '/volume/connectors/{volume_connector_id}',
@ -1401,6 +1443,21 @@ volume_policies = [
deprecated_reason=deprecated_volume_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
name='baremetal:volume:view_target_properties',
check_str=TARGET_PROPERTIES_READER,
scope_types=['system', 'project'],
description='Ability to view volume target properties',
operations=[
{'path': '/volume/connectors/{volume_connector_id}',
'method': 'GET'},
{'path': '/volume/targets/{volume_target_id}',
'method': 'GET'}
],
deprecated_rule=deprecated_volume_update,
deprecated_reason=deprecated_volume_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
]

20
ironic/db/api.py

@ -714,7 +714,8 @@ class Connection(object, metaclass=abc.ABCMeta):
@abc.abstractmethod
def get_volume_connector_list(self, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None,
project=None):
"""Return a list of volume connectors.
:param limit: Maximum number of volume connectors to return.
@ -723,6 +724,8 @@ class Connection(object, metaclass=abc.ABCMeta):
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc)
:param project: The associated node project to search with.
:returns: a list of :class:`VolumeConnector` objects
:returns: A list of volume connectors.
:raises: InvalidParameterValue If sort_key does not exist.
"""
@ -750,7 +753,7 @@ class Connection(object, metaclass=abc.ABCMeta):
@abc.abstractmethod
def get_volume_connectors_by_node_id(self, node_id, limit=None,
marker=None, sort_key=None,
sort_dir=None):
sort_dir=None, project=None):
"""List all the volume connectors for a given node.
:param node_id: The integer node ID.
@ -760,6 +763,8 @@ class Connection(object, metaclass=abc.ABCMeta):
:param sort_key: Attribute by which results should be sorted
:param sort_dir: Direction in which results should be sorted
(asc, desc)
:param project: The associated node project to search with.
:returns: a list of :class:`VolumeConnector` objects
:returns: A list of volume connectors.
:raises: InvalidParameterValue If sort_key does not exist.
"""
@ -813,7 +818,8 @@ class Connection(object, metaclass=abc.ABCMeta):
@abc.abstractmethod
def get_volume_target_list(self, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None,
project=None):
"""Return a list of volume targets.
:param limit: Maximum number of volume targets to return.
@ -822,6 +828,8 @@ class Connection(object, metaclass=abc.ABCMeta):
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:param project: The associated node project to search with.
:returns: a list of :class:`VolumeConnector` objects
:returns: A list of volume targets.
:raises: InvalidParameterValue if sort_key does not exist.
"""
@ -849,7 +857,7 @@ class Connection(object, metaclass=abc.ABCMeta):
@abc.abstractmethod
def get_volume_targets_by_node_id(self, node_id, limit=None,
marker=None, sort_key=None,
sort_dir=None):
sort_dir=None, project=None):
"""List all the volume targets for a given node.
:param node_id: The integer node ID.
@ -859,6 +867,8 @@ class Connection(object, metaclass=abc.ABCMeta):
:param sort_key: Attribute by which results should be sorted
:param sort_dir: direction in which results should be sorted
(asc, desc)
:param project: The associated node project to search with.
:returns: a list of :class:`VolumeConnector` objects
:returns: A list of volume targets.
:raises: InvalidParameterValue if sort_key does not exist.
"""
@ -866,7 +876,7 @@ class Connection(object, metaclass=abc.ABCMeta):
@abc.abstractmethod
def get_volume_targets_by_volume_id(self, volume_id, limit=None,
marker=None, sort_key=None,
sort_dir=None):
sort_dir=None, project=None):
"""List all the volume targets for a given volume id.
:param volume_id: The UUID of the volume.

41
ironic/db/sqlalchemy/api.py

@ -169,6 +169,20 @@ def add_portgroup_filter_by_node_project(query, value):
| (models.Node.lessee == value))
def add_volume_conn_filter_by_node_project(query, value):
query = query.join(models.Node,
models.VolumeConnector.node_id == models.Node.id)
return query.filter((models.Node.owner == value)
| (models.Node.lessee == value))
def add_volume_target_filter_by_node_project(query, value):
query = query.join(models.Node,
models.VolumeTarget.node_id == models.Node.id)
return query.filter((models.Node.owner == value)
| (models.Node.lessee == value))
def add_portgroup_filter(query, value):
"""Adds a portgroup-specific filter to a query.
@ -1235,9 +1249,12 @@ class Connection(api.Connection):
% addresses)
def get_volume_connector_list(self, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None, project=None):
query = model_query(models.VolumeConnector)
if project:
query = add_volume_conn_filter_by_node_project(query, project)
return _paginate_query(models.VolumeConnector, limit, marker,
sort_key, sort_dir)
sort_key, sort_dir, query)
def get_volume_connector_by_id(self, db_id):
query = model_query(models.VolumeConnector).filter_by(id=db_id)
@ -1256,8 +1273,10 @@ class Connection(api.Connection):
def get_volume_connectors_by_node_id(self, node_id, limit=None,
marker=None, sort_key=None,
sort_dir=None):
sort_dir=None, project=None):
query = model_query(models.VolumeConnector).filter_by(node_id=node_id)
if project:
add_volume_conn_filter_by_node_project(query, project)
return _paginate_query(models.VolumeConnector, limit, marker,
sort_key, sort_dir, query)
@ -1315,9 +1334,12 @@ class Connection(api.Connection):
raise exception.VolumeConnectorNotFound(connector=ident)
def get_volume_target_list(self, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None, project=None):
query = model_query(models.VolumeTarget)
if project:
query = add_volume_target_filter_by_node_project(query, project)
return _paginate_query(models.VolumeTarget, limit, marker,
sort_key, sort_dir)
sort_key, sort_dir, query)
def get_volume_target_by_id(self, db_id):
query = model_query(models.VolumeTarget).filter_by(id=db_id)
@ -1334,15 +1356,20 @@ class Connection(api.Connection):
raise exception.VolumeTargetNotFound(target=uuid)
def get_volume_targets_by_node_id(self, node_id, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None,
project=None):
query = model_query(models.VolumeTarget).filter_by(node_id=node_id)
if project:
add_volume_target_filter_by_node_project(query, project)
return _paginate_query(models.VolumeTarget, limit, marker, sort_key,
sort_dir, query)
def get_volume_targets_by_volume_id(self, volume_id, limit=None,
marker=None, sort_key=None,
sort_dir=None):
sort_dir=None, project=None):
query = model_query(models.VolumeTarget).filter_by(volume_id=volume_id)
if project:
query = add_volume_target_filter_by_node_project(query, project)
return _paginate_query(models.VolumeTarget, limit, marker, sort_key,
sort_dir, query)

13
ironic/objects/volume_connector.py

@ -108,7 +108,7 @@ class VolumeConnector(base.IronicObject,
# @object_base.remotable_classmethod
@classmethod
def list(cls, context, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None, project=None):
"""Return a list of VolumeConnector objects.
:param context: security context
@ -116,13 +116,15 @@ class VolumeConnector(base.IronicObject,
:param marker: pagination marker for large data sets
:param sort_key: column to sort results by
:param sort_dir: direction to sort. "asc" or "desc".
:param project: The associated node project to search with.
:returns: a list of :class:`VolumeConnector` objects
:raises: InvalidParameterValue if sort_key does not exist
"""
db_connectors = cls.dbapi.get_volume_connector_list(limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
sort_dir=sort_dir,
project=project)
return cls._from_db_object_list(context, db_connectors)
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
@ -131,7 +133,7 @@ class VolumeConnector(base.IronicObject,
# @object_base.remotable_classmethod
@classmethod
def list_by_node_id(cls, context, node_id, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None, project=None):
"""Return a list of VolumeConnector objects related to a given node ID.
:param context: security context
@ -140,6 +142,8 @@ class VolumeConnector(base.IronicObject,
:param marker: pagination marker for large data sets
:param sort_key: column to sort results by
:param sort_dir: direction to sort. "asc" or "desc".
:param project: The associated node project to search with.
:returns: a list of :class:`VolumeConnector` objects
:returns: a list of :class:`VolumeConnector` objects
:raises: InvalidParameterValue if sort_key does not exist
"""
@ -148,7 +152,8 @@ class VolumeConnector(base.IronicObject,
limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
sort_dir=sort_dir,
project=project)
return cls._from_db_object_list(context, db_connectors)
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable

19
ironic/objects/volume_target.py

@ -107,7 +107,7 @@ class VolumeTarget(base.IronicObject,
# @object_base.remotable_classmethod
@classmethod
def list(cls, context, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None, project=None):
"""Return a list of VolumeTarget objects.
:param context: security context
@ -115,13 +115,16 @@ class VolumeTarget(base.IronicObject,
:param marker: pagination marker for large data sets
:param sort_key: column to sort results by
:param sort_dir: direction to sort. "asc" or "desc".
:param project: The associated node project to search with.
:returns: a list of :class:`VolumeConnector` objects
:returns: a list of :class:`VolumeTarget` objects
:raises: InvalidParameterValue if sort_key does not exist
"""
db_targets = cls.dbapi.get_volume_target_list(limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
sort_dir=sort_dir,
project=project)
return cls._from_db_object_list(context, db_targets)
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
@ -130,7 +133,7 @@ class VolumeTarget(base.IronicObject,
# @object_base.remotable_classmethod
@classmethod
def list_by_node_id(cls, context, node_id, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None, project=None):
"""Return a list of VolumeTarget objects related to a given node ID.
:param context: security context
@ -139,6 +142,8 @@ class VolumeTarget(base.IronicObject,
:param marker: pagination marker for large data sets
:param sort_key: column to sort results by
:param sort_dir: direction to sort. "asc" or "desc".
:param project: The associated node project to search with.
:returns: a list of :class:`VolumeConnector` objects
:returns: a list of :class:`VolumeTarget` objects
:raises: InvalidParameterValue if sort_key does not exist
"""
@ -147,7 +152,8 @@ class VolumeTarget(base.IronicObject,
limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
sort_dir=sort_dir,
project=project)
return cls._from_db_object_list(context, db_targets)
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
@ -156,7 +162,7 @@ class VolumeTarget(base.IronicObject,
# @object_base.remotable_classmethod
@classmethod
def list_by_volume_id(cls, context, volume_id, limit=None, marker=None,
sort_key=None, sort_dir=None):
sort_key=None, sort_dir=None, project=None):
"""Return a list of VolumeTarget objects related to a given volume ID.
:param context: security context
@ -174,7 +180,8 @@ class VolumeTarget(base.IronicObject,
limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
sort_dir=sort_dir,
project=project)
return cls._from_db_object_list(context, db_targets)
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable

7
ironic/tests/unit/api/test_acl.py

@ -391,6 +391,13 @@ class TestRBACProjectScoped(TestACLBase):
node_id=owned_node['id'],
name='magicfoo',
address='01:03:09:ff:01:01')
db_utils.create_test_volume_target(
uuid='a265e2f0-e97f-4177-b1c0-8298add53086',
node_id=owned_node['id'])
db_utils.create_test_volume_connector(
uuid='65ea0296-219b-4635-b0c8-a6e055da878d',
node_id=owned_node['id'],
connector_id='iqn.2012-06.org.openstack.magic')
# Leased nodes
leased_node = db_utils.create_test_node(

28
ironic/tests/unit/api/test_rbac_legacy.yaml

@ -1315,9 +1315,9 @@ volume_connectors_post_admin:
path: '/v1/volume/connectors'
method: post
headers: *admin_headers
assert_status: 400
assert_status: 201
body: &volume_connector_body
node_uuid: 68a552fb-dcd2-43bf-9302-e4c93287be16
node_uuid: 1be26c0b-03f2-4d2e-ae87-c02d7f33c123
type: ip
connector_id: 192.168.1.100
deprecated: true
@ -1349,7 +1349,7 @@ volume_volume_connector_id_get_member:
path: '/v1/volume/connectors/{volume_connector_ident}'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
volume_volume_connector_id_get_observer:
@ -1375,7 +1375,7 @@ volume_volume_connector_id_patch_member:
method: patch
headers: *member_headers
body: *connector_patch_body
assert_status: 403
assert_status: 404
deprecated: true
volume_volume_connector_id_patch_observer:
@ -1397,7 +1397,7 @@ volume_volume_connector_id_delete_member:
path: '/v1/volume/connectors/{volume_connector_ident}'
method: delete
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
volume_volume_connector_id_delete_observer:
@ -1437,11 +1437,11 @@ volume_targets_post_admin:
path: '/v1/volume/targets'
method: post
headers: *admin_headers
assert_status: 400
assert_status: 201
body: &volume_target_body
node_uuid: 68a552fb-dcd2-43bf-9302-e4c93287be16
node_uuid: 1be26c0b-03f2-4d2e-ae87-c02d7f33c123
volume_type: iscsi
boot_index: 0
boot_index: 4
volume_id: 'test-id'
deprecated: true
@ -1472,7 +1472,7 @@ volume_volume_target_id_get_member:
path: '/v1/volume/targets/{volume_target_ident}'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
volume_volume_target_id_get_observer:
@ -1493,12 +1493,12 @@ volume_volume_target_id_patch_admin:
assert_status: 503
deprecated: true
volume_volume_target_id_patch_admin:
volume_volume_target_id_patch_member:
path: '/v1/volume/targets/{volume_target_ident}'
method: patch
body: *volume_target_patch
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
volume_volume_target_id_patch_observer:
@ -1520,7 +1520,7 @@ volume_volume_target_id_delete_member:
path: '/v1/volume/targets/{volume_target_ident}'
method: delete
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
volume_volume_target_id_delete_observer:
@ -1564,7 +1564,7 @@ nodes_volume_connectors_get_member:
path: '/v1/nodes/{node_ident}/volume/connectors'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_volume_connectors_get_observer:
@ -1585,7 +1585,7 @@ nodes_volume_targets_get_member:
path: '/v1/nodes/{node_ident}/volume/targets'
method: get
headers: *member_headers
assert_status: 403
assert_status: 404
deprecated: true
nodes_volume_targets_get_observer:

110
ironic/tests/unit/api/test_rbac_project_scoped.yaml

@ -1884,7 +1884,6 @@ owner_reader_can_list_volume_connectors:
assert_status: 200
assert_list_length:
connectors: 2
skip_reason: policy not implemented
lessee_reader_can_list_volume_connectors:
path: '/v1/volume/connectors'
@ -1893,27 +1892,24 @@ lessee_reader_can_list_volume_connectors:
assert_status: 200
assert_list_length:
connectors: 1
skip_reason: policy not implemented
third_party_admin_cannot_get_connector_list:
path: '/v1/volume/targets'
path: '/v1/volume/connectors'
method: get
headers: *third_party_admin_headers
assert_status: 200
assert_list_length:
connectors: 0
skip_reason: policy not implemented
owner_admin_can_post_volume_connector:
path: '/v1/volume/connectors'
method: post
headers: *owner_reader_headers
assert_status: 400
headers: *owner_admin_headers
assert_status: 201
body: &volume_connector_body
node_uuid: 68a552fb-dcd2-43bf-9302-e4c93287be16
node_uuid: 1ab63b9e-66d7-4cd7-8618-dddd0f9f7881
type: ip
connector_id: 192.168.1.100
skip_reason: policy not implemented
lessee_admin_cannot_post_volume_connector:
path: '/v1/volume/connectors'
@ -1921,7 +1917,6 @@ lessee_admin_cannot_post_volume_connector:
headers: *lessee_admin_headers
assert_status: 403
body: *volume_connector_body
skip_reason: policy not implemented
third_party_admin_cannot_post_volume_connector:
path: '/v1/volume/connectors'
@ -1929,28 +1924,24 @@ third_party_admin_cannot_post_volume_connector:
headers: *third_party_admin_headers
assert_status: 403
body: *volume_connector_body
skip_reason: policy not implemented
owner_reader_can_get_volume_connector:
path: '/v1/volume/connectors/{volume_connector_ident}'
method: get
headers: *owner_reader_headers
assert_status: 200
skip_reason: policy not implemented
lessee_reader_can_get_volume_connector:
path: '/v1/volume/connectors/{volume_connector_ident}'
method: get
headers: *lessee_reader_headers
assert_status: 200
skip_reason: policy not implemented
third_party_admin_cannot_get_volume_connector:
path: '/v1/volume/connectors/{volume_connector_ident}'
method: get
headers: *third_party_admin_headers
assert_status: 404
skip_reason: policy not implemented
lessee_member_cannot_patch_volume_connectors:
path: '/v1/volume/connectors/{volume_connector_ident}'
@ -1961,7 +1952,6 @@ lessee_member_cannot_patch_volume_connectors:
path: /extra
value: {'test': 'testing'}
assert_status: 403
skip_reason: policy not implemented
owner_admin_can_patch_volume_connectors:
path: '/v1/volume/connectors/{volume_connector_ident}'
@ -1969,7 +1959,6 @@ owner_admin_can_patch_volume_connectors:
headers: *owner_member_headers
body: *connector_patch_body
assert_status: 503
skip_reason: policy not implemented
lessee_admin_cannot_patch_volume_connectors:
path: '/v1/volume/connectors/{volume_connector_ident}'
@ -1977,7 +1966,6 @@ lessee_admin_cannot_patch_volume_connectors:
headers: *owner_member_headers
body: *connector_patch_body
assert_status: 503
skip_reason: policy not implemented
owner_member_can_patch_volume_connectors:
path: '/v1/volume/connectors/{volume_connector_ident}'
@ -1985,7 +1973,6 @@ owner_member_can_patch_volume_connectors:
headers: *owner_member_headers
body: *connector_patch_body
assert_status: 503
skip_reason: policy not implemented
lessee_member_cannot_patch_volume_connectors:
path: '/v1/volume/connectors/{volume_connector_ident}'
@ -1993,7 +1980,6 @@ lessee_member_cannot_patch_volume_connectors:
headers: *lessee_member_headers
body: *connector_patch_body
assert_status: 403
skip_reason: policy not implemented
third_party_admin_cannot_patch_volume_connectors:
path: '/v1/volume/connectors/{volume_connector_ident}'
@ -2001,28 +1987,24 @@ third_party_admin_cannot_patch_volume_connectors:
headers: *third_party_admin_headers
body: *connector_patch_body
assert_status: 404
skip_reason: policy not implemented
owner_admin_can_delete_volume_connectors:
path: '/v1/volume/connectors/{volume_connector_ident}'
method: delete
headers: *owner_reader_headers
assert_status: 403
skip_reason: policy not implemented
lessee_admin_cannot_delete_volume_connectors:
path: '/v1/volume/connectors/{volume_connector_ident}'
method: delete
headers: *lessee_reader_headers
assert_status: 403
skip_reason: policy not implemented
third_party_admin_cannot_delete_volume_connector:
path: '/v1/volume/connectors/{volume_connector_ident}'
method: delete
headers: *third_party_admin_headers
assert_status: 403
skip_reason: policy not implemented
assert_status: 404
# Volume targets
@ -2034,7 +2016,6 @@ owner_reader_can_get_targets:
assert_status: 200
assert_list_length:
targets: 2
skip_reason: policy not implemented
lesse_reader_can_get_targets:
path: '/v1/volume/targets'
@ -2043,7 +2024,6 @@ lesse_reader_can_get_targets:
assert_status: 200
assert_list_length:
targets: 1
skip_reason: policy not implemented
third_party_admin_cannot_get_target_list:
path: '/v1/volume/targets'
@ -2052,56 +2032,58 @@ third_party_admin_cannot_get_target_list:
assert_status: 200
assert_list_length:
targets: 0
skip_reason: policy not implemented
owner_reader_can_get_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
method: get
headers: *owner_reader_headers
assert_status: 200
skip_reason: policy not implemented
assert_dict_contains:
# This helps assert that the field has been redacted.
properties:
redacted_contents: '** Value redacted: Requires permission baremetal:volume:view_target_properties access. Permission denied. **'
lessee_reader_can_get_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
method: get
headers: *lessee_reader_headers
assert_status: 200
skip_reason: policy not implemented
third_party_admin_cannot_get_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
method: get
headers: *third_party_admin_headers
assert_status: 404
skip_reason: policy not implemented
owner_admin_create_volume_target:
path: '/v1/volume/targets'
method: post
headers: *owner_admin_headers
assert_status: 400
assert_status: 201
body: &volume_target_body
node_uuid: 68a552fb-dcd2-43bf-9302-e4c93287be16
node_uuid: 1ab63b9e-66d7-4cd7-8618-dddd0f9f7881
volume_type: iscsi
boot_index: 0
boot_index: 2
volume_id: 'test-id'
skip_reason: policy not implemented
lessee_admin_create_volume_target:
path: '/v1/volume/targets'
method: post
headers: *owner_admin_headers
assert_status: 400
body: *volume_target_body
skip_reason: policy not implemented
assert_status: 201
body:
node_uuid: 38d5abed-c585-4fce-a57e-a2ffc2a2ec6f
volume_type: iscsi
boot_index: 2
volume_id: 'test-id2'
third_party_admin_cannot_create_volume_target:
path: '/v1/volume/targets'
method: post
headers: *owner_admin_headers
assert_status: 400
headers: *third_party_admin_headers
assert_status: 403
body: *volume_target_body
skip_reason: policy not implemented
owner_member_can_patch_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
@ -2110,16 +2092,22 @@ owner_member_can_patch_volume_target:
- op: replace
path: /extra
value: {'test': 'testing'}
assert_status: 403
skip_reason: policy not implemented
headers: *owner_member_headers
assert_status: 503
lessee_member_can_patch_volume_target:
lessee_admin_can_patch_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
method: patch
body: *volume_target_patch
headers: *lessee_member_headers
headers: *lessee_admin_headers
assert_status: 503
skip_reason: policy not implemented
lessee_member_cannot_patch_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
method: patch
body: *volume_target_patch
headers: *lessee_member_headers
assert_status: 403
third_party_admin_cannot_patch_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
@ -2127,86 +2115,74 @@ third_party_admin_cannot_patch_volume_target:
body: *volume_target_patch
headers: *third_party_admin_headers
assert_status: 404
skip_reason: policy not implemented
owner_admin_can_delete_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
method: delete
headers: *owner_admin_headers
assert_status: 403
skip_reason: policy not implemented
assert_status: 503
lessee_admin_can_delete_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
method: delete
headers: *lessee_admin_headers
assert_status: 201
skip_reason: policy not implemented
assert_status: 503
owner_member_cannot_delete_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
method: delete
headers: *owner_member_headers
assert_status: 403
skip_reason: policy not implemented
lessee_member_cannot_delete_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
method: delete
headers: *lessee_member_headers
assert_status: 403
skip_reason: policy not implemented
third_party_admin_cannot_delete_volume_target:
path: '/v1/volume/targets/{volume_target_ident}'
method: delete
headers: *third_party_admin_headers
assert_status: 403
skip_reason: policy not implemented
assert_status: 404
# Get Volumes by Node - https://docs.openstack.org/api-ref/baremetal/#listing-volume-resources-by-node-nodes-volume
owner_reader_can_get_volume_connectors:
path: '/v1/nodes/{node_ident}/volume/connectors'
path: '/v1/nodes/{owner_node_ident}/volume/connectors'
method: get
headers: *owner_reader_headers
assert_status: 200
skip_reason: policy not implemented
lessee_reader_can_get_node_volume_connectors:
path: '/v1/nodes/{node_ident}/volume/connectors'
path: '/v1/nodes/{lessee_node_ident}/volume/connectors'
method: get
headers: *lessee_reader_headers