From e870bd34d0ccacbaef7f4e4def2535eb28f822b9 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Wed, 17 Feb 2021 21:01:47 -0800 Subject: [PATCH] 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 --- doc/source/admin/secure-rbac.rst | 8 +- ironic/api/controllers/v1/utils.py | 104 +++++++++++++++++ ironic/api/controllers/v1/volume_connector.py | 61 ++++++---- ironic/api/controllers/v1/volume_target.py | 87 +++++++++++--- ironic/common/policy.py | 73 ++++++++++-- ironic/db/api.py | 20 +++- ironic/db/sqlalchemy/api.py | 41 +++++-- ironic/objects/volume_connector.py | 13 ++- ironic/objects/volume_target.py | 19 ++- ironic/tests/unit/api/test_acl.py | 7 ++ ironic/tests/unit/api/test_rbac_legacy.yaml | 28 ++--- .../unit/api/test_rbac_project_scoped.yaml | 110 +++++++----------- .../unit/api/test_rbac_system_scoped.yaml | 20 ++-- .../unit/objects/test_volume_connector.py | 9 +- .../tests/unit/objects/test_volume_target.py | 11 +- 15 files changed, 446 insertions(+), 165 deletions(-) diff --git a/doc/source/admin/secure-rbac.rst b/doc/source/admin/secure-rbac.rst index 245feef8f3..f25aa6a96d 100644 --- a/doc/source/admin/secure-rbac.rst +++ b/doc/source/admin/secure-rbac.rst @@ -66,8 +66,12 @@ Supported Endpoints * /nodes * /nodes//ports * /nodes//portgroups +* /nodes//volume/connectors +* /nodes//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 diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index b618b2f455..01483c47ba 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/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. diff --git a/ironic/api/controllers/v1/volume_connector.py b/ironic/api/controllers/v1/volume_connector.py index 0a6ffa4d52..1008220296 100644 --- a/ironic/api/controllers/v1/volume_connector.py +++ b/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, diff --git a/ironic/api/controllers/v1/volume_target.py b/ironic/api/controllers/v1/volume_target.py index 9fa5f89099..d98f461edc 100644 --- a/ironic/api/controllers/v1/volume_target.py +++ b/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() diff --git a/ironic/common/policy.py b/ironic/common/policy.py index 10641bd4e1..5ffb373abb 100644 --- a/ironic/common/policy.py +++ b/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 + ), ] diff --git a/ironic/db/api.py b/ironic/db/api.py index 92fab5b60f..d44ea73ef2 100644 --- a/ironic/db/api.py +++ b/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. diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py index 7b5f1731bf..6f38c4b8f4 100644 --- a/ironic/db/sqlalchemy/api.py +++ b/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) diff --git a/ironic/objects/volume_connector.py b/ironic/objects/volume_connector.py index e91706d78c..57070ab069 100644 --- a/ironic/objects/volume_connector.py +++ b/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 diff --git a/ironic/objects/volume_target.py b/ironic/objects/volume_target.py index 68a54c4357..051d9ed9e7 100644 --- a/ironic/objects/volume_target.py +++ b/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 diff --git a/ironic/tests/unit/api/test_acl.py b/ironic/tests/unit/api/test_acl.py index 4bbb1ff38e..b670738021 100644 --- a/ironic/tests/unit/api/test_acl.py +++ b/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( diff --git a/ironic/tests/unit/api/test_rbac_legacy.yaml b/ironic/tests/unit/api/test_rbac_legacy.yaml index 6a3c5127b1..deda21757c 100644 --- a/ironic/tests/unit/api/test_rbac_legacy.yaml +++ b/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: diff --git a/ironic/tests/unit/api/test_rbac_project_scoped.yaml b/ironic/tests/unit/api/test_rbac_project_scoped.yaml index 475e07e4f9..5f96ea8ddf 100644 --- a/ironic/tests/unit/api/test_rbac_project_scoped.yaml +++ b/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_admin_headers + assert_status: 503 + +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: 503 - skip_reason: policy not implemented + 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 assert_status: 200 - skip_reason: policy not implemented third_party_admin_cannot_get_node_volume_connectors: - path: '/v1/nodes/{node_ident}/volume/connectors' + path: '/v1/nodes/{lessee_node_ident}/volume/connectors' method: get headers: *third_party_admin_headers - assert_status: 200 - skip_reason: policy not implemented + assert_status: 404 owner_reader_can_get_node_volume_targets: - path: '/v1/nodes/{node_ident}/volume/targets' + path: '/v1/nodes/{owner_node_ident}/volume/targets' method: get headers: *owner_reader_headers assert_status: 200 - skip_reason: policy not implemented lessee_reader_can_get_node_volume_targets: - path: '/v1/nodes/{node_ident}/volume/targets' + path: '/v1/nodes/{lessee_node_ident}/volume/targets' method: get headers: *lessee_reader_headers assert_status: 200 - skip_reason: policy not implemented third_part_admin_cannot_read_node_volume_targets: - path: '/v1/nodes/{node_ident}/volume/targets' + path: '/v1/nodes/{lessee_node_ident}/volume/targets' method: get - headers: *owner_reader_headers + headers: *third_party_admin_headers assert_status: 404 - skip_reason: policy not implemented # Drivers - https://docs.openstack.org/api-ref/baremetal/#drivers-drivers diff --git a/ironic/tests/unit/api/test_rbac_system_scoped.yaml b/ironic/tests/unit/api/test_rbac_system_scoped.yaml index 340878c1ee..c0126b04dc 100644 --- a/ironic/tests/unit/api/test_rbac_system_scoped.yaml +++ b/ironic/tests/unit/api/test_rbac_system_scoped.yaml @@ -1160,9 +1160,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 @@ -1172,7 +1172,7 @@ volume_connectors_post_member: path: '/v1/volume/connectors' method: post headers: *scoped_member_headers - assert_status: 400 + assert_status: 201 body: *volume_connector_body volume_connectors_post_reader: @@ -1269,19 +1269,23 @@ 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: 1 volume_id: 'test-id' volume_targets_post_member: path: '/v1/volume/targets' method: post headers: *scoped_member_headers - assert_status: 400 - body: *volume_target_body + assert_status: 201 + body: + node_uuid: 1be26c0b-03f2-4d2e-ae87-c02d7f33c123 + volume_type: iscsi + boot_index: 2 + volume_id: 'test-id2' volume_targets_post_reader: path: '/v1/volume/targets' diff --git a/ironic/tests/unit/objects/test_volume_connector.py b/ironic/tests/unit/objects/test_volume_connector.py index 7030f4766d..380caf9820 100644 --- a/ironic/tests/unit/objects/test_volume_connector.py +++ b/ironic/tests/unit/objects/test_volume_connector.py @@ -84,7 +84,8 @@ class TestVolumeConnectorObject(db_base.DbTestCase, self.context, limit=4, sort_key='uuid', sort_dir='asc') mock_get_list.assert_called_once_with( - limit=4, marker=None, sort_key='uuid', sort_dir='asc') + limit=4, marker=None, sort_key='uuid', sort_dir='asc', + project=None) self.assertThat(volume_connectors, HasLength(1)) self.assertIsInstance(volume_connectors[0], objects.VolumeConnector) @@ -98,7 +99,8 @@ class TestVolumeConnectorObject(db_base.DbTestCase, self.context, limit=4, sort_key='uuid', sort_dir='asc') mock_get_list.assert_called_once_with( - limit=4, marker=None, sort_key='uuid', sort_dir='asc') + limit=4, marker=None, sort_key='uuid', sort_dir='asc', + project=None) self.assertEqual([], volume_connectors) def test_list_by_node_id(self): @@ -111,7 +113,8 @@ class TestVolumeConnectorObject(db_base.DbTestCase, self.context, node_id, limit=10, sort_dir='desc') mock_get_list_by_node_id.assert_called_once_with( - node_id, limit=10, marker=None, sort_key=None, sort_dir='desc') + node_id, limit=10, marker=None, sort_key=None, sort_dir='desc', + project=None) self.assertThat(volume_connectors, HasLength(1)) self.assertIsInstance(volume_connectors[0], objects.VolumeConnector) diff --git a/ironic/tests/unit/objects/test_volume_target.py b/ironic/tests/unit/objects/test_volume_target.py index 3882a368c1..cb57e6b396 100644 --- a/ironic/tests/unit/objects/test_volume_target.py +++ b/ironic/tests/unit/objects/test_volume_target.py @@ -83,7 +83,8 @@ class TestVolumeTargetObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): self.context, limit=4, sort_key='uuid', sort_dir='asc') mock_get_list.assert_called_once_with( - limit=4, marker=None, sort_key='uuid', sort_dir='asc') + limit=4, marker=None, sort_key='uuid', sort_dir='asc', + project=None) self.assertThat(volume_targets, HasLength(1)) self.assertIsInstance(volume_targets[0], objects.VolumeTarget) @@ -97,7 +98,8 @@ class TestVolumeTargetObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): self.context, limit=4, sort_key='uuid', sort_dir='asc') mock_get_list.assert_called_once_with( - limit=4, marker=None, sort_key='uuid', sort_dir='asc') + limit=4, marker=None, sort_key='uuid', sort_dir='asc', + project=None) self.assertEqual([], volume_targets) def test_list_by_node_id(self): @@ -109,7 +111,8 @@ class TestVolumeTargetObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): self.context, node_id, limit=10, sort_dir='desc') mock_get_list_by_node_id.assert_called_once_with( - node_id, limit=10, marker=None, sort_key=None, sort_dir='desc') + node_id, limit=10, marker=None, sort_key=None, sort_dir='desc', + project=None) self.assertThat(volume_targets, HasLength(1)) self.assertIsInstance(volume_targets[0], objects.VolumeTarget) self.assertEqual(self.context, volume_targets[0]._context) @@ -124,7 +127,7 @@ class TestVolumeTargetObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): mock_get_list_by_volume_id.assert_called_once_with( volume_id, limit=10, marker=None, - sort_key=None, sort_dir='desc') + sort_key=None, sort_dir='desc', project=None) self.assertThat(volume_targets, HasLength(1)) self.assertIsInstance(volume_targets[0], objects.VolumeTarget) self.assertEqual(self.context, volume_targets[0]._context)