diff options
author | Tzu-Mainn Chen <tzumainn@redhat.com> | 2019-11-19 00:08:54 +0000 |
---|---|---|
committer | Tzu-Mainn Chen <tzumainn@redhat.com> | 2020-01-02 15:12:43 +0000 |
commit | 6f16a2268ce9c7e17c09a3ebd5f48c544e93c808 (patch) | |
tree | 5efd12ffda6abdb91a323ef1cc259e32ee993879 /ironic/api/controllers | |
parent | 2a43e371f17e78ea749bb0d17c98df24652aeb0c (diff) | |
download | ironic-6f16a2268ce9c7e17c09a3ebd5f48c544e93c808.tar.gz |
Allow node owners to administer associated ports
Expose a port's node's owner to policy, giving Ironic admins
the option of modifying the policy file to allow users
specified by a node's owner field to perform API actions on
that node's associated ports.
Change-Id: I47ecbad9729f18dacc703e2a625ef3e92bfb00ed
Story: #2006506
Task: #37539
Diffstat (limited to 'ironic/api/controllers')
-rw-r--r-- | ironic/api/controllers/v1/port.py | 62 | ||||
-rw-r--r-- | ironic/api/controllers/v1/utils.py | 46 |
2 files changed, 76 insertions, 32 deletions
diff --git a/ironic/api/controllers/v1/port.py b/ironic/api/controllers/v1/port.py index 982ac6572..26c0b1409 100644 --- a/ironic/api/controllers/v1/port.py +++ b/ironic/api/controllers/v1/port.py @@ -339,7 +339,8 @@ class PortsController(rest.RestController): def _get_ports_collection(self, node_ident, address, portgroup_ident, marker, limit, sort_key, sort_dir, - resource_url=None, fields=None, detail=None): + resource_url=None, fields=None, detail=None, + owner=None): limit = api_utils.validate_limit(limit) sort_dir = api_utils.validate_sort_dir(sort_dir) @@ -370,7 +371,8 @@ class PortsController(rest.RestController): portgroup.id, limit, marker_obj, sort_key=sort_key, - sort_dir=sort_dir) + sort_dir=sort_dir, + owner=owner) elif node_ident: # FIXME(comstud): Since all we need is the node ID, we can # make this more efficient by only querying @@ -380,13 +382,14 @@ class PortsController(rest.RestController): ports = objects.Port.list_by_node_id(api.request.context, node.id, limit, marker_obj, sort_key=sort_key, - sort_dir=sort_dir) + sort_dir=sort_dir, + owner=owner) elif address: - ports = self._get_ports_by_address(address) + ports = self._get_ports_by_address(address, owner=owner) else: ports = objects.Port.list(api.request.context, limit, marker_obj, sort_key=sort_key, - sort_dir=sort_dir) + sort_dir=sort_dir, owner=owner) parameters = {} if detail is not None: @@ -399,7 +402,7 @@ class PortsController(rest.RestController): sort_dir=sort_dir, **parameters) - def _get_ports_by_address(self, address): + def _get_ports_by_address(self, address, owner=None): """Retrieve a port by its address. :param address: MAC address of a port, to get the port which has @@ -408,7 +411,8 @@ class PortsController(rest.RestController): """ try: - port = objects.Port.get_by_address(api.request.context, address) + port = objects.Port.get_by_address(api.request.context, address, + owner=owner) return [port] except exception.PortNotFound: return [] @@ -469,8 +473,7 @@ class PortsController(rest.RestController): for that portgroup. :raises: NotAcceptable, HTTPNotFound """ - cdict = api.request.context.to_policy_values() - policy.authorize('baremetal:port:get', cdict, cdict) + owner = api_utils.check_port_list_policy() api_utils.check_allow_specify_fields(fields) self._check_allowed_port_fields(fields) @@ -493,7 +496,7 @@ class PortsController(rest.RestController): return self._get_ports_collection(node_uuid or node, address, portgroup, marker, limit, sort_key, sort_dir, fields=fields, - detail=detail) + detail=detail, owner=owner) @METRICS.timer('PortsController.detail') @expose.expose(PortCollection, types.uuid_or_name, types.uuid, @@ -523,8 +526,7 @@ class PortsController(rest.RestController): :param sort_dir: direction to sort. "asc" or "desc". Default: asc. :raises: NotAcceptable, HTTPNotFound """ - cdict = api.request.context.to_policy_values() - policy.authorize('baremetal:port:get', cdict, cdict) + owner = api_utils.check_port_list_policy() self._check_allowed_port_fields([sort_key]) if portgroup and not api_utils.allow_portgroups_subcontrollers(): @@ -546,7 +548,7 @@ class PortsController(rest.RestController): resource_url = '/'.join(['ports', 'detail']) return self._get_ports_collection(node_uuid or node, address, portgroup, marker, limit, sort_key, - sort_dir, resource_url) + sort_dir, resource_url, owner=owner) @METRICS.timer('PortsController.get_one') @expose.expose(Port, types.uuid, types.listtype) @@ -558,16 +560,15 @@ class PortsController(rest.RestController): of the resource to be returned. :raises: NotAcceptable, HTTPNotFound """ - cdict = api.request.context.to_policy_values() - policy.authorize('baremetal:port:get', cdict, cdict) - if self.parent_node_ident or self.parent_portgroup_ident: raise exception.OperationNotPermitted() + rpc_port, rpc_node = api_utils.check_port_policy_and_retrieve( + 'baremetal:port:get', port_uuid) + api_utils.check_allow_specify_fields(fields) self._check_allowed_port_fields(fields) - rpc_port = objects.Port.get_by_uuid(api.request.context, port_uuid) return Port.convert_with_links(rpc_port, fields=fields) @METRICS.timer('PortsController.post') @@ -578,13 +579,13 @@ class PortsController(rest.RestController): :param port: a port within the request body. :raises: NotAcceptable, HTTPNotFound, Conflict """ + if self.parent_node_ident or self.parent_portgroup_ident: + raise exception.OperationNotPermitted() + context = api.request.context cdict = context.to_policy_values() policy.authorize('baremetal:port:create', cdict, cdict) - if self.parent_node_ident or self.parent_portgroup_ident: - raise exception.OperationNotPermitted() - pdict = port.as_dict() self._check_allowed_port_fields(pdict) @@ -660,13 +661,14 @@ class PortsController(rest.RestController): :param patch: a json PATCH document to apply to this port. :raises: NotAcceptable, HTTPNotFound """ - context = api.request.context - cdict = context.to_policy_values() - policy.authorize('baremetal:port:update', cdict, cdict) - if self.parent_node_ident or self.parent_portgroup_ident: raise exception.OperationNotPermitted() + rpc_port, rpc_node = api_utils.check_port_policy_and_retrieve( + 'baremetal:port:update', port_uuid) + + context = api.request.context + fields_to_check = set() for field in (self.advanced_net_fields + ['portgroup_uuid', 'physical_network', @@ -677,7 +679,6 @@ class PortsController(rest.RestController): fields_to_check.add(field) self._check_allowed_port_fields(fields_to_check) - rpc_port = objects.Port.get_by_uuid(context, port_uuid) port_dict = rpc_port.as_dict() # NOTE(lucasagomes): # 1) Remove node_id because it's an internal value and @@ -708,7 +709,6 @@ class PortsController(rest.RestController): if rpc_port[field] != patch_val: rpc_port[field] = patch_val - rpc_node = objects.Node.get_by_id(context, rpc_port.node_id) if (rpc_node.provision_state == ir_states.INSPECTING and api_utils.allow_inspect_wait_state()): msg = _('Cannot update port "%(port)s" on "%(node)s" while it is ' @@ -742,15 +742,13 @@ class PortsController(rest.RestController): :param port_uuid: UUID of a port. :raises: OperationNotPermitted, HTTPNotFound """ - context = api.request.context - cdict = context.to_policy_values() - policy.authorize('baremetal:port:delete', cdict, cdict) - if self.parent_node_ident or self.parent_portgroup_ident: raise exception.OperationNotPermitted() - rpc_port = objects.Port.get_by_uuid(context, port_uuid) - rpc_node = objects.Node.get_by_id(context, rpc_port.node_id) + rpc_port, rpc_node = api_utils.check_port_policy_and_retrieve( + 'baremetal:port:delete', port_uuid) + + context = api.request.context portgroup_uuid = None if rpc_port.portgroup_id: diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index fc6d00a9d..7989c8440 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -1228,6 +1228,52 @@ def check_node_list_policy(owner=None): return owner +def check_port_policy_and_retrieve(policy_name, port_uuid): + """Check if the specified policy authorizes this request on a port. + + :param: policy_name: Name of the policy to check. + :param: port_uuid: the UUID of a port. + + :raises: HTTPForbidden if the policy forbids access. + :raises: NodeNotFound if the node is not found. + :return: RPC port identified by port_uuid and associated node + """ + context = api.request.context + cdict = context.to_policy_values() + + try: + rpc_port = objects.Port.get_by_uuid(context, port_uuid) + except exception.PortNotFound: + # don't expose non-existence of port unless requester + # has generic access to policy + policy.authorize(policy_name, cdict, cdict) + raise + + rpc_node = objects.Node.get_by_id(context, rpc_port.node_id) + target_dict = dict(cdict) + target_dict['node.owner'] = rpc_node['owner'] + policy.authorize(policy_name, target_dict, cdict) + + return rpc_port, rpc_node + + +def check_port_list_policy(): + """Check if the specified policy authorizes this request on a port. + + :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() + try: + policy.authorize('baremetal:port:list_all', cdict, cdict) + except exception.HTTPForbidden: + owner = cdict.get('project_id') + if not owner: + raise + policy.authorize('baremetal:port:list', cdict, cdict) + return owner + + def allow_build_configdrive(): """Check if building configdrive is allowed. |