Allow project scoped admins to create/delete nodes

Adds capabilites for a project scoped admin to
create and delete nodes in Ironic's API.

These nodes are automatically associated with the
project of the requestor.

Effectively, this does allow anyone with sufficient
privilges, i.e. admin, in an OpenStack deployment
to be able to create new baremetal nodes and delete
those baremetal nodes. In this case, the user has
the "owner" level of rights in the RBAC model.

Change-Id: I3fd9ce5de0bc600275b5c4b7a95b0f9405342688
This commit is contained in:
Julia Kreger 2022-08-10 15:14:30 -07:00
parent 4a347b3069
commit bc8705c160
11 changed files with 201 additions and 17 deletions

View File

@ -267,3 +267,16 @@ restrictive and an ``owner`` may revoke access to ``lessee``.
Access to the underlying baremetal node is not exclusive between the
``owner`` and ``lessee``, and this use model expects that some level of
communication takes place between the appropriate parties.
Can I, a project admin, create a node?
--------------------------------------
Starting in API version ``1.80``, the capability was added
to allow users with an ``admin`` role to be able to create and
delete their own nodes in Ironic.
This functionality is enabled by default, and automatically
imparts ``owner`` privileges to the created Bare Metal node.
This functionality can be disabled by setting
``[api]project_admin_can_manage_own_nodes`` to ``False``.

View File

@ -2,6 +2,12 @@
REST API Version History
========================
1.80 (Zed)
----------
This verison is a signifier of additional RBAC functionality allowing
a project scoped ``admin`` to create or delete nodes in Ironic.
1.79 (Zed, 21.0)
----------------------
A node with the same name as the allocation ``name`` is moved to the
@ -9,6 +15,7 @@ start of the derived candidate list.
1.78 (Xena, 18.2)
----------------------
Add endpoints to allow history events for nodes to be retrieved via
the REST API.

View File

