Allocation support for project scoped RBAC
Adds policy scope based RBAC handling for the allocations endpoing which enables admins to create allocations if they have baremetal nodes which are available to them. Change-Id: I60e273afaf344fded9bdb8c4c8e143efc9971fc1
This commit is contained in:
parent
ebaa359937
commit
88673f1e94
@ -68,10 +68,12 @@ Supported Endpoints
|
||||
* /nodes/<uuid>/portgroups
|
||||
* /nodes/<uuid>/volume/connectors
|
||||
* /nodes/<uuid>/volume/targets
|
||||
* /nodes/<uuid>/allocation
|
||||
* /ports
|
||||
* /portgroups
|
||||
* /volume/connectors
|
||||
* /volume/targets
|
||||
* /allocations
|
||||
|
||||
How Project Scoped Works
|
||||
------------------------
|
||||
@ -147,6 +149,31 @@ change the owner.
|
||||
|
||||
More information is available on these fields in :doc:`/configuration/policy`.
|
||||
|
||||
Allocations
|
||||
~~~~~~~~~~~
|
||||
|
||||
The ``allocations`` endpoint of the API is somewhat different than other
|
||||
other endpoints as it allows for the allocation of physical machines to
|
||||
an admin. In this context, there is not already an ``owner`` or ``project_id``
|
||||
to leverage to control access for the creation process, any project admin
|
||||
does have the inherent prilege of requesting an allocation. That being said,
|
||||
their allocation request will require physical nodes to be owned or leased
|
||||
to the ``project_id`` through the ``node`` fields ``owner`` or ``lessee``.
|
||||
|
||||
Ability to override the owner is restricted to system scoped users by default
|
||||
and any new allocation being requested with a specific owner, if made in
|
||||
``project`` scope, will have the ``project_id`` recorded as the owner of
|
||||
the allocation.
|
||||
|
||||
.. WARNING:: The allocation endpoint's use is restricted to project scoped
|
||||
interactions until ``[oslo_policy]enforce_new_defaults`` has been set
|
||||
to ``True`` using the ``baremetal:allocation:create_pre_rbac`` policy
|
||||
rule. This is in order to prevent endpoint misuse. Afterwards all
|
||||
project scoped allocations will automatically populate an owner.
|
||||
System scoped request are not subjected to this restriction,
|
||||
and operators may change the default restriction via the
|
||||
``baremetal:allocation:create_restricted`` policy.
|
||||
|
||||
Pratical differences
|
||||
--------------------
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
import pecan
|
||||
from webob import exc as webob_exc
|
||||
@ -28,8 +29,10 @@ from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic import objects
|
||||
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
||||
ALLOCATION_SCHEMA = {
|
||||
'type': 'object',
|
||||
@ -133,7 +136,8 @@ class AllocationsController(pecan.rest.RestController):
|
||||
def _get_allocations_collection(self, node_ident=None, resource_class=None,
|
||||
state=None, owner=None, marker=None,
|
||||
limit=None, sort_key='id', sort_dir='asc',
|
||||
resource_url=None, fields=None):
|
||||
resource_url=None, fields=None,
|
||||
parent_node=None):
|
||||
"""Return allocations collection.
|
||||
|
||||
:param node_ident: UUID or name of a node.
|
||||
@ -145,6 +149,9 @@ class AllocationsController(pecan.rest.RestController):
|
||||
:param fields: Optional, a list with a specified set of fields
|
||||
of the resource to be returned.
|
||||
:param owner: project_id of owner to filter by
|
||||
:param parent_node: The explicit parent node uuid to return if
|
||||
the controller is being accessed as a
|
||||
sub-resource. i.e. /v1/nodes/<uuid>/allocation
|
||||
"""
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
@ -154,17 +161,48 @@ class AllocationsController(pecan.rest.RestController):
|
||||
_("The sort_key value %(key)s is an invalid field for "
|
||||
"sorting") % {'key': sort_key})
|
||||
|
||||
node_uuid = None
|
||||
# If the user is not allowed to see everything, we need to filter
|
||||
# based upon access rights.
|
||||
cdict = api.request.context.to_policy_values()
|
||||
if cdict.get('system_scope') != 'all' and not parent_node:
|
||||
# The user is a project scoped, and there is not an explicit
|
||||
# parent node which will be returned.
|
||||
if not api_utils.check_policy_true(
|
||||
'baremetal:allocation:list_all'):
|
||||
# If the user cannot see everything via the policy,
|
||||
# we need to filter the view down to only what they should
|
||||
# be able to see in the database.
|
||||
owner = cdict.get('project_id')
|
||||
else:
|
||||
# The controller is being accessed as a sub-resource.
|
||||
# Cool, but that means there can only be one result.
|
||||
node_uuid = parent_node
|
||||
# Override if any node_ident was submitted in since this
|
||||
# is a subresource query.
|
||||
node_ident = parent_node
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
marker_obj = objects.Allocation.get_by_uuid(api.request.context,
|
||||
marker)
|
||||
|
||||
if node_ident:
|
||||
try:
|
||||
node_uuid = api_utils.get_rpc_node(node_ident).uuid
|
||||
# Check ability to access the associated node or requested
|
||||
# node to filter by.
|
||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||
api_utils.check_owner_policy('node', 'baremetal:node:get',
|
||||
rpc_node.owner,
|
||||
lessee=rpc_node.lessee,
|
||||
conceal_node=False)
|
||||
node_uuid = rpc_node.uuid
|
||||
except exception.NodeNotFound as exc:
|
||||
exc.code = http_client.BAD_REQUEST
|
||||
raise
|
||||
except exception.NotAuthorized as exc:
|
||||
if not parent_node:
|
||||
exc.code = http_client.BAD_REQUEST
|
||||
raise exception.NotFound()
|
||||
else:
|
||||
node_uuid = None
|
||||
|
||||
@ -179,13 +217,16 @@ class AllocationsController(pecan.rest.RestController):
|
||||
for key, value in possible_filters.items():
|
||||
if value is not None:
|
||||
filters[key] = value
|
||||
|
||||
allocations = objects.Allocation.list(api.request.context,
|
||||
limit=limit,
|
||||
marker=marker_obj,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir,
|
||||
filters=filters)
|
||||
for allocation in allocations:
|
||||
api_utils.check_owner_policy('allocation',
|
||||
'baremetal:allocation:get',
|
||||
allocation.owner)
|
||||
return list_convert_with_links(allocations, limit,
|
||||
url=resource_url,
|
||||
fields=fields,
|
||||
@ -267,18 +308,33 @@ class AllocationsController(pecan.rest.RestController):
|
||||
def _authorize_create_allocation(self, allocation):
|
||||
|
||||
try:
|
||||
# PRE-RBAC this rule was logically restricted, it is more-unlocked
|
||||
# post RBAC, but we need to ensure it is not abused.
|
||||
api_utils.check_policy('baremetal:allocation:create')
|
||||
self._check_allowed_allocation_fields(allocation)
|
||||
if (not CONF.oslo_policy.enforce_new_defaults
|
||||
and not allocation.get('owner')):
|
||||
# Even if permitted, we need to go ahead and check if this is
|
||||
# restricted for now until scoped interaction is the default
|
||||
# interaction.
|
||||
api_utils.check_policy('baremetal:allocation:create_pre_rbac')
|
||||
# TODO(TheJulia): This can be removed later once we
|
||||
# move entirely to scope based checking. This requires
|
||||
# that if the scope enforcement is not enabled, that
|
||||
# any user can't create an allocation until the deployment
|
||||
# is in a new operating mode *where* owner will be added
|
||||
# automatically if not a privilged user.
|
||||
except exception.HTTPForbidden:
|
||||
cdict = api.request.context.to_policy_values()
|
||||
owner = cdict.get('project_id')
|
||||
if not owner or (allocation.get('owner')
|
||||
and owner != allocation.get('owner')):
|
||||
project = cdict.get('project_id')
|
||||
if (project and allocation.get('owner')
|
||||
and project != allocation.get('owner')):
|
||||
raise
|
||||
if project and not CONF.oslo_policy.enforce_new_defaults:
|
||||
api_utils.check_policy('baremetal:allocation:create_pre_rbac')
|
||||
api_utils.check_policy('baremetal:allocation:create_restricted')
|
||||
self._check_allowed_allocation_fields(allocation)
|
||||
allocation['owner'] = owner
|
||||
|
||||
allocation['owner'] = project
|
||||
return allocation
|
||||
|
||||
@METRICS.timer('AllocationsController.post')
|
||||
@ -291,6 +347,7 @@ class AllocationsController(pecan.rest.RestController):
|
||||
:param allocation: an allocation within the request body.
|
||||
"""
|
||||
context = api.request.context
|
||||
cdict = context.to_policy_values()
|
||||
allocation = self._authorize_create_allocation(allocation)
|
||||
|
||||
if (allocation.get('name')
|
||||
@ -299,11 +356,47 @@ class AllocationsController(pecan.rest.RestController):
|
||||
"'%(name)s'") % {'name': allocation['name']}
|
||||
raise exception.Invalid(msg)
|
||||
|
||||
# TODO(TheJulia): We need to likely look at refactoring post
|
||||
# processing for allocations as pep8 says it is a complexity of 19,
|
||||
# although it is not actually that horrible since it is phased out
|
||||
# just modifying/assembling the allocation. Given that, it seems
|
||||
# not great to try for a full method rewrite at the same time as
|
||||
# RBAC work, so the complexity limit is being raised. :(
|
||||
if (CONF.oslo_policy.enforce_new_defaults
|
||||
and cdict.get('system_scope') != 'all'):
|
||||
# if not a system scope originated request, we need to check/apply
|
||||
# an owner - But we can only do this with when new defaults are
|
||||
# enabled.
|
||||
project_id = cdict.get('project_id')
|
||||
req_alloc_owner = allocation.get('owner')
|
||||
if req_alloc_owner:
|
||||
if not api_utils.check_policy_true(
|
||||
'baremetal:allocation:create_restricted'):
|
||||
if req_alloc_owner != project_id:
|
||||
msg = _("Cannot create allocation with an owner "
|
||||
"Project ID value %(req_owner)s not matching "
|
||||
"the requestor Project ID %(project)s. "
|
||||
"Policy baremetal:allocation:create_restricted"
|
||||
" is required for this capability."
|
||||
) % {'req_owner': req_alloc_owner,
|
||||
'project': project_id}
|
||||
raise exception.NotAuthorized(msg)
|
||||
# NOTE(TheJulia): IF not restricted, i.e. else above,
|
||||
# their supplied allocation owner is okay, they are allowed
|
||||
# to provide an override by policy.
|
||||
else:
|
||||
# An allocation owner was not supplied, we need to save one.
|
||||
allocation['owner'] = project_id
|
||||
|
||||
node = None
|
||||
if allocation.get('node'):
|
||||
if api_utils.allow_allocation_backfill():
|
||||
try:
|
||||
node = api_utils.get_rpc_node(allocation['node'])
|
||||
api_utils.check_owner_policy(
|
||||
'node', 'baremetal:node:get',
|
||||
node.owner, node.lessee,
|
||||
conceal_node=allocation['node'])
|
||||
except exception.NodeNotFound as exc:
|
||||
exc.code = http_client.BAD_REQUEST
|
||||
raise
|
||||
@ -323,8 +416,17 @@ class AllocationsController(pecan.rest.RestController):
|
||||
if allocation.get('candidate_nodes'):
|
||||
# Convert nodes from names to UUIDs and check their validity
|
||||
try:
|
||||
owner = None
|
||||
if not api_utils.check_policy_true(
|
||||
'baremetal:allocation:create_restricted'):
|
||||
owner = cdict.get('project_id')
|
||||
# Filter the candidate search by the requestor project ID
|
||||
# if any. The result is processes authenticating with system
|
||||
# scope will not be impacted, where as project scoped requests
|
||||
# will need additional authorization.
|
||||
converted = api.request.dbapi.check_node_list(
|
||||
allocation['candidate_nodes'])
|
||||
allocation['candidate_nodes'],
|
||||
project=owner)
|
||||
except exception.NodeNotFound as exc:
|
||||
exc.code = http_client.BAD_REQUEST
|
||||
raise
|
||||
@ -458,10 +560,12 @@ class NodeAllocationController(pecan.rest.RestController):
|
||||
@method.expose()
|
||||
@args.validate(fields=args.string_list)
|
||||
def get_all(self, fields=None):
|
||||
api_utils.check_policy('baremetal:allocation:get')
|
||||
parent_node = self.parent_node_ident
|
||||
result = self.inner._get_allocations_collection(
|
||||
parent_node,
|
||||
fields=fields,
|
||||
parent_node=parent_node)
|
||||
|
||||
result = self.inner._get_allocations_collection(self.parent_node_ident,
|
||||
fields=fields)
|
||||
try:
|
||||
return result['allocations'][0]
|
||||
except IndexError:
|
||||
@ -473,15 +577,26 @@ class NodeAllocationController(pecan.rest.RestController):
|
||||
@method.expose(status_code=http_client.NO_CONTENT)
|
||||
def delete(self):
|
||||
context = api.request.context
|
||||
api_utils.check_policy('baremetal:allocation:delete')
|
||||
|
||||
rpc_node = api_utils.get_rpc_node_with_suffix(self.parent_node_ident)
|
||||
# Check the policy, and 404 if not authorized.
|
||||
api_utils.check_owner_policy('node', 'baremetal:node:get',
|
||||
rpc_node.owner, lessee=rpc_node.lessee,
|
||||
conceal_node=self.parent_node_ident)
|
||||
|
||||
# A project ID is associated, thus we should filter
|
||||
# our search using it.
|
||||
filters = {'node_uuid': rpc_node.uuid}
|
||||
allocations = objects.Allocation.list(
|
||||
api.request.context,
|
||||
filters={'node_uuid': rpc_node.uuid})
|
||||
filters=filters)
|
||||
|
||||
try:
|
||||
rpc_allocation = allocations[0]
|
||||
allocation_owner = allocations[0]['owner']
|
||||
api_utils.check_owner_policy('allocation',
|
||||
'baremetal:allocation:delete',
|
||||
allocation_owner)
|
||||
except IndexError:
|
||||
raise exception.AllocationNotFound(
|
||||
_("Allocation for node %s was not found") %
|
||||
|
@ -1507,9 +1507,23 @@ def check_policy(policy_name):
|
||||
# effectively a noop from an authorization perspective because the values
|
||||
# we're comparing are coming from the same place.
|
||||
cdict = api.request.context.to_policy_values()
|
||||
|
||||
policy.authorize(policy_name, cdict, api.request.context)
|
||||
|
||||
|
||||
def check_policy_true(policy_name):
|
||||
"""Check if the specified policy is authorised for this request.
|
||||
|
||||
:policy_name: Name of the policy to check.
|
||||
:returns: True if policy is matched, otherwise false.
|
||||
"""
|
||||
# NOTE(lbragstad): Mapping context attributes into a target dictionary is
|
||||
# effectively a noop from an authorization perspective because the values
|
||||
# we're comparing are coming from the same place.
|
||||
cdict = api.request.context.to_policy_values()
|
||||
return policy.check_policy(policy_name, cdict, api.request.context)
|
||||
|
||||
|
||||
def check_owner_policy(object_type, policy_name, owner, lessee=None,
|
||||
conceal_node=False):
|
||||
"""Check if the policy authorizes this request on an object.
|
||||
@ -1592,13 +1606,13 @@ def check_allocation_policy_and_retrieve(policy_name, allocation_ident):
|
||||
try:
|
||||
rpc_allocation = get_rpc_allocation_with_suffix(
|
||||
allocation_ident)
|
||||
except exception.AllocationNotFound:
|
||||
# don't expose non-existence unless requester
|
||||
# has generic access to policy
|
||||
cdict = api.request.context.to_policy_values()
|
||||
policy.authorize(policy_name, cdict, api.request.context)
|
||||
raise
|
||||
|
||||
# If the user is not allowed to view the allocation, then
|
||||
# we need to check that and respond with a 404.
|
||||
check_owner_policy('allocation', 'baremetal:allocation:get',
|
||||
rpc_allocation['owner'])
|
||||
except exception.NotAuthorized:
|
||||
raise exception.AllocationNotFound(allocation=allocation_ident)
|
||||
# The primary policy check for allocation.
|
||||
check_owner_policy('allocation', policy_name, rpc_allocation['owner'])
|
||||
return rpc_allocation
|
||||
|
||||
|
@ -92,6 +92,10 @@ PROJECT_OWNER_MEMBER = ('role:member and project_id:%(node.owner)s')
|
||||
PROJECT_OWNER_READER = ('role:reader and project_id:%(node.owner)s')
|
||||
PROJECT_LESSEE_ADMIN = ('role:admin and project_id:%(node.lessee)s')
|
||||
|
||||
ALLOCATION_OWNER_ADMIN = ('role:admin and project_id:%(allocation.owner)s')
|
||||
ALLOCATION_OWNER_MEMBER = ('role:member and project_id:%(allocation.owner)s')
|
||||
ALLOCATION_OWNER_READER = ('role:reader and project_id:%(allocation.owner)s')
|
||||
|
||||
SYSTEM_OR_OWNER_MEMBER_AND_LESSEE_ADMIN = (
|
||||
'(' + SYSTEM_MEMBER + ') or (' + PROJECT_OWNER_MEMBER + ') or (' + PROJECT_LESSEE_ADMIN + ')' # noqa
|
||||
)
|
||||
@ -117,6 +121,25 @@ SYSTEM_MEMBER_OR_OWNER_LESSEE_ADMIN = (
|
||||
)
|
||||
|
||||
|
||||
# The system has rights to manage the allocations for users, in a sense
|
||||
# a delegated management since they are not creating a real object or asset,
|
||||
# but allocations of assets.
|
||||
ALLOCATION_ADMIN = (
|
||||
'(' + SYSTEM_MEMBER + ') or (' + ALLOCATION_OWNER_ADMIN + ')'
|
||||
)
|
||||
|
||||
ALLOCATION_MEMBER = (
|
||||
'(' + SYSTEM_MEMBER + ') or (' + ALLOCATION_OWNER_MEMBER + ')'
|
||||
)
|
||||
|
||||
ALLOCATION_READER = (
|
||||
'(' + SYSTEM_READER + ') or (' + ALLOCATION_OWNER_READER + ')'
|
||||
)
|
||||
|
||||
ALLOCATION_CREATOR = (
|
||||
'(' + SYSTEM_MEMBER + ') or (role:admin)'
|
||||
)
|
||||
|
||||
# Special purpose aliases for things like "ability to access the API
|
||||
# as a reader, or permission checking that does not require node
|
||||
# owner relationship checking
|
||||
@ -1497,7 +1520,7 @@ deprecated_allocation_list = policy.DeprecatedRule(
|
||||
)
|
||||
deprecated_allocation_list_all = policy.DeprecatedRule(
|
||||
name='baremetal:allocation:list_all',
|
||||
check_str='rule:baremetal:allocation:get'
|
||||
check_str='rule:baremetal:allocation:get and is_admin_project:True'
|
||||
)
|
||||
deprecated_allocation_create = policy.DeprecatedRule(
|
||||
name='baremetal:allocation:create',
|
||||
@ -1523,8 +1546,8 @@ roles.
|
||||
allocation_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:allocation:get',
|
||||
check_str=SYSTEM_READER,
|
||||
scope_types=['system'],
|
||||
check_str=ALLOCATION_READER,
|
||||
scope_types=['system', 'project'],
|
||||
description='Retrieve Allocation records',
|
||||
operations=[
|
||||
{'path': '/allocations/{allocation_id}', 'method': 'GET'},
|
||||
@ -1536,8 +1559,8 @@ allocation_policies = [
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:allocation:list',
|
||||
check_str=SYSTEM_READER,
|
||||
scope_types=['system'],
|
||||
check_str=API_READER,
|
||||
scope_types=['system', 'project'],
|
||||
description='Retrieve multiple Allocation records, filtered by owner',
|
||||
operations=[{'path': '/allocations', 'method': 'GET'}],
|
||||
deprecated_rule=deprecated_allocation_list,
|
||||
@ -1547,7 +1570,7 @@ allocation_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:allocation:list_all',
|
||||
check_str=SYSTEM_READER,
|
||||
scope_types=['system'],
|
||||
scope_types=['system', 'project'],
|
||||
description='Retrieve multiple Allocation records',
|
||||
operations=[{'path': '/allocations', 'method': 'GET'}],
|
||||
deprecated_rule=deprecated_allocation_list_all,
|
||||
@ -1556,8 +1579,8 @@ allocation_policies = [
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:allocation:create',
|
||||
check_str=SYSTEM_MEMBER,
|
||||
scope_types=['system'],
|
||||
check_str=ALLOCATION_CREATOR,
|
||||
scope_types=['system', 'project'],
|
||||
description='Create Allocation records',
|
||||
operations=[{'path': '/allocations', 'method': 'POST'}],
|
||||
deprecated_rule=deprecated_allocation_create,
|
||||
@ -1567,9 +1590,9 @@ allocation_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:allocation:create_restricted',
|
||||
check_str=SYSTEM_MEMBER,
|
||||
scope_types=['system'],
|
||||
scope_types=['system', 'project'],
|
||||
description=(
|
||||
'Create Allocation records that are restricted to an owner'
|
||||
'Create Allocation records with a specific owner.'
|
||||
),
|
||||
operations=[{'path': '/allocations', 'method': 'POST'}],
|
||||
deprecated_rule=deprecated_allocation_create_restricted,
|
||||
@ -1578,8 +1601,8 @@ allocation_policies = [
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:allocation:delete',
|
||||
check_str=SYSTEM_MEMBER,
|
||||
scope_types=['system'],
|
||||
check_str=ALLOCATION_ADMIN,
|
||||
scope_types=['system', 'project'],
|
||||
description='Delete Allocation records',
|
||||
operations=[
|
||||
{'path': '/allocations/{allocation_id}', 'method': 'DELETE'},
|
||||
@ -1590,8 +1613,8 @@ allocation_policies = [
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:allocation:update',
|
||||
check_str=SYSTEM_MEMBER,
|
||||
scope_types=['system'],
|
||||
check_str=ALLOCATION_MEMBER,
|
||||
scope_types=['system', 'project'],
|
||||
description='Change name and extra fields of an allocation',
|
||||
operations=[
|
||||
{'path': '/allocations/{allocation_id}', 'method': 'PATCH'},
|
||||
@ -1600,6 +1623,22 @@ allocation_policies = [
|
||||
deprecated_reason=deprecated_allocation_reason,
|
||||
deprecated_since=versionutils.deprecated.WALLABY
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:allocation:create_pre_rbac',
|
||||
check_str=('rule:is_member and role:baremetal_admin or '
|
||||
'is_admin_project:True and role:admin'),
|
||||
scope_types=['project'],
|
||||
description=('Logical restrictor to prevent legacy allocation rule '
|
||||
'missuse - Requires blank allocations to originate from '
|
||||
'the legacy baremetal_admin.'),
|
||||
operations=[
|
||||
{'path': '/allocations/{allocation_id}', 'method': 'PATCH'},
|
||||
],
|
||||
deprecated_reason=deprecated_allocation_reason,
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since=versionutils.deprecated.WALLABY
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
@ -439,7 +439,7 @@ class Connection(api.Connection):
|
||||
return _paginate_query(models.Node, limit, marker,
|
||||
sort_key, sort_dir, query)
|
||||
|
||||
def check_node_list(self, idents):
|
||||
def check_node_list(self, idents, project=None):
|
||||
mapping = {}
|
||||
if idents:
|
||||
idents = set(idents)
|
||||
@ -459,6 +459,10 @@ class Connection(api.Connection):
|
||||
sql.or_(models.Node.uuid.in_(uuids),
|
||||
models.Node.name.in_(names))
|
||||
)
|
||||
if project:
|
||||
query = query.filter((models.Node.owner == project)
|
||||
| (models.Node.lessee == project))
|
||||
|
||||
for row in query:
|
||||
if row[0] in idents:
|
||||
mapping[row[0]] = row[0]
|
||||
|
@ -15,10 +15,12 @@ Tests for the API /allocations/ methods.
|
||||
|
||||
import datetime
|
||||
from http import client as http_client
|
||||
import json
|
||||
from unittest import mock
|
||||
from urllib import parse as urlparse
|
||||
|
||||
import fixtures
|
||||
from keystonemiddleware import auth_token
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
@ -391,6 +393,15 @@ class TestListAllocations(test_api_base.BaseApiTest):
|
||||
owner=owner,
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
name='allocation%s' % i)
|
||||
# NOTE(TheJulia): Force the cast of the action to a system scoped
|
||||
# scoped request. System scoped is allowed to view everything,
|
||||
# where as project scoped requests are actually filtered with the
|
||||
# secure-rbac work. This was done in troubleshooting the code,
|
||||
# so may not be necessary, but filtered views are checked in
|
||||
# the RBAC testing.
|
||||
headers = self.headers
|
||||
headers['X-Roles'] = "member,reader"
|
||||
headers['OpenStack-System-Scope'] = "all"
|
||||
data = self.get_json("/allocations?owner=12345",
|
||||
headers=self.headers)
|
||||
self.assertEqual(3, len(data['allocations']))
|
||||
@ -994,6 +1005,57 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||
|
||||
@mock.patch.object(auth_token.AuthProtocol, 'process_request',
|
||||
autospec=True)
|
||||
def test_create_allocation_owner_not_my_projet_id(self, mock_auth_req):
|
||||
# This is only enforced, test wise with the new oslo policy rbac
|
||||
# model and enforcement. Likely can be cleaned up past the Xena cycle.
|
||||
cfg.CONF.set_override('enforce_scope', True, group='oslo_policy')
|
||||
cfg.CONF.set_override('enforce_new_defaults', True,
|
||||
group='oslo_policy')
|
||||
# Tests normally run in noauth, but we need policy
|
||||
# enforcement to run completely here to ensure the logic is followed.
|
||||
cfg.CONF.set_override('auth_strategy', 'keystone')
|
||||
self.headers['X-Project-ID'] = '0987'
|
||||
self.headers['X-Roles'] = 'admin,member,reader'
|
||||
owner = '12345'
|
||||
adict = apiutils.allocation_post_data(owner=owner)
|
||||
response = self.post_json('/allocations', adict, expect_errors=True,
|
||||
headers=self.headers)
|
||||
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||
expected_faultstring = ('Cannot create allocation with an owner '
|
||||
'Project ID value 12345 not matching the '
|
||||
'requestor Project ID 0987. Policy '
|
||||
'baremetal:allocation:create_restricted '
|
||||
'is required for this capability.')
|
||||
error_body = json.loads(response.json['error_message'])
|
||||
self.assertEqual(expected_faultstring,
|
||||
error_body.get('faultstring'))
|
||||
|
||||
def test_create_allocation_owner_auto_filled(self):
|
||||
cfg.CONF.set_override('enforce_new_defaults', True,
|
||||
group='oslo_policy')
|
||||
self.headers['X-Project-ID'] = '123456'
|
||||
adict = apiutils.allocation_post_data()
|
||||
self.post_json('/allocations', adict, headers=self.headers)
|
||||
result = self.get_json('/allocations/%s' % adict['uuid'],
|
||||
headers=self.headers)
|
||||
self.assertEqual('123456', result['owner'])
|
||||
|
||||
def test_create_allocation_is_restricted_until_scope_enforcement(self):
|
||||
cfg.CONF.set_override('enforce_new_defaults', False,
|
||||
group='oslo_policy')
|
||||
cfg.CONF.set_override('auth_strategy', 'keystone')
|
||||
# We're setting ourselves to be a random ID and member
|
||||
# which is allowed to create an allocation.
|
||||
self.headers['X-Project-ID'] = '1135'
|
||||
self.headers['X-Roles'] = 'admin, member, reader'
|
||||
self.headers['X-Is-Admin-Project'] = 'False'
|
||||
adict = apiutils.allocation_post_data()
|
||||
response = self.post_json('/allocations', adict, expect_errors=True,
|
||||
headers=self.headers)
|
||||
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||
|
||||
def test_backfill(self):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
adict = apiutils.allocation_post_data(node=node.uuid)
|
||||
@ -1092,7 +1154,6 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
raise exception.HTTPForbidden(resource='fake')
|
||||
return True
|
||||
mock_authorize.side_effect = mock_authorize_function
|
||||
|
||||
owner = '12345'
|
||||
adict = apiutils.allocation_post_data()
|
||||
del adict['owner']
|
||||
@ -1126,7 +1187,6 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
raise exception.HTTPForbidden(resource='fake')
|
||||
return True
|
||||
mock_authorize.side_effect = mock_authorize_function
|
||||
|
||||
owner = '12345'
|
||||
adict = apiutils.allocation_post_data(owner=owner)
|
||||
adict['owner'] = owner
|
||||
|
@ -1253,14 +1253,18 @@ class TestCheckAllocationPolicyAndRetrieve(base.TestCase):
|
||||
'fake_policy', self.valid_allocation_uuid
|
||||
)
|
||||
mock_graws.assert_called_once_with(self.valid_allocation_uuid)
|
||||
mock_authorize.assert_called_once_with(
|
||||
'fake_policy', expected_target, fake_context)
|
||||
|
||||
expected = [mock.call('baremetal:allocation:get',
|
||||
expected_target, fake_context),
|
||||
mock.call('fake_policy', expected_target,
|
||||
fake_context)]
|
||||
mock_authorize.assert_has_calls(expected)
|
||||
self.assertEqual(self.allocation, rpc_allocation)
|
||||
|
||||
@mock.patch.object(api, 'request', spec_set=["context"])
|
||||
@mock.patch.object(policy, 'authorize', spec=True)
|
||||
@mock.patch.object(utils, 'get_rpc_allocation_with_suffix', autospec=True)
|
||||
def test_check_alloc_policy_and_retrieve_no_alloc_policy_forbidden(
|
||||
def test_check_alloc_policy_and_retrieve_no_alloc_policy_not_found(
|
||||
self, mock_graws, mock_authorize, mock_pr
|
||||
):
|
||||
mock_pr.context.to_policy_values.return_value = {}
|
||||
@ -1269,7 +1273,7 @@ class TestCheckAllocationPolicyAndRetrieve(base.TestCase):
|
||||
allocation=self.valid_allocation_uuid)
|
||||
|
||||
self.assertRaises(
|
||||
exception.HTTPForbidden,
|
||||
exception.AllocationNotFound,
|
||||
utils.check_allocation_policy_and_retrieve,
|
||||
'fake-policy',
|
||||
self.valid_allocation_uuid
|
||||
@ -1295,7 +1299,7 @@ class TestCheckAllocationPolicyAndRetrieve(base.TestCase):
|
||||
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
||||
@mock.patch.object(policy, 'authorize', spec=True)
|
||||
@mock.patch.object(utils, 'get_rpc_allocation_with_suffix', autospec=True)
|
||||
def test_check_allocation_policy_and_retrieve_policy_forbidden(
|
||||
def test_check_allocation_policy_and_retrieve_policy_not_found(
|
||||
self, mock_graws, mock_authorize, mock_pr
|
||||
):
|
||||
mock_pr.version.minor = 50
|
||||
@ -1304,7 +1308,7 @@ class TestCheckAllocationPolicyAndRetrieve(base.TestCase):
|
||||
mock_graws.return_value = self.allocation
|
||||
|
||||
self.assertRaises(
|
||||
exception.HTTPForbidden,
|
||||
exception.AllocationNotFound,
|
||||
utils.check_allocation_policy_and_retrieve,
|
||||
'fake-policy',
|
||||
self.valid_allocation_uuid
|
||||
|
@ -398,9 +398,15 @@ class TestRBACProjectScoped(TestACLBase):
|
||||
uuid='65ea0296-219b-4635-b0c8-a6e055da878d',
|
||||
node_id=owned_node['id'],
|
||||
connector_id='iqn.2012-06.org.openstack.magic')
|
||||
fake_owner_allocation = db_utils.create_test_allocation(
|
||||
node_id=owned_node['id'],
|
||||
owner=owner_project_id,
|
||||
resource_class="CUSTOM_TEST")
|
||||
|
||||
# Leased nodes
|
||||
fake_allocation_id = 61
|
||||
leased_node = db_utils.create_test_node(
|
||||
allocation_id=fake_allocation_id,
|
||||
uuid=lessee_node_ident,
|
||||
owner=owner_project_id,
|
||||
lessee=lessee_project_id,
|
||||
@ -416,6 +422,11 @@ class TestRBACProjectScoped(TestACLBase):
|
||||
node_id=leased_node['id'])
|
||||
fake_trait = 'CUSTOM_MEOW'
|
||||
fake_vif_port_id = "0e21d58f-5de2-4956-85ff-33935ea1ca01"
|
||||
fake_leased_allocation = db_utils.create_test_allocation(
|
||||
id=fake_allocation_id,
|
||||
node_id=leased_node['id'],
|
||||
owner=lessee_project_id,
|
||||
resource_class="CUSTOM_LEASED")
|
||||
|
||||
# Random objects that shouldn't be project visible
|
||||
other_port = db_utils.create_test_port(
|
||||
@ -447,7 +458,9 @@ class TestRBACProjectScoped(TestACLBase):
|
||||
'other_port_ident': other_port['uuid'],
|
||||
'owner_portgroup_ident': owner_pgroup['uuid'],
|
||||
'other_portgroup_ident': other_pgroup['uuid'],
|
||||
'driver_name': 'fake-driverz'})
|
||||
'driver_name': 'fake-driverz',
|
||||
'owner_allocation': fake_owner_allocation['uuid'],
|
||||
'lessee_allocation': fake_leased_allocation['uuid']})
|
||||
|
||||
@ddt.file_data('test_rbac_project_scoped.yaml')
|
||||
@ddt.unpack
|
||||
|
@ -1938,7 +1938,7 @@ allocations_allocation_id_get_member:
|
||||
path: '/v1/allocations/{allocation_ident}'
|
||||
method: get
|
||||
headers: *member_headers
|
||||
assert_status: 403
|
||||
assert_status: 404
|
||||
deprecated: true
|
||||
|
||||
allocations_allocation_id_get_observer:
|
||||
@ -1964,7 +1964,7 @@ allocations_allocation_id_patch_member:
|
||||
method: patch
|
||||
headers: *member_headers
|
||||
body: *allocation_patch
|
||||
assert_status: 403
|
||||
assert_status: 404
|
||||
deprecated: true
|
||||
|
||||
allocations_allocation_id_patch_observer:
|
||||
@ -1986,7 +1986,7 @@ allocations_allocation_id_delete_member:
|
||||
path: '/v1/allocations/{allocation_ident}'
|
||||
method: delete
|
||||
headers: *member_headers
|
||||
assert_status: 403
|
||||
assert_status: 404
|
||||
deprecated: true
|
||||
|
||||
allocations_allocation_id_delete_observer:
|
||||
@ -2008,7 +2008,7 @@ nodes_allocation_get_member:
|
||||
path: '/v1/nodes/{allocated_node_ident}/allocation'
|
||||
method: get
|
||||
headers: *member_headers
|
||||
assert_status: 403
|
||||
assert_status: 404
|
||||
deprecated: true
|
||||
|
||||
nodes_allocation_get_observer:
|
||||
@ -2029,7 +2029,7 @@ nodes_allocation_delete_member:
|
||||
path: '/v1/nodes/{allocated_node_ident}/allocation'
|
||||
method: delete
|
||||
headers: *member_headers
|
||||
assert_status: 403
|
||||
assert_status: 404
|
||||
deprecated: true
|
||||
|
||||
nodes_allocation_delete_observer:
|
||||
|
@ -2272,60 +2272,153 @@ third_party_admin_cannot_get_conductors:
|
||||
|
||||
# This is a system scoped endpoint, everything should fail in this section.
|
||||
|
||||
owner_reader_cannot_get_allocations:
|
||||
owner_reader_can_get_allocations:
|
||||
path: '/v1/allocations'
|
||||
method: get
|
||||
headers: *lessee_admin_headers
|
||||
assert_status: 403
|
||||
skip_reason: policy not implemented
|
||||
headers: *lessee_reader_headers
|
||||
assert_status: 200
|
||||
assert_list_length:
|
||||
allocations: 1
|
||||
|
||||
lessee_reader_cannot_get_allocations:
|
||||
lessee_reader_can_get_allocations:
|
||||
path: '/v1/allocations'
|
||||
method: get
|
||||
headers: *lessee_admin_headers
|
||||
assert_status: 403
|
||||
skip_reason: policy not implemented
|
||||
headers: *lessee_reader_headers
|
||||
assert_status: 200
|
||||
assert_list_length:
|
||||
allocations: 1
|
||||
|
||||
third_party_admin_cannot_get_allocations:
|
||||
owner_reader_can_get_their_allocation:
|
||||
path: '/v1/allocations/{owner_allocation}'
|
||||
method: get
|
||||
headers: *owner_reader_headers
|
||||
assert_status: 200
|
||||
assert_dict_contains:
|
||||
resource_class: CUSTOM_TEST
|
||||
|
||||
lessee_reader_can_get_their_allocation:
|
||||
path: '/v1/allocations/{lessee_allocation}'
|
||||
method: get
|
||||
headers: *lessee_reader_headers
|
||||
assert_status: 200
|
||||
assert_dict_contains:
|
||||
resource_class: CUSTOM_LEASED
|
||||
|
||||
owner_admin_can_delete_their_allocation:
|
||||
path: '/v1/allocations/{owner_allocation}'
|
||||
method: delete
|
||||
headers: *owner_admin_headers
|
||||
assert_status: 503
|
||||
|
||||
lessee_admin_can_delete_their_allocation:
|
||||
path: '/v1/allocations/{lessee_allocation}'
|
||||
method: delete
|
||||
headers: *lessee_admin_headers
|
||||
assert_status: 503
|
||||
|
||||
owner_member_cannot_delete_their_allocation:
|
||||
path: '/v1/allocations/{owner_allocation}'
|
||||
method: delete
|
||||
headers: *owner_member_headers
|
||||
assert_status: 403
|
||||
|
||||
lessee_member_cannot_delete_their_allocation:
|
||||
path: '/v1/allocations/{lessee_allocation}'
|
||||
method: delete
|
||||
headers: *lessee_member_headers
|
||||
assert_status: 403
|
||||
|
||||
owner_member_can_patch_allocation:
|
||||
path: '/v1/allocations/{owner_allocation}'
|
||||
method: patch
|
||||
headers: *owner_member_headers
|
||||
body: &allocation_patch
|
||||
- op: replace
|
||||
path: /extra
|
||||
value: {'test': 'testing'}
|
||||
assert_status: 200
|
||||
|
||||
lessee_member_can_patch_allocation:
|
||||
path: '/v1/allocations/{lessee_allocation}'
|
||||
method: patch
|
||||
headers: *lessee_member_headers
|
||||
body: *allocation_patch
|
||||
assert_status: 200
|
||||
|
||||
third_party_admin_can_get_allocations:
|
||||
path: '/v1/allocations'
|
||||
method: get
|
||||
headers: *third_party_admin_headers
|
||||
assert_status: 403
|
||||
skip_reason: policy not implemented
|
||||
assert_status: 200
|
||||
assert_list_length:
|
||||
allocations: 0
|
||||
|
||||
third_party_admin_cannot_create_allocation:
|
||||
third_party_admin_can_create_allocation:
|
||||
# This is distinctly different than most other behavior,
|
||||
# should be applied to filter this, however this is also handled
|
||||
# in the conductor, the only case where a user *should* be able
|
||||
# to pass a UUID directly in though is a special case which
|
||||
# should not be possible unless the user is the owner of the
|
||||
# owner or lessee of the node.
|
||||
path: '/v1/allocations'
|
||||
method: post
|
||||
headers: *third_party_admin_headers
|
||||
body: &allocation_body
|
||||
resource_class: CUSTOM_TEST
|
||||
assert_status: 403
|
||||
skip_reason: policy not implemented
|
||||
assert_status: 503
|
||||
|
||||
third_party_admin_cannot_create_allocation_with_owner_node:
|
||||
path: '/v1/allocations'
|
||||
method: post
|
||||
headers: *third_party_admin_headers
|
||||
body:
|
||||
resource_class: CUSTOM_TEST
|
||||
node: 1ab63b9e-66d7-4cd7-8618-dddd0f9f7881
|
||||
assert_status: 400
|
||||
|
||||
third_party_admin_cannot_create_allocation_with_candidates_not_owned:
|
||||
path: '/v1/allocations'
|
||||
method: post
|
||||
headers: *third_party_admin_headers
|
||||
body:
|
||||
resource_class: CUSTOM_TEST
|
||||
candidate_nodes:
|
||||
- 1ab63b9e-66d7-4cd7-8618-dddd0f9f7881
|
||||
- 38d5abed-c585-4fce-a57e-a2ffc2a2ec6f
|
||||
assert_status: 400
|
||||
|
||||
owner_admin_can_create_allocation_with_their_uuid:
|
||||
# NOTE(TheJulia): Owner/Lessee are equivelent in
|
||||
# this context, so testing only one is fine.
|
||||
path: '/v1/allocations'
|
||||
method: post
|
||||
headers: *owner_admin_headers
|
||||
body:
|
||||
resource_class: CUSTOM_TEST
|
||||
node: 1ab63b9e-66d7-4cd7-8618-dddd0f9f7881
|
||||
assert_status: 503
|
||||
|
||||
third_party_admin_cannot_read_an_allocation:
|
||||
path: '/v1/allocations/{allocation_ident}'
|
||||
path: '/v1/allocations/{lessee_allocation}'
|
||||
method: get
|
||||
headers: *third_party_admin_headers
|
||||
assert_status: 403
|
||||
skip_reason: policy not implemented
|
||||
assert_status: 404
|
||||
|
||||
third_party_admin_cannot_patch_an_allocation:
|
||||
path: '/v1/allocations/{allocation_ident}'
|
||||
path: '/v1/allocations/{owner_allocation}'
|
||||
method: patch
|
||||
headers: *third_party_admin_headers
|
||||
body:
|
||||
- op: replace
|
||||
path: /extra
|
||||
value: {'test': 'testing'}
|
||||
assert_status: 403
|
||||
skip_reason: policy not implemented
|
||||
assert_status: 404
|
||||
|
||||
third_party_admin_cannot_delete_an_allocation:
|
||||
path: '/v1/allocations/{allocation_ident}'
|
||||
path: '/v1/allocations/{owner_allocation}'
|
||||
method: delete
|
||||
headers: *third_party_admin_headers
|
||||
assert_status: 403
|
||||
skip_reason: policy not implemented
|
||||
assert_status: 404
|
||||
|
||||
# Allocations ( Node level) - https://docs.openstack.org/api-ref/baremetal/#node-allocation-allocations-nodes
|
||||
|
||||
@ -2334,42 +2427,36 @@ owner_reader_can_read_node_allocation:
|
||||
method: get
|
||||
headers: *owner_reader_headers
|
||||
assert_status: 200
|
||||
skip_reason: policy not implemented
|
||||
|
||||
lessee_reader_can_read_node_allocation:
|
||||
path: '/v1/nodes/{lessee_node_ident}/allocation'
|
||||
method: get
|
||||
headers: *lessee_reader_headers
|
||||
assert_status: 200
|
||||
skip_reason: policy not implemented
|
||||
|
||||
third_party_admin_cannot_read_node_allocation:
|
||||
path: '/v1/nodes/{owner_node_ident}/allocation'
|
||||
method: get
|
||||
headers: *third_party_admin_headers
|
||||
assert_status: 404
|
||||
skip_reason: policy not implemented
|
||||
|
||||
owner_admin_can_delete_allocation:
|
||||
path: '/v1/nodes/{owner_node_ident}/allocation'
|
||||
method: delete
|
||||
headers: *owner_admin_headers
|
||||
assert_status: 503
|
||||
skip_reason: policy not implemented
|
||||
|
||||
lessee_admin_cannot_delete_allocation:
|
||||
lessee_admin_not_delete_allocation:
|
||||
path: '/v1/nodes/{allocated_node_ident}/allocation'
|
||||
method: delete
|
||||
headers: *lessee_admin_headers
|
||||
assert_status: 403
|
||||
skip_reason: policy not implemented
|
||||
assert_status: 503
|
||||
|
||||
third_party_admin_cannot_delete_allocation:
|
||||
path: '/v1/nodes/{allocated_node_ident}/allocation'
|
||||
method: delete
|
||||
headers: *third_party_admin_headers
|
||||
assert_status: 403
|
||||
skip_reason: policy not implemented
|
||||
assert_status: 404
|
||||
|
||||
# Deploy Templates - https://docs.openstack.org/api-ref/baremetal/#deploy-templates-deploy-templates
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
---
|
||||
security:
|
||||
- |
|
||||
Ability to create an allocation has been restricted by a new policy rule
|
||||
``baremetal::allocation::create_pre_rbac`` which prevents creation of
|
||||
allocations by any project administrator when operating with the new
|
||||
Role Based Access Control model. The use and enforcement of this
|
||||
rule is disabled when ``[oslo_policy]enforce_new_defaults`` is set which
|
||||
also makes the population of a ``owner`` field for allocations to
|
||||
become automatically populated. Most deployments should not encounter any
|
||||
issues with this security change, and the policy rule will be removed
|
||||
when support for the legacy ``baremetal_admin`` custom role has been
|
||||
removed.
|
2
tox.ini
2
tox.ini
@ -127,7 +127,7 @@ filename = *.py,app.wsgi
|
||||
exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
||||
import-order-style = pep8
|
||||
application-import-names = ironic
|
||||
max-complexity=18
|
||||
max-complexity=19
|
||||
# [H106] Don't put vim configuration in source files.
|
||||
# [H203] Use assertIs(Not)None to check for None.
|
||||
# [H204] Use assert(Not)Equal to check for equality.
|
||||
|
Loading…
Reference in New Issue
Block a user