From e870bd34d0ccacbaef7f4e4def2535eb28f822b9 Mon Sep 17 00:00:00 2001
From: Julia Kreger <juliaashleykreger@gmail.com>
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/<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
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)