Add service role in Nova policy

RBAC community wide goal phase-2[1] is to add 'service'
role for the service APIs policy rule. This commit
defaults the service APIs to 'service' role. This way
service APIs will be allowed for service user only.

Tempest tests also modified to simulate the service-to-service
communication. Tempest tests send the user with service
role to nova API.
- https://review.opendev.org/c/openstack/tempest/+/892639>

Partial implement blueprint policy-service-role-default

[1] https://governance.openstack.org/tc/goals/selected/consistent-and-secure-rbac.html#phase-2

Change-Id: I1565ea163fa2c8212f71c9ba375654d2aab28330
Signed-off-by: Ghanshyam Maan <gmaan@ghanshyammann.com>
This commit is contained in:
Ghanshyam Maan
2025-08-16 04:11:31 +00:00
parent a7e5377da4
commit f914cb185c
34 changed files with 308 additions and 183 deletions

View File

@@ -54,6 +54,19 @@ the `API guide <https://docs.openstack.org/api-guide/compute/index.html>`_.
.. include:: os-server-external-events.inc
.. include:: server-topology.inc
=====================
Internal Service APIs
=====================
.. warning::
The below Nova APIs are meant to communicate to OpenStack services. Those
APIs are not supposed to be used by any users because they can make
deployment or resources in unwanted state.
.. include:: os-assisted-volume-snapshots.inc
.. include:: os-volume-attachments-swap.inc
.. include:: os-server-external-events.inc
===============
Deprecated APIs
===============

View File

@@ -0,0 +1,60 @@
.. -*- rst -*-
.. _os-volume-attachments-swap:
===============================================================================
Update ("swapping") Server volume attachments (servers, os-volume\_attachments)
===============================================================================
Update ("swapping") the server volume attachments which means swapping
the volume attached to the server.
Update(swapping) a volume attachment
====================================
.. rest_method:: PUT /servers/{server_id}/os-volume_attachments/{volume_id}
Update a volume attachment.
.. note:: This action only valid when the server is in ACTIVE, PAUSED and RESIZED state,
or a conflict(409) error will be returned.
.. Important::
When updating volumeId, this API **MUST** only be used
as part of a larger orchestrated volume
migration operation initiated in the block storage
service via the ``os-retype`` or ``os-migrate_volume``
volume actions. Direct usage of this API is not supported
and will be blocked by nova with a 409 conflict.
Furthermore, updating ``volumeId`` via this API is only
implemented by `certain compute drivers`_.
.. _certain compute drivers: https://docs.openstack.org/nova/latest/user/support-matrix.html#operation_swap_volume
Updating, or what is commonly referred to as "swapping", volume attachments
with volumes that have more than one read/write attachment, is not supported.
Normal response codes: 202
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404), conflict(409)
Request
-------
.. rest_parameters:: parameters.yaml
- server_id: server_id_path
- volume_id: volume_id_swap_src
- volumeAttachment: volumeAttachment_put
- volumeId: volumeId_swap
**Example Update a volume attachment: JSON request**
.. literalinclude:: ../../doc/api_samples/os-volume_attachments/update-volume-req.json
:language: javascript
Response
--------
No body is returned on successful request.

View File

@@ -182,31 +182,10 @@ Update a volume attachment
Update a volume attachment.
.. note:: This action only valid when the server is in ACTIVE, PAUSED and RESIZED state,
or a conflict(409) error will be returned.
.. Important::
When updating volumeId, this API **MUST** only be used
as part of a larger orchestrated volume
migration operation initiated in the block storage
service via the ``os-retype`` or ``os-migrate_volume``
volume actions. Direct usage of this API is not supported
and will be blocked by nova with a 409 conflict.
Furthermore, updating ``volumeId`` via this API is only
implemented by `certain compute drivers`_.
.. _certain compute drivers: https://docs.openstack.org/nova/latest/user/support-matrix.html#operation_swap_volume
Policy default role is 'rule:system_admin_or_owner', its scope is
[system, project], which allow project members or system admins to
change the fields of an attached volume of a server. Policy defaults
enable only users with the administrative role to change ``volumeId``
via this operation. Cloud providers can change these permissions
through the ``policy.json`` file.
Updating, or what is commonly referred to as "swapping", volume attachments
with volumes that have more than one read/write attachment, is not supported.
Policy default role is 'rule:admin_or_owner', its scope is [project], which
allow project members or admins to change the fields of an attached volume of
a server. Cloud providers can change these permissions through the
``policy.yaml`` file.
Normal response codes: 202
@@ -218,9 +197,9 @@ Request
.. rest_parameters:: parameters.yaml
- server_id: server_id_path
- volume_id: volume_id_swap_src
- volume_id: volume_id_path
- volumeAttachment: volumeAttachment_put
- volumeId: volumeId_swap
- volumeId: volumeId_update
- delete_on_termination: delete_on_termination_put_req
- device: attachment_device_put_req
- serverId: attachment_server_id_put_req

