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 Access to the underlying baremetal node is not exclusive between the
``owner`` and ``lessee``, and this use model expects that some level of ``owner`` and ``lessee``, and this use model expects that some level of
communication takes place between the appropriate parties. 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 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) 1.79 (Zed, 21.0)
---------------------- ----------------------
A node with the same name as the allocation ``name`` is moved to the 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) 1.78 (Xena, 18.2)
---------------------- ----------------------
Add endpoints to allow history events for nodes to be retrieved via Add endpoints to allow history events for nodes to be retrieved via
the REST API. the REST API.

View File

@ -2462,6 +2462,14 @@ class NodesController(rest.RestController):
raise exception.OperationNotPermitted() raise exception.OperationNotPermitted()
context = api.request.context context = api.request.context
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') api_utils.check_policy('baremetal:node:create')
reject_fields_in_newer_versions(node) reject_fields_in_newer_versions(node)
@ -2486,6 +2494,28 @@ class NodesController(rest.RestController):
if not node.get('resource_class'): if not node.get('resource_class'):
node['resource_class'] = CONF.default_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 = _replace_chassis_uuid_with_id(node)
chassis_uuid = chassis and chassis.uuid or None chassis_uuid = chassis and chassis.uuid or None
@ -2739,8 +2769,16 @@ class NodesController(rest.RestController):
raise exception.OperationNotPermitted() raise exception.OperationNotPermitted()
context = api.request.context context = api.request.context
try:
rpc_node = api_utils.check_node_policy_and_retrieve( rpc_node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:delete', node_ident, with_suffix=True) '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) chassis_uuid = _get_chassis_uuid(rpc_node)
notify.emit_start_notification(context, rpc_node, 'delete', 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.77: Add fields selector to drivers list and driver detail.
# v1.78: Add node history endpoint # v1.78: Add node history endpoint
# v1.79: Change allocation behaviour to prefer node name match # 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_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1 MINOR_1_INITIAL_VERSION = 1
MINOR_2_AVAILABLE_STATE = 2 MINOR_2_AVAILABLE_STATE = 2
@ -198,6 +198,7 @@ MINOR_76_NODE_CHANGE_BOOT_MODE = 76
MINOR_77_DRIVER_FIELDS_SELECTOR = 77 MINOR_77_DRIVER_FIELDS_SELECTOR = 77
MINOR_78_NODE_HISTORY = 78 MINOR_78_NODE_HISTORY = 78
MINOR_79_ALLOCATION_NODE_NAME = 79 MINOR_79_ALLOCATION_NODE_NAME = 79
MINOR_80_PROJECT_CREATE_DELETE_NODE = 80
# When adding another version, update: # When adding another version, update:
# - MINOR_MAX_VERSION # - MINOR_MAX_VERSION
@ -205,7 +206,7 @@ MINOR_79_ALLOCATION_NODE_NAME = 79
# explanation of what changed in the new version # explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api'] # - 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 # String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -437,11 +437,19 @@ node_policies = [
policy.DocumentedRuleDefault( policy.DocumentedRuleDefault(
name='baremetal:node:create', name='baremetal:node:create',
check_str=SYSTEM_ADMIN, check_str=SYSTEM_ADMIN,
scope_types=['system'], scope_types=['system', 'project'],
description='Create Node records', description='Create Node records',
operations=[{'path': '/nodes', 'method': 'POST'}], operations=[{'path': '/nodes', 'method': 'POST'}],
deprecated_rule=deprecated_node_create 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( policy.DocumentedRuleDefault(
name='baremetal:node:list', name='baremetal:node:list',
check_str=API_READER, check_str=API_READER,
@ -663,7 +671,14 @@ node_policies = [
operations=[{'path': '/nodes/{node_ident}', 'method': 'DELETE'}], operations=[{'path': '/nodes/{node_ident}', 'method': 'DELETE'}],
deprecated_rule=deprecated_node_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( policy.DocumentedRuleDefault(
name='baremetal:node:validate', name='baremetal:node:validate',
check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN, check_str=SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN,

View File

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

View File

@ -86,6 +86,11 @@ opts = [
'network_data_schema', 'network_data_schema',
default='$pybasedir/api/controllers/v1/network-data-schema.json', default='$pybasedir/api/controllers/v1/network-data-schema.json',
help=_("Schema for network data used by this deployment.")), 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', 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') ndict = test_api_utils.post_get_test_node(owner='cowsay')
response = self.post_json('/nodes', ndict, response = self.post_json('/nodes', ndict,
headers={api_base.Version.string: 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) self.assertEqual(http_client.CREATED, response.status_int)
result = self.get_json('/nodes/%s' % ndict['uuid'], result = self.get_json('/nodes/%s' % ndict['uuid'],
headers={api_base.Version.string: headers={api_base.Version.string:
str(api_v1.max_version())}) str(api_v1.max_version())})
self.assertEqual('cowsay', result['owner']) 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): def test_create_node_owner_old_api_version(self):
headers = {api_base.Version.string: '1.32'} headers = {api_base.Version.string: '1.32'}
ndict = test_api_utils.post_get_test_node(owner='bob') 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, body=None, assert_status=None,
assert_dict_contains=None, assert_dict_contains=None,
assert_list_length=None, assert_list_length=None,
deprecated=None): deprecated=None,
self_manage_nodes=True):
path = path.format(**self.format_data) path = path.format(**self.format_data)
self.mock_auth.side_effect = self._fake_process_request 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 # always request the latest api version
version = api_versions.max_version_string() version = api_versions.max_version_string()
rheaders = { rheaders = {

View File

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