diff --git a/api-ref/source/index.rst b/api-ref/source/index.rst index 765c21a339..9a1d9ee714 100644 --- a/api-ref/source/index.rst +++ b/api-ref/source/index.rst @@ -54,6 +54,19 @@ the `API guide `_. .. 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 =============== diff --git a/api-ref/source/os-volume-attachments-swap.inc b/api-ref/source/os-volume-attachments-swap.inc new file mode 100644 index 0000000000..5cca5c9a2a --- /dev/null +++ b/api-ref/source/os-volume-attachments-swap.inc @@ -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. diff --git a/api-ref/source/os-volume-attachments.inc b/api-ref/source/os-volume-attachments.inc index c20e8121f4..359d4d16e8 100644 --- a/api-ref/source/os-volume-attachments.inc +++ b/api-ref/source/os-volume-attachments.inc @@ -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 diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index d9db54680e..c1cf611fa7 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -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. diff --git a/doc/source/configuration/policy-concepts.rst b/doc/source/configuration/policy-concepts.rst index f7cbce4f6e..eeb8cc6a6f 100644 --- a/doc/source/configuration/policy-concepts.rst +++ b/doc/source/configuration/policy-concepts.rst @@ -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 ---------------------- diff --git a/nova/api/openstack/compute/assisted_volume_snapshots.py b/nova/api/openstack/compute/assisted_volume_snapshots.py index d60fd10ee0..9d57220c3d 100644 --- a/nova/api/openstack/compute/assisted_volume_snapshots.py +++ b/nova/api/openstack/compute/assisted_volume_snapshots.py @@ -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 = {} diff --git a/nova/api/openstack/compute/server_external_events.py b/nova/api/openstack/compute/server_external_events.py index 53d5e1d907..0328ec08fe 100644 --- a/nova/api/openstack/compute/server_external_events.py +++ b/nova/api/openstack/compute/server_external_events.py @@ -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 = [] diff --git a/nova/api/openstack/compute/volume_attachments.py b/nova/api/openstack/compute/volume_attachments.py index 212ecc052b..aa72ea046f 100644 --- a/nova/api/openstack/compute/volume_attachments.py +++ b/nova/api/openstack/compute/volume_attachments.py @@ -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: diff --git a/nova/policies/assisted_volume_snapshots.py b/nova/policies/assisted_volume_snapshots.py index 98a67a8e37..f3e112b75f 100644 --- a/nova/policies/assisted_volume_snapshots.py +++ b/nova/policies/assisted_volume_snapshots.py @@ -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=[ { diff --git a/nova/policies/base.py b/nova/policies/base.py index 0d4c3ac658..70673e555b 100644 --- a/nova/policies/base.py +++ b/nova/policies/base.py @@ -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), + ] diff --git a/nova/policies/server_external_events.py b/nova/policies/server_external_events.py index 56034d0186..29d5371abd 100644 --- a/nova/policies/server_external_events.py +++ b/nova/policies/server_external_events.py @@ -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=[ { diff --git a/nova/policies/volumes_attachments.py b/nova/policies/volumes_attachments.py index 68a1694c59..071a5d6eb1 100644 --- a/nova/policies/volumes_attachments.py +++ b/nova/policies/volumes_attachments.py @@ -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=[ { diff --git a/nova/tests/unit/policies/base.py b/nova/tests/unit/policies/base.py index 2d1c0d68e7..8d4aa03142 100644 --- a/nova/tests/unit/policies/base.py +++ b/nova/tests/unit/policies/base.py @@ -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) diff --git a/nova/tests/unit/policies/test_assisted_volume_snapshots.py b/nova/tests/unit/policies/test_assisted_volume_snapshots.py index 1474702e7b..7afd69f644 100644 --- a/nova/tests/unit/policies/test_assisted_volume_snapshots.py +++ b/nova/tests/unit/policies/test_assisted_volume_snapshots.py @@ -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( diff --git a/nova/tests/unit/policies/test_availability_zone.py b/nova/tests/unit/policies/test_availability_zone.py index 1852f8444c..4a24f5cea9 100644 --- a/nova/tests/unit/policies/test_availability_zone.py +++ b/nova/tests/unit/policies/test_availability_zone.py @@ -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): diff --git a/nova/tests/unit/policies/test_extensions.py b/nova/tests/unit/policies/test_extensions.py index 565e410acf..c345edc3fb 100644 --- a/nova/tests/unit/policies/test_extensions.py +++ b/nova/tests/unit/policies/test_extensions.py @@ -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, diff --git a/nova/tests/unit/policies/test_flavor_access.py b/nova/tests/unit/policies/test_flavor_access.py index cfdbbd2470..e7f967f0fc 100644 --- a/nova/tests/unit/policies/test_flavor_access.py +++ b/nova/tests/unit/policies/test_flavor_access.py @@ -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): diff --git a/nova/tests/unit/policies/test_flavor_extra_specs.py b/nova/tests/unit/policies/test_flavor_extra_specs.py index 7c3efccdc3..514629bc92 100644 --- a/nova/tests/unit/policies/test_flavor_extra_specs.py +++ b/nova/tests/unit/policies/test_flavor_extra_specs.py @@ -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) diff --git a/nova/tests/unit/policies/test_floating_ip_pools.py b/nova/tests/unit/policies/test_floating_ip_pools.py index 5360a86eaf..e6c8083ed4 100644 --- a/nova/tests/unit/policies/test_floating_ip_pools.py +++ b/nova/tests/unit/policies/test_floating_ip_pools.py @@ -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) diff --git a/nova/tests/unit/policies/test_floating_ips.py b/nova/tests/unit/policies/test_floating_ips.py index b90fa37d84..1ed4ceaa18 100644 --- a/nova/tests/unit/policies/test_floating_ips.py +++ b/nova/tests/unit/policies/test_floating_ips.py @@ -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 ] diff --git a/nova/tests/unit/policies/test_keypairs.py b/nova/tests/unit/policies/test_keypairs.py index c3fb201768..48d054bbb5 100644 --- a/nova/tests/unit/policies/test_keypairs.py +++ b/nova/tests/unit/policies/test_keypairs.py @@ -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] diff --git a/nova/tests/unit/policies/test_limits.py b/nova/tests/unit/policies/test_limits.py index 3905dc03d6..c7951a552a 100644 --- a/nova/tests/unit/policies/test_limits.py +++ b/nova/tests/unit/policies/test_limits.py @@ -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, ] diff --git a/nova/tests/unit/policies/test_networks.py b/nova/tests/unit/policies/test_networks.py index cf3181dc8a..c64d46f9ff 100644 --- a/nova/tests/unit/policies/test_networks.py +++ b/nova/tests/unit/policies/test_networks.py @@ -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 ] diff --git a/nova/tests/unit/policies/test_quota_sets.py b/nova/tests/unit/policies/test_quota_sets.py index d950f523f8..84a3cf0b1b 100644 --- a/nova/tests/unit/policies/test_quota_sets.py +++ b/nova/tests/unit/policies/test_quota_sets.py @@ -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): diff --git a/nova/tests/unit/policies/test_security_groups.py b/nova/tests/unit/policies/test_security_groups.py index 086daf4f69..990794f2fd 100644 --- a/nova/tests/unit/policies/test_security_groups.py +++ b/nova/tests/unit/policies/test_security_groups.py @@ -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 ] diff --git a/nova/tests/unit/policies/test_server_external_events.py b/nova/tests/unit/policies/test_server_external_events.py index 401b55325f..a2441d2fe7 100644 --- a/nova/tests/unit/policies/test_server_external_events.py +++ b/nova/tests/unit/policies/test_server_external_events.py @@ -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( diff --git a/nova/tests/unit/policies/test_server_groups.py b/nova/tests/unit/policies/test_server_groups.py index b712dd6d4d..0c55f3336b 100644 --- a/nova/tests/unit/policies/test_server_groups.py +++ b/nova/tests/unit/policies/test_server_groups.py @@ -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): diff --git a/nova/tests/unit/policies/test_servers.py b/nova/tests/unit/policies/test_servers.py index 1167a60935..eb1bc8d847 100644 --- a/nova/tests/unit/policies/test_servers.py +++ b/nova/tests/unit/policies/test_servers.py @@ -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. diff --git a/nova/tests/unit/policies/test_snapshots.py b/nova/tests/unit/policies/test_snapshots.py index bf6469455d..9e61b0f696 100644 --- a/nova/tests/unit/policies/test_snapshots.py +++ b/nova/tests/unit/policies/test_snapshots.py @@ -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 ] diff --git a/nova/tests/unit/policies/test_tenant_networks.py b/nova/tests/unit/policies/test_tenant_networks.py index c98b7a39b1..56ce126b1a 100644 --- a/nova/tests/unit/policies/test_tenant_networks.py +++ b/nova/tests/unit/policies/test_tenant_networks.py @@ -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 ] diff --git a/nova/tests/unit/policies/test_volume_attachments.py b/nova/tests/unit/policies/test_volume_attachments.py index 38d69e4558..589d7aa9ba 100644 --- a/nova/tests/unit/policies/test_volume_attachments.py +++ b/nova/tests/unit/policies/test_volume_attachments.py @@ -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): diff --git a/nova/tests/unit/policies/test_volumes.py b/nova/tests/unit/policies/test_volumes.py index fccf3032e5..32d9c7ad1b 100644 --- a/nova/tests/unit/policies/test_volumes.py +++ b/nova/tests/unit/policies/test_volumes.py @@ -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 ] diff --git a/nova/tests/unit/test_policy.py b/nova/tests/unit/test_policy.py index b74b969403..03873bfce0 100644 --- a/nova/tests/unit/test_policy.py +++ b/nova/tests/unit/test_policy.py @@ -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) diff --git a/releasenotes/notes/add-policy-service-role-eaa391e30431a9d6.yaml b/releasenotes/notes/add-policy-service-role-eaa391e30431a9d6.yaml new file mode 100644 index 0000000000..fc6ea3893a --- /dev/null +++ b/releasenotes/notes/add-policy-service-role-eaa391e30431a9d6.yaml @@ -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