View File

@@ -7628,6 +7628,12 @@ volumeId_swap:
in: body
required: true
type: string
volumeId_update:
description: |
The UUID of the attached volume.
in: body
required: true
type: string
volumes:
description: |
The list of ``volume`` objects.

View File

@@ -276,6 +276,25 @@ With these new defaults, you can solve the problem of:
to provide access to project level user to perform operations within
their project only.
.. rubric:: ``service``
The ``service`` role is a special role in Keystone, which is used for the
internal service-to-service communication. It is assigned to service users
i.e. nova or neutron which model the OpenStack services. Nova defaults its
service-to-service APIs to require the ``service`` role so that they cannot
be used by any non-service users. Allowing access to service-to-service APIs
to non-service users can be destructive to resources and leave the deployment
in an invalid state. It's advisable to audit the ``policy.yaml`` files and
keystone users to make sure those APIs are not allowed to any non-service
users and the service role is not granted to human admin accounts.
.. note::
Make sure the configured nova service user in other services has the
``service`` role otherwise communication from the other services to
Nova will fail. For example, user configured as ``username`` option in
``neutron.conf`` file under ``[nova]`` section has the ``service`` role.
Nova supported scope & Roles
-----------------------------
@@ -308,6 +327,10 @@ overridden in the policy.yaml file but scope is not override-able.
Such policy rules are default to most of the read only APIs so that legacy
admin can continue to access those APIs.
#. SERVICE_ROLE (Internal): ``service`` role on service users with ``project``
scope. Such policy rules are default to the service-to-service APIs (The
APIs only meant to be called by the OpenStack services).
Backward Compatibility
----------------------

View File

@@ -41,11 +41,6 @@ class AssistedVolumeSnapshotsController(wsgi.Controller):
def create(self, req, body):
"""Creates a new snapshot."""
context = req.environ['nova.context']
# NOTE(gmann) We pass empty target to policy enforcement. This API
# is called by cinder which does not have correct project_id.
# By passing the empty target, we make sure that we do not check
# the requester project_id and allow users with
# allowed role to create snapshot.
context.can(avs_policies.POLICY_ROOT % 'create', target={})
snapshot = body['snapshot']
@@ -75,11 +70,6 @@ class AssistedVolumeSnapshotsController(wsgi.Controller):
def delete(self, req, id):
"""Delete a snapshot."""
context = req.environ['nova.context']
# NOTE(gmann) We pass empty target to policy enforcement. This API
# is called by cinder which does not have correct project_id.
# By passing the empty target, we make sure that we do not check
# the requester project_id and allow users with allowed role to
# delete snapshot.
context.can(avs_policies.POLICY_ROOT % 'delete', target={})
delete_metadata = {}

View File

@@ -80,11 +80,6 @@ class ServerExternalEventsController(wsgi.Controller):
def create(self, req, body):
"""Creates a new instance event."""
context = req.environ['nova.context']
# NOTE(gmann) We pass empty target to policy enforcement. This API
# is called by neutron which does not have correct project_id where
# server belongs to. By passing the empty target, we make sure that
# we do not check the requester project_id and allow users with
# allowed role to create external event.
context.can(see_policies.POLICY_ROOT % 'create', target={})
response_events = []

View File

@@ -329,11 +329,6 @@ class VolumeAttachmentController(wsgi.Controller):
# different from the 'id' in the url path, or only swap is allowed by
# the microversion, we should check the swap volume policy.
# otherwise, check the volume update policy.
# NOTE(gmann) We pass empty target to policy enforcement. This API
# is called by cinder which does not have correct project_id where
# server belongs to. By passing the empty target, we make sure that
# we do not check the requester project_id and allow users with
# allowed role to perform the swap volume.
if only_swap or id != volume_id:
context.can(va_policies.POLICY_ROOT % 'swap', target={})
else:

View File

