Merge "Allow project scoped admins to create/delete nodes"
This commit is contained in:
commit
7f15710bc4
@ -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``.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -491,7 +491,7 @@ RELEASE_MAPPING = {
|
||||
}
|
||||
},
|
||||
'master': {
|
||||
'api': '1.79',
|
||||
'api': '1.80',
|
||||
'rpc': '1.55',
|
||||
'objects': {
|
||||
'Allocation': ['1.1'],
|
||||
|
@ -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',
|
||||
|
@ -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')
|
||||
|
@ -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 = {
|
||||
|
@ -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}'
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user