@ -2462,7 +2462,15 @@ class NodesController(rest.RestController):
raise exception.OperationNotPermitted()
context = api.request.context
api_utils.check_policy('baremetal:node:create')
owned_node = False
if CONF.api.project_admin_can_manage_own_nodes:
owned_node = api_utils.check_policy_true(
'baremetal:node:create:self_owned_node')
else:
owned_node = False
if not owned_node:
api_utils.check_policy('baremetal:node:create')
reject_fields_in_newer_versions(node)
@ -2486,6 +2494,28 @@ class NodesController(rest.RestController):
if not node.get('resource_class'):
node['resource_class'] = CONF.default_resource_class
cdict = context.to_policy_values()
if cdict.get('system_scope') != 'all' and owned_node:
# This only applies when the request is not system
# scoped.
# First identify what was requested, and if there is
# a project ID to use.
project_id = None
requested_owner = node.get('owner', None)
if cdict.get('project_id', False):
project_id = cdict.get('project_id')
if requested_owner and requested_owner != project_id:
# Translation: If project scoped, and an owner has been
# requested, and that owner does not match the requestor's
# project ID value.
msg = _("Cannot create a node as a project scoped admin "
"with an owner other than your own project.")
raise exception.Invalid(msg)
# Finally, note the project ID
node['owner'] = project_id
chassis = _replace_chassis_uuid_with_id(node)
chassis_uuid = chassis and chassis.uuid or None
@ -2739,8 +2769,16 @@ class NodesController(rest.RestController):
raise exception.OperationNotPermitted()
context = api.request.context
rpc_node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:delete', node_ident, with_suffix=True)
try:
rpc_node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:delete', node_ident, with_suffix=True)
except exception.HTTPForbidden:
if not CONF.api.project_admin_can_manage_own_nodes:
raise
else:
rpc_node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:delete:self_owned_node', node_ident,
with_suffix=True)
chassis_uuid = _get_chassis_uuid(rpc_node)
notify.emit_start_notification(context, rpc_node, 'delete',

View File

@ -117,7 +117,7 @@ BASE_VERSION = 1
# v1.77: Add fields selector to drivers list and driver detail.
# v1.78: Add node history endpoint
# v1.79: Change allocation behaviour to prefer node name match
# v1.80: Marker to represent self service node creation/deletion
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
MINOR_2_AVAILABLE_STATE = 2
@ -198,6 +198,7 @@ MINOR_76_NODE_CHANGE_BOOT_MODE = 76
MINOR_77_DRIVER_FIELDS_SELECTOR = 77
MINOR_78_NODE_HISTORY = 78
MINOR_79_ALLOCATION_NODE_NAME = 79
MINOR_80_PROJECT_CREATE_DELETE_NODE = 80
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -205,7 +206,7 @@ MINOR_79_ALLOCATION_NODE_NAME = 79
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_79_ALLOCATION_NODE_NAME
MINOR_MAX_VERSION = MINOR_80_PROJECT_CREATE_DELETE_NODE
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -437,11 +437,19 @@ node_policies = [
policy.DocumentedRuleDefault(
name='baremetal:node:create',
check_str=SYSTEM_ADMIN,
scope_types=['system'],
scope_types=['system', 'project'],
description='Create Node records',
operations=[{'path': '/nodes', 'method': 'POST'}],
deprecated_rule=deprecated_node_create
),
policy.DocumentedRuleDefault(
name='baremetal:node:create:self_owned_node',
check_str=('role:admin'),
scope_types=['project'],
description='Create node records which will be tracked '
'as owned by the associated user project.',
operations=[{'path': '/nodes', 'method': 'POST'}],
),
policy.DocumentedRuleDefault(
name='baremetal:node:list',
check_str=API_READER,
@ -663,7 +671,14 @@ node_policies = [
operations=[{'path': '/nodes/{node_ident}', 'method': 'DELETE'}],
deprecated_rule=deprecated_node_delete
),
policy.DocumentedRuleDefault(
name='baremetal:node:delete:self_owned_node',
check_str=PROJECT_ADMIN,
scope_types=['project'],
description='Delete node records which are associated with '
'the requesting project.',
operations=[{'path': '/nodes/{node_ident}', 'method': 'DELETE'}],
),
policy.DocumentedRuleDefault(
name='baremetal:node:validate',
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,

View File

@ -491,7 +491,7 @@ RELEASE_MAPPING = {
}
},
'master': {
'api': '1.79',
'api': '1.80',
'rpc': '1.55',
'objects': {
'Allocation': ['1.1'],

View File

@ -86,6 +86,11 @@ opts = [
'network_data_schema',
default='$pybasedir/api/controllers/v1/network-data-schema.json',
help=_("Schema for network data used by this deployment.")),
cfg.BoolOpt('project_admin_can_manage_own_nodes',
default=True,
mutable=True,
help=_('If a project scoped administrative user is permitted '
'to create/delte baremetal nodes in their project.')),
]
opt_group = cfg.OptGroup(name='api',

View File

@ -4898,13 +4898,39 @@ class TestPost(test_api_base.BaseApiTest):
ndict = test_api_utils.post_get_test_node(owner='cowsay')
response = self.post_json('/nodes', ndict,
headers={api_base.Version.string:
str(api_v1.max_version())})
str(api_v1.max_version()),
'X-Project-Id': 'cowsay'})
self.assertEqual(http_client.CREATED, response.status_int)
result = self.get_json('/nodes/%s' % ndict['uuid'],
headers={api_base.Version.string:
str(api_v1.max_version())})
self.assertEqual('cowsay', result['owner'])
def test_create_node_owner_system_scope(self):
ndict = test_api_utils.post_get_test_node(owner='catsay')
response = self.post_json('/nodes', ndict,
headers={api_base.Version.string:
str(api_v1.max_version()),
'OpenStack-System-Scope': 'all',
'X-Roles': 'admin'})
self.assertEqual(http_client.CREATED, response.status_int)
result = self.get_json('/nodes/%s' % ndict['uuid'],
headers={api_base.Version.string:
str(api_v1.max_version())})
self.assertEqual('catsay', result['owner'])
def test_create_node_owner_recorded_project_scope(self):
ndict = test_api_utils.post_get_test_node()
response = self.post_json('/nodes', ndict,
headers={api_base.Version.string:
str(api_v1.max_version()),
'X-Project-Id': 'ravensay'})
self.assertEqual(http_client.CREATED, response.status_int)
result = self.get_json('/nodes/%s' % ndict['uuid'],
headers={api_base.Version.string:
str(api_v1.max_version())})
self.assertEqual('ravensay', result['owner'])
def test_create_node_owner_old_api_version(self):
headers = {api_base.Version.string: '1.32'}
ndict = test_api_utils.post_get_test_node(owner='bob')

View File

@ -81,10 +81,18 @@ class TestACLBase(base.BaseApiTest):
body=None, assert_status=None,
assert_dict_contains=None,
assert_list_length=None,
deprecated=None):
deprecated=None,
self_manage_nodes=True):
path = path.format(**self.format_data)
self.mock_auth.side_effect = self._fake_process_request
# Set self management override
if not self_manage_nodes:
cfg.CONF.set_override(
'project_admin_can_manage_own_nodes',
False,
'api')
# always request the latest api version
version = api_versions.max_version_string()
rheaders = {

View File

@ -89,35 +89,71 @@ owner_admin_cannot_post_nodes:
body: &node_post_body
name: node
driver: fake-driverz
assert_status: 500
assert_status: 403
self_manage_nodes: False
owner_admin_can_post_nodes:
path: '/v1/nodes'
method: post
headers: *owner_admin_headers
body: *node_post_body
assert_status: 503
self_manage_nodes: True
owner_manager_cannot_post_nodes:
path: '/v1/nodes'
method: post
headers: *owner_manager_headers
body: *node_post_body
assert_status: 500
assert_status: 403
lessee_admin_cannot_post_nodes:
path: '/v1/nodes'
method: post
headers: *lessee_admin_headers
body: *node_post_body
assert_status: 500
assert_status: 403
self_manage_nodes: False
lessee_admin_can_post_nodes:
path: '/v1/nodes'
method: post
headers: *lessee_admin_headers
body: *node_post_body
assert_status: 403
self_manage_nodes: False
lessee_manager_cannot_post_nodes:
path: '/v1/nodes'
method: post
headers: *lessee_manager_headers
body: *node_post_body
assert_status: 500
assert_status: 403
self_manage_nodes: False
lessee_manager_can_post_nodes:
path: '/v1/nodes'
method: post
headers: *lessee_manager_headers
body: *node_post_body
assert_status: 403
self_manage_nodes: True
third_party_admin_cannot_post_nodes:
path: '/v1/nodes'
method: post
headers: *third_party_admin_headers
body: *node_post_body
assert_status: 500
assert_status: 403
self_manage_nodes: False
third_party_admin_can_post_nodes:
path: '/v1/nodes'
method: post
headers: *third_party_admin_headers
body: *node_post_body
assert_status: 503
self_manage_nodes: True
# Based on nodes_post_member
owner_member_cannot_post_nodes:
@ -125,7 +161,7 @@ owner_member_cannot_post_nodes:
method: post
headers: *owner_member_headers
body: *node_post_body
assert_status: 500
assert_status: 403
# Based on nodes_post_reader
owner_reader_cannot_post_reader:
@ -133,7 +169,7 @@ owner_reader_cannot_post_reader:
method: post
headers: *owner_reader_headers
body: *node_post_body
assert_status: 500
assert_status: 403
# Based on nodes_get_admin
# TODO: Create 3 nodes, 2 owned, 1 leased where it is also owned.
@ -671,6 +707,14 @@ owner_admin_cannot_delete_nodes:
method: delete
headers: *owner_admin_headers
assert_status: 403
self_manage_nodes: False
owner_admin_can_delete_nodes:
path: '/v1/nodes/{owner_node_ident}'
method: delete
headers: *owner_admin_headers
assert_status: 503
self_manage_nodes: True
owner_manager_cannot_delete_nodes:
path: '/v1/nodes/{owner_node_ident}'

View File

@ -0,0 +1,27 @@
---
features:
- |
Adds the capability for a project scoped ``admin`` user to be able to
create nodes in Ironic, which are then manageable by the project scoped
``admin`` user. Effectively, this is self service Bare Metal as a Service,
however more advanced fields such as drivers, chassies, are not available
to these users. This is controlled through an auto-population of the
Node ``owner`` field, and can be controlled through the
``[api]project_admin_can_manage_own_nodes`` setting, which defaults to
``True``, and the new policy ``baremetal:node:create:self_owned_node``.
- |
Adds the capability for a project scoped ``admin`` user to be able to
delete nodes from Ironic which their `project` owns. This can be
contolled through the ``[api]project_admin_can_manage_own_nodes``
setting, which defaults to ``True``, as well as the
``baremetal:node:delete:self_owned_node`` policy.
security:
- |
This release contains an improvement which, by default, allows users to
create and delete baremetal nodes inside their own project. This can be
disabled using the ``[api]project_admin_can_manage_own_nodes`` setting.
upgrades:
- |
The API version has been increased to ``1.80`` in order to signify
the addition of additoinal Role Based Access Controls capabilities
around node creation and deletion.