@@ -24,14 +24,7 @@ POLICY_ROOT = 'os_compute_api:os-assisted-volume-snapshots:%s'
assisted_volume_snapshots_policies = [
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'create',
# TODO(gmann): This is internal API policy and called by
# cinder. Add 'service' role in this policy so that cinder
# can call it with user having 'service' role (not having
# correct project_id). That is for phase-2 of RBAC goal and until
# then, we keep it open for all admin in any project. We cannot
# default it to ADMIN which has the project_id in
# check_str and will fail if cinder call it with other project_id.
check_str=base.ADMIN,
check_str=base.SERVICE_ROLE,
description="Create an assisted volume snapshot",
operations=[
{
@@ -42,14 +35,7 @@ assisted_volume_snapshots_policies = [
scope_types=['project']),
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'delete',
# TODO(gmann): This is internal API policy and called by
# cinder. Add 'service' role in this policy so that cinder
# can call it with user having 'service' role (not having
# correct project_id). That is for phase-2 of RBAC goal and until
# then, we keep it open for all admin in any project. We cannot
# default it to ADMIN which has the project_id in
# check_str and will fail if cinder call it with other project_id.
check_str=base.ADMIN,
check_str=base.SERVICE_ROLE,
description="Delete an assisted volume snapshot",
operations=[
{

View File

@@ -41,6 +41,12 @@ ADMIN = 'rule:context_is_admin'
PROJECT_MEMBER = 'rule:project_manager_api'
PROJECT_MEMBER = 'rule:project_member_api'
PROJECT_READER = 'rule:project_reader_api'
# TODO(gmaan): Remove the admin role from the service rule in 2026.2. We are
# continue allowing admin to access the service APIs, otherwise it will break
# deployment where nova service users in other services are not assigned
# 'service' role. After one SLURP (2026.1), we can make service APIs only
# allowed for the 'service' role.
SERVICE_ROLE = 'rule:service_or_admin'
PROJECT_MANAGER_OR_ADMIN = 'rule:project_manager_or_admin'
PROJECT_MEMBER_OR_ADMIN = 'rule:project_member_or_admin'
PROJECT_READER_OR_ADMIN = 'rule:project_reader_or_admin'
@@ -106,6 +112,11 @@ rules = [
"role:reader and project_id:%(project_id)s",
"Default rule for Project level read only APIs.",
deprecated_rule=DEPRECATED_ADMIN_OR_OWNER_POLICY),
policy.RuleDefault(
"service_api",
"role:service",
"Default rule for service-to-service APIs.",
deprecated_rule=DEPRECATED_ADMIN_POLICY),
policy.RuleDefault(
"project_manager_or_admin",
"rule:project_manager_api or rule:context_is_admin",
@@ -120,7 +131,13 @@ rules = [
"project_reader_or_admin",
"rule:project_reader_api or rule:context_is_admin",
"Default rule for Project reader or admin APIs.",
deprecated_rule=DEPRECATED_ADMIN_OR_OWNER_POLICY)
deprecated_rule=DEPRECATED_ADMIN_OR_OWNER_POLICY),
policy.RuleDefault(
"service_or_admin",
"rule:service_api or rule:context_is_admin",
"Default rule for service or admin APIs.",
deprecated_rule=DEPRECATED_ADMIN_POLICY),
]

View File

@@ -24,15 +24,7 @@ POLICY_ROOT = 'os_compute_api:os-server-external-events:%s'
server_external_events_policies = [
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'create',
# TODO(gmann): This is internal API policy and supposed to be called
# by neutron, cinder, ironic, and cyborg (may be other openstack
# services in future). Add 'service' role in this policy so that
# neutron can call it with user having 'service' role (not having
# server's project_id). That is for phase-2 of RBAC goal and until
# then, we keep it open for all admin in any project. We cannot
# default it to ADMIN which has the project_id in
# check_str and will fail if neutron call it with other project_id.
check_str=base.ADMIN,
check_str=base.SERVICE_ROLE,
description="Create one or more external events",
operations=[
{

View File

@@ -73,14 +73,7 @@ always superset of this policy permission.
scope_types=['project']),
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'swap',
# TODO(gmann): This is internal API policy and supposed to be called
# only by cinder. Add 'service' role in this policy so that cinder
# can call it with user having 'service' role (not having server's
# project_id). That is for phase-2 of RBAC goal and until then,
# we keep it open for all admin in any project. We cannot default it to
# ADMIN which has the project_id in check_str and will fail
# if cinder call it with other project_id.
check_str=base.ADMIN,
check_str=base.SERVICE_ROLE,
description="Update a volume attachment with a different volumeId",
operations=[
{

View File

@@ -132,6 +132,12 @@ class BasePolicyTest(test.TestCase):
project_id=self.project_id_other,
roles=['reader'])
# service user
self.service_context = nova_context.RequestContext(
user_id="service_user",
project_id="service_user_project_id",
roles=['service'])
self.all_contexts = set([
self.legacy_admin_context, self.system_admin_context,
self.system_member_context, self.system_reader_context,
@@ -140,7 +146,8 @@ class BasePolicyTest(test.TestCase):
self.project_member_context, self.project_reader_context,
self.other_project_manager_context,
self.other_project_member_context,
self.project_foo_context, self.other_project_reader_context
self.project_foo_context, self.other_project_reader_context,
self.service_context
])
# All the project contexts for easy access.
@@ -238,6 +245,7 @@ class BasePolicyTest(test.TestCase):
"rule:project_member_api or rule:context_is_admin",
"project_reader_or_admin":
"rule:project_reader_api or rule:context_is_admin",
"service_api": "role:service",
})
self.policy.set_rules(self.rules_without_deprecation,
overwrite=False)

View File

@@ -34,11 +34,10 @@ class AssistedVolumeSnapshotPolicyTest(base.BasePolicyTest):
self.controller = snapshots.AssistedVolumeSnapshotsController()
self.req = fakes.HTTPRequest.blank('')
# By default, legacy rule are enable and scope check is disabled.
# system admin, legacy admin, and project admin is able to
# take volume snapshot.
# admin and service user is able to manage volume snapshot.
self.project_admin_authorized_contexts = [
self.legacy_admin_context, self.system_admin_context,
self.project_admin_context]
self.project_admin_context, self.service_context]
@mock.patch('nova.compute.api.API.volume_snapshot_create')
def test_assisted_create_policy(self, mock_create):
@@ -98,7 +97,8 @@ class AssistedSnapshotScopeTypePolicyTest(AssistedVolumeSnapshotPolicyTest):
# With scope check enabled, system admin is not able to
# take volume snapshot.
self.project_admin_authorized_contexts = [
self.legacy_admin_context, self.project_admin_context]
self.legacy_admin_context, self.project_admin_context,
self.service_context]
class AssistedSnapshotScopeTypeNoLegacyPolicyTest(

View File

@@ -84,7 +84,8 @@ class AvailabilityZoneScopeTypePolicyTest(AvailabilityZonePolicyTest):
# able to get AZ with host information.
self.project_admin_authorized_contexts = [self.legacy_admin_context,
self.project_admin_context]
self.project_authorized_contexts = self.all_project_contexts
self.project_authorized_contexts = (self.all_project_contexts | set([
self.service_context]))
class AZScopeTypeNoLegacyPolicyTest(AvailabilityZoneScopeTypePolicyTest):

View File

@@ -30,17 +30,7 @@ class ExtensionsPolicyTest(base.BasePolicyTest):
self.req = fakes.HTTPRequest.blank('')
# Check that everyone is able to get extension info.
self.everyone_authorized_contexts = [
self.legacy_admin_context, self.system_admin_context,
self.project_admin_context, self.project_manager_context,
self.project_member_context, self.project_reader_context,
self.project_foo_context,
self.other_project_reader_context,
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
]
self.everyone_authorized_contexts = self.all_contexts
self.everyone_unauthorized_contexts = []
def test_list_extensions_policy(self):
@@ -80,7 +70,8 @@ class ExtensionsScopeTypePolicyTest(ExtensionsPolicyTest):
self.project_foo_context,
self.other_project_manager_context,
self.other_project_reader_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
self.everyone_unauthorized_contexts = [
self.system_admin_context, self.system_member_context,

View File

@@ -126,7 +126,8 @@ class FlavorAccessScopeTypePolicyTest(FlavorAccessPolicyTest):
self.admin_authorized_contexts = [
self.legacy_admin_context,
self.project_admin_context]
self.admin_index_authorized_contexts = self.all_project_contexts
self.admin_index_authorized_contexts = (self.all_project_contexts |
set([self.service_context]))
class FlavorAccessScopeTypeNoLegacyPolicyTest(FlavorAccessScopeTypePolicyTest):

View File

@@ -51,13 +51,15 @@ class FlavorExtraSpecsPolicyTest(base.BasePolicyTest):
# In the base/legacy case, all project and system contexts are
# authorized in the "anyone" case.
self.all_authorized_contexts = (self.all_project_contexts |
self.all_system_contexts)
self.all_system_contexts |
set([self.service_context]))
# In the base/legacy case, all project and system contexts are
# authorized in the case of things that distinguish between
# scopes, since scope checking is disabled.
self.all_project_authorized_contexts = (self.all_project_contexts |
self.all_system_contexts)
self.all_system_contexts |
set([self.service_context]))
# In the base/legacy case, any admin is an admin.
self.admin_authorized_contexts = set([self.project_admin_context,
@@ -211,8 +213,10 @@ class FlavorExtraSpecsScopeTypePolicyTest(FlavorExtraSpecsPolicyTest):
self.flags(enforce_scope=True, group="oslo_policy")
# Only project users are authorized
self.reduce_set('all_project_authorized', self.all_project_contexts)
self.reduce_set('all_authorized', self.all_project_contexts)
self.reduce_set('all_project_authorized',
self.all_project_contexts | set([self.service_context]))
self.reduce_set('all_authorized',
self.all_project_contexts | set([self.service_context]))
# Only admins can do admin things
self.admin_authorized_contexts = [self.legacy_admin_context,
@@ -254,9 +258,10 @@ class FlavorExtraSpecsNoLegacyPolicyTest(FlavorExtraSpecsScopeTypePolicyTest):
# contexts stay separate.
self.reduce_set(
'all_project_authorized',
self.all_project_contexts - set([self.project_foo_context]))
self.all_project_contexts - set([self.project_foo_context,
self.service_context]))
everything_but_foo_and_system = (
self.all_contexts - set([
self.project_foo_context,
self.project_foo_context, self.service_context,
]) - self.all_system_contexts)
self.reduce_set('all_authorized', everything_but_foo_and_system)

View File

@@ -32,16 +32,7 @@ class FloatingIPPoolsPolicyTest(base.BasePolicyTest):
self.req = fakes.HTTPRequest.blank('')
# Check that everyone is able to list FIP pools.
self.everyone_authorized_contexts = set([
self.legacy_admin_context, self.system_admin_context,
self.project_admin_context, self.project_manager_context,
self.project_member_context, self.project_reader_context,
self.project_foo_context,
self.other_project_manager_context,
self.other_project_reader_context,
self.other_project_member_context,
self.system_member_context, self.system_reader_context,
self.system_foo_context])
self.everyone_authorized_contexts = self.all_contexts
self.everyone_unauthorized_contexts = set([])
@mock.patch('nova.network.neutron.API.get_floating_ip_pools')
@@ -68,7 +59,8 @@ class FloatingIPPoolsScopeTypePolicyTest(FloatingIPPoolsPolicyTest):
super(FloatingIPPoolsScopeTypePolicyTest, self).setUp()
self.flags(enforce_scope=True, group="oslo_policy")
self.reduce_set('everyone_authorized', self.all_project_contexts)
self.reduce_set('everyone_authorized', self.all_project_contexts |
set([self.service_context]))
self.everyone_unauthorized_contexts = (
self.all_contexts - self.everyone_authorized_contexts)

View File

@@ -64,7 +64,8 @@ class FloatingIPPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
self.project_reader_authorized_contexts = [
self.legacy_admin_context, self.system_admin_context,
@@ -75,7 +76,8 @@ class FloatingIPPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
# With legacy rule and no scope checks, all admin, project members
# project reader or other project role(because legacy rule allow server
@@ -218,7 +220,8 @@ class FloatingIPScopeTypePolicyTest(FloatingIPPolicyTest):
self.project_reader_context, self.project_foo_context,
self.other_project_manager_context,
self.other_project_reader_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
self.project_reader_authorized_contexts = [
self.legacy_admin_context, self.project_admin_context,
@@ -226,7 +229,8 @@ class FloatingIPScopeTypePolicyTest(FloatingIPPolicyTest):
self.project_reader_context, self.project_foo_context,
self.other_project_manager_context,
self.other_project_reader_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]

View File

@@ -53,17 +53,7 @@ class KeypairsPolicyTest(base.BasePolicyTest):
# Check that everyone is able to create, delete and get
# their keypairs.
self.everyone_authorized_contexts = set([
self.legacy_admin_context, self.system_admin_context,
self.project_admin_context,
self.system_member_context, self.system_reader_context,
self.system_foo_context, self.project_manager_context,
self.project_member_context, self.project_reader_context,
self.project_foo_context,
self.other_project_manager_context,
self.other_project_member_context,
self.other_project_reader_context,
])
self.everyone_authorized_contexts = self.all_contexts
# Check that admin is able to create, delete and get
# other users keypairs.
@@ -177,7 +167,8 @@ class KeypairsScopeTypePolicyTest(KeypairsPolicyTest):
self.flags(enforce_scope=True, group="oslo_policy")
# With scope checking, only project-scoped users are allowed
self.reduce_set('everyone_authorized', self.all_project_contexts)
self.reduce_set('everyone_authorized', self.all_project_contexts |
set([self.service_context]))
self.admin_authorized_contexts = [
self.legacy_admin_context,
self.project_admin_context]

View File

@@ -131,7 +131,8 @@ class LimitsScopeTypePolicyTest(LimitsPolicyTest):
self.project_reader_context,
self.other_project_manager_context,
self.other_project_member_context,
self.project_foo_context, self.other_project_reader_context
self.project_foo_context, self.other_project_reader_context,
self.service_context,
]
@@ -157,5 +158,6 @@ class LimitsScopeTypeNoLegacyPolicyTest(LimitsScopeTypePolicyTest):
self.project_reader_context,
self.other_project_manager_context,
self.other_project_member_context,
self.project_foo_context, self.other_project_reader_context
self.project_foo_context, self.other_project_reader_context,
self.service_context,
]

View File

@@ -48,7 +48,8 @@ class NetworksPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
@mock.patch('nova.network.neutron.API.get_all')
@@ -116,7 +117,8 @@ class NetworksScopeTypePolicyTest(NetworksPolicyTest):
self.project_reader_context, self.project_foo_context,
self.other_project_manager_context,
self.other_project_reader_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]

View File

@@ -49,7 +49,8 @@ class QuotaSetsPolicyTest(base.BasePolicyTest):
self.project_foo_context,
self.other_project_manager_context,
self.other_project_member_context,
self.other_project_reader_context])
self.other_project_reader_context,
self.service_context])
# Everyone is able to get the default quota
self.everyone_authorized_contexts = set([
self.legacy_admin_context, self.system_admin_context,
@@ -60,7 +61,7 @@ class QuotaSetsPolicyTest(base.BasePolicyTest):
self.project_foo_context,
self.other_project_manager_context,
self.other_project_member_context,
self.other_project_reader_context])
self.other_project_reader_context, self.service_context])
@mock.patch('nova.quota.QUOTAS.get_project_quotas')
@mock.patch('nova.quota.QUOTAS.get_settable_quotas')
@@ -185,8 +186,10 @@ class QuotaSetsScopeTypePolicyTest(QuotaSetsPolicyTest):
self.legacy_admin_context,
self.project_admin_context]))
self.reduce_set('project_reader_authorized',
self.all_project_contexts)
self.everyone_authorized_contexts = self.all_project_contexts
self.all_project_contexts | set([
self.service_context]))
self.everyone_authorized_contexts = (self.all_project_contexts | set([
self.service_context]))
class QuotaSetsScopeTypeNoLegacyPolicyTest(QuotaSetsScopeTypePolicyTest):

