Merge "Allow project scoped admins to create/delete nodes"

This commit is contained in:
Zuul 2022-08-31 14:00:03 +00:00 committed by Gerrit Code Review
commit 7f15710bc4
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.