View File

@@ -152,7 +152,8 @@ class SecurityGroupsPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
self.project_reader_authorized_contexts = [
self.legacy_admin_context, self.system_admin_context,
@@ -163,7 +164,8 @@ class SecurityGroupsPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
@mock.patch('nova.network.security_group_api.list')
@@ -304,7 +306,8 @@ class SecurityGroupsScopeTypePolicyTest(SecurityGroupsPolicyTest):
self.project_reader_context,
self.project_foo_context, self.other_project_reader_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
self.project_reader_authorized_contexts = [
self.legacy_admin_context, self.project_admin_context,
@@ -312,7 +315,8 @@ class SecurityGroupsScopeTypePolicyTest(SecurityGroupsPolicyTest):
self.project_reader_context,
self.project_foo_context, self.other_project_reader_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]

View File

@@ -34,11 +34,11 @@ class ServerExternalEventsPolicyTest(base.BasePolicyTest):
self.controller = ev.ServerExternalEventsController()
self.req = fakes.HTTPRequest.blank('')
# With legacy rule and no scope checks, all admin can
# create the server external events.
# With legacy rule and no scope checks, all admin and service user
# can create the server external events.
self.project_admin_authorized_contexts = [
self.legacy_admin_context, self.system_admin_context,
self.project_admin_context
self.project_admin_context, self.service_context
]
@mock.patch('nova.compute.api.API.external_instance_event')
@@ -82,7 +82,8 @@ class ServerExternalEventsScopeTypePolicyTest(ServerExternalEventsPolicyTest):
# With scope checks, system admin is not allowed.
self.project_admin_authorized_contexts = [
self.legacy_admin_context, self.project_admin_context]
self.legacy_admin_context, self.project_admin_context,
self.service_context]
class ServerExternalEventsScopeTypeNoLegacyPolicyTest(

View File

@@ -83,7 +83,8 @@ class ServerGroupPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
# With legacy rule, anyone can create SG.
@@ -222,19 +223,13 @@ class ServerGroupScopeTypePolicyTest(ServerGroupPolicyTest):
self.project_reader_context, self.project_foo_context,
self.other_project_reader_context,
self.other_project_member_context,
self.other_project_manager_context]
self.other_project_manager_context,
self.service_context]
self.project_admin_authorized_contexts = [
self.legacy_admin_context, self.project_admin_context]
self.everyone_authorized_contexts = [
self.legacy_admin_context, self.project_admin_context,
self.project_manager_context, self.project_member_context,
self.project_reader_context, self.project_foo_context,
self.other_project_manager_context,
self.other_project_reader_context,
self.other_project_member_context
]
self.everyone_authorized_contexts = (
self.project_create_authorized_contexts)
class ServerGroupScopeTypeNoLegacyPolicyTest(ServerGroupScopeTypePolicyTest):

View File

@@ -1367,7 +1367,8 @@ class ServersNoLegacyNoScopeTest(ServersPolicyTest):
# see everything in their project.
self.reduce_set('everyone_authorized',
self.all_contexts - set([self.project_foo_context,
self.system_foo_context]))
self.system_foo_context,
self.service_context]))
# Disabling legacy support means readers and random roles lose
# power to create things on their own projects. Note that
@@ -1381,7 +1382,8 @@ class ServersNoLegacyNoScopeTest(ServersPolicyTest):
self.system_foo_context,
self.project_reader_context,
self.project_foo_context,
self.other_project_reader_context]))
self.other_project_reader_context,
self.service_context]))
class ServersScopeTypePolicyTest(ServersPolicyTest):
@@ -1428,7 +1430,8 @@ class ServersScopeTypePolicyTest(ServersPolicyTest):
# With scope checking enabled, system users no longer have
# project access, even to create their own resources.
self.reduce_set('project_member_authorized', self.all_project_contexts)
self.reduce_set('project_member_authorized',
self.all_project_contexts | set([self.service_context]))
# With scope checking enabled, system admin is no longer an
# admin of project resources.

View File

@@ -61,7 +61,8 @@ class SnapshotsPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
self.project_reader_authorized_contexts = [
self.legacy_admin_context, self.system_admin_context,
@@ -72,7 +73,8 @@ class SnapshotsPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
@mock.patch('nova.volume.cinder.API.get_all_snapshots')
@@ -191,7 +193,8 @@ class SnapshotsScopeTypePolicyTest(SnapshotsPolicyTest):
self.project_reader_context, self.project_foo_context,
self.other_project_reader_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
self.project_reader_authorized_contexts = [
self.legacy_admin_context, self.project_admin_context,
@@ -200,7 +203,8 @@ class SnapshotsScopeTypePolicyTest(SnapshotsPolicyTest):
self.project_reader_context, self.project_foo_context,
self.other_project_reader_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]

View File

@@ -48,7 +48,8 @@ class TenantNetworksPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
@mock.patch('nova.network.neutron.API.get_all')
@@ -113,7 +114,7 @@ class TenantNetworksScopeTypePolicyTest(TenantNetworksPolicyTest):
self.project_reader_context, self.project_foo_context,
self.other_project_reader_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context, self.service_context
]

View File

@@ -119,11 +119,11 @@ class VolumeAttachPolicyTest(base.BasePolicyTest):
self.project_member_authorized_contexts)
# By default, legacy rule are enable and scope check is disabled.
# system admin, legacy admin, and project admin is able to update
# volume attachment with a different volumeId.
# system admin, legacy admin, project admin, and service user is able
# to update volume attachment with a different volumeId.
self.project_admin_authorized_contexts = [
self.legacy_admin_context, self.system_admin_context,
self.project_admin_context]
self.project_admin_context, self.service_context]
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid')
def test_index_volume_attach_policy(self, mock_get_instance):
@@ -256,7 +256,8 @@ class VolumeAttachScopeTypePolicyTest(VolumeAttachPolicyTest):
self.project_m_r_or_admin_with_scope_and_legacy)
self.project_admin_authorized_contexts = [
self.legacy_admin_context, self.project_admin_context]
self.legacy_admin_context, self.project_admin_context,
self.service_context]
class VolumeAttachScopeTypeNoLegacyPolicyTest(VolumeAttachScopeTypePolicyTest):

View File

@@ -51,7 +51,8 @@ class VolumesPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
self.project_reader_authorized_contexts = [
self.legacy_admin_context, self.system_admin_context,
@@ -62,7 +63,8 @@ class VolumesPolicyTest(base.BasePolicyTest):
self.system_member_context, self.system_reader_context,
self.system_foo_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context,
self.service_context
]
@mock.patch('nova.volume.cinder.API.get_all')
@@ -204,7 +206,7 @@ class VolumesScopeTypePolicyTest(VolumesPolicyTest):
self.project_reader_context, self.project_foo_context,
self.other_project_reader_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context, self.service_context
]
self.project_reader_authorized_contexts = [
self.legacy_admin_context, self.project_admin_context,
@@ -213,7 +215,7 @@ class VolumesScopeTypePolicyTest(VolumesPolicyTest):
self.project_reader_context, self.project_foo_context,
self.other_project_reader_context,
self.other_project_manager_context,
self.other_project_member_context
self.other_project_member_context, self.service_context
]

View File

@@ -308,6 +308,11 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
self.admin_context = context.RequestContext(
'fake', 'fake', True, roles=[
'admin', 'manager', 'member', 'reader'])
self.service_context = context.RequestContext(
user_id="service_user",
project_id="service_user_project_id",
roles=['service'])
self.target = {}
self.fake_policy = jsonutils.loads(fake_policy.policy_data)
@@ -359,12 +364,8 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
"os_compute_api:os-shelve:shelve_offload",
"os_compute_api:os-shelve:unshelve_to_host",
"os_compute_api:os-availability-zone:detail",
"os_compute_api:os-assisted-volume-snapshots:create",
"os_compute_api:os-assisted-volume-snapshots:delete",
"os_compute_api:os-console-auth-tokens",
"os_compute_api:os-quota-class-sets:update",
"os_compute_api:os-server-external-events:create",
"os_compute_api:os-volumes-attachments:swap",
"os_compute_api:servers:create:zero_disk_flavor",
"os_compute_api:os-baremetal-nodes:list",
"os_compute_api:os-baremetal-nodes:show",
@@ -525,6 +526,13 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
servers_policy.CROSS_CELL_RESIZE,
)
self.service_rules = (
"os_compute_api:os-assisted-volume-snapshots:create",
"os_compute_api:os-assisted-volume-snapshots:delete",
"os_compute_api:os-server-external-events:create",
"os_compute_api:os-volumes-attachments:swap",
)
def test_all_rules_in_sample_file(self):
special_rules = ["context_is_admin", "admin_or_owner", "default"]
for (name, rule) in self.fake_policy.items():
@@ -556,6 +564,18 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
self.admin_context, rule, self.target)
def test_service_only_rules(self):
for rule in self.service_rules:
self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
self.non_admin_context, rule,
{'project_id': 'fake', 'user_id': 'fake'})
# TODO(gmaan): For backward compatibility, we are allowing admin
# user to access service only rules, but once we remove that
# access, we need to assert here that the admin cannot access the
# service only rules.
policy.authorize(self.admin_context, rule)
policy.authorize(self.service_context, rule)
def test_rule_missing(self):
rules = policy.get_rules()
# eliqiao os_compute_api:os-quota-class-sets:show requires
@@ -567,9 +587,11 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
'project_member_api', 'project_reader_api',
'project_manager_or_admin',
'project_member_or_admin',
'project_reader_or_admin')
'project_reader_or_admin', 'service_api',
'service_or_admin')
result = set(rules.keys()) - set(self.admin_only_rules +
self.admin_or_owner_rules +
self.allow_all_rules +
self.allow_nobody_rules + special_rules)
self.allow_nobody_rules + special_rules +
self.service_rules)
self.assertEqual(set([]), result)

View File

@@ -0,0 +1,43 @@
---
features:
- |
A few of the Nova APIs are meant only for use by other Openstack services.
Those APIs are not supposed to be used by any non-service users (even
admins) because they can make deployment or resources in unwanted state.
To restrict the usage of those APIs by users, Nova now defaults those APIs
to a policy rule of the ``service`` role. This will make sure they are
allowed to be used by the OpenStack services only.
upgrade:
- |
Nova changed the default access for the service-to-service APIs which are
meant to be used by the OpenStack services only and not by any users.
The below service-to-service APIs access default to the ``service`` role:
* os_compute_api:os-assisted-volume-snapshots:create
* os_compute_api:os-assisted-volume-snapshots:delete
* os_compute_api:os-server-external-events:create
* os_compute_api:os-volumes-attachments:swap
Make sure the configured nova service user in other services has the
``service`` role otherwise communication from the other services to
Nova will fail. For example, user configured as ``username`` option in
``neutron.conf`` file under ``[nova]`` section has the ``service``
role.
If you are allowing these APIs to be accessed by admin or non-admin users
then it is highly recommended to remove that permission and make sure
those APIs are not accessible by any non-service users.
For backward compatibility, Nova continue allow ``admin`` role token to
access service APIs but in future release, ``admin`` access will be
removed.
deprecations:
- |
The below service-to-service APIs policy rule default value
``role:admin or role:service`` is deprecated and will be changed to
``role:service`` in future release:
* os_compute_api:os-assisted-volume-snapshots:create
* os_compute_api:os-assisted-volume-snapshots:delete
* os_compute_api:os-server-external-events:create
* os_compute_api:os-volumes-attachments:swap