From a9e075021fe04b8de6c963d8e8d39c2869f5ed4b Mon Sep 17 00:00:00 2001 From: TommyLike Date: Wed, 11 Oct 2017 09:02:13 +0800 Subject: [PATCH] [policy in code] Add support for volume, volume type resources This patch adds policy in code support for volume and volume type resources. Change-Id: I47d11a2f6423a76ca053abf075791ed70ee84b07 Partial-Implements: blueprint policy-in-code --- cinder/api/common.py | 9 - cinder/api/contrib/admin_actions.py | 16 +- cinder/api/contrib/types_extra_specs.py | 13 +- cinder/api/contrib/types_manage.py | 10 +- cinder/api/contrib/volume_actions.py | 10 +- .../api/contrib/volume_encryption_metadata.py | 6 +- cinder/api/contrib/volume_host_attribute.py | 9 +- cinder/api/contrib/volume_image_metadata.py | 12 +- cinder/api/contrib/volume_manage.py | 10 +- .../contrib/volume_mig_status_attribute.py | 8 +- cinder/api/contrib/volume_tenant_attribute.py | 9 +- cinder/api/contrib/volume_type_access.py | 20 +- cinder/api/contrib/volume_type_encryption.py | 13 +- cinder/api/contrib/volume_unmanage.py | 4 +- cinder/api/extensions.py | 14 +- cinder/api/openstack/wsgi.py | 18 -- cinder/api/v2/views/types.py | 9 +- cinder/api/v3/volumes.py | 15 +- cinder/policies/__init__.py | 16 ++ cinder/policies/base.py | 2 - cinder/policies/manageable_volumes.py | 66 +++++ cinder/policies/type_extra_specs.py | 83 ++++++ cinder/policies/volume_access.py | 73 ++++++ cinder/policies/volume_actions.py | 239 ++++++++++++++++++ cinder/policies/volume_metadata.py | 128 ++++++++++ cinder/policies/volume_transfer.py | 87 +++++++ cinder/policies/volume_type.py | 106 ++++++++ cinder/policies/volumes.py | 168 ++++++++++++ cinder/policy.py | 11 - .../unit/api/contrib/test_types_manage.py | 24 +- .../api/contrib/test_volume_type_access.py | 11 +- cinder/tests/unit/api/v2/test_types.py | 21 +- .../unit/attachments/test_attachments_api.py | 25 +- cinder/tests/unit/policy.json | 23 -- cinder/transfer/api.py | 11 +- cinder/volume/api.py | 116 ++++----- cinder/volume/flows/api/create_volume.py | 4 +- etc/cinder/policy.json | 59 +---- 38 files changed, 1117 insertions(+), 361 deletions(-) create mode 100644 cinder/policies/manageable_volumes.py create mode 100644 cinder/policies/type_extra_specs.py create mode 100644 cinder/policies/volume_access.py create mode 100644 cinder/policies/volume_actions.py create mode 100644 cinder/policies/volume_metadata.py create mode 100644 cinder/policies/volume_transfer.py create mode 100644 cinder/policies/volume_type.py create mode 100644 cinder/policies/volumes.py diff --git a/cinder/api/common.py b/cinder/api/common.py index ea117ff7756..a2904ab316a 100644 --- a/cinder/api/common.py +++ b/cinder/api/common.py @@ -28,7 +28,6 @@ from cinder.api import microversions as mv from cinder.common import constants from cinder import exception from cinder.i18n import _ -import cinder.policy from cinder import utils @@ -85,14 +84,6 @@ def validate_key_names(key_names_list): return True -def validate_policy(context, action): - try: - cinder.policy.enforce_action(context, action) - return True - except exception.PolicyNotAuthorized: - return False - - def get_pagination_params(params, max_limit=None): """Return marker, limit, offset tuple from request. diff --git a/cinder/api/contrib/admin_actions.py b/cinder/api/contrib/admin_actions.py index 0d9cd73740a..2ae61c87d04 100644 --- a/cinder/api/contrib/admin_actions.py +++ b/cinder/api/contrib/admin_actions.py @@ -79,18 +79,10 @@ class AdminController(wsgi.Controller): return update def authorize(self, context, action_name): - # TODO(tommylike): We have two different ways to authorize during - # implementing code base policies, the if/else statement can be - # removed when all resources are upgraded. - if self.resource_name in ['backup', 'snapshot']: - context.authorize( - 'volume_extension:%(resource)s_admin_actions:%(action)s' % - {'resource': self.resource_name, - 'action': action_name}) - else: - # e.g. "snapshot_admin_actions:reset_status" - action = '%s_admin_actions:%s' % (self.resource_name, action_name) - extensions.extension_authorizer('volume', action)(context) + context.authorize( + 'volume_extension:%(resource)s_admin_actions:%(action)s' % + {'resource': self.resource_name, + 'action': action_name}) def _remove_worker(self, context, id): # Remove the cleanup worker from the DB when we change a resource diff --git a/cinder/api/contrib/types_extra_specs.py b/cinder/api/contrib/types_extra_specs.py index b09270a14b0..00f65fd9d61 100644 --- a/cinder/api/contrib/types_extra_specs.py +++ b/cinder/api/contrib/types_extra_specs.py @@ -28,6 +28,7 @@ from cinder import context as ctxt from cinder import db from cinder import exception from cinder.i18n import _ +from cinder.policies import type_extra_specs as policy from cinder import rpc from cinder import utils from cinder.volume import volume_types @@ -46,8 +47,6 @@ extraspec_opts = [ CONF = cfg.CONF CONF.register_opts(extraspec_opts) -authorize = extensions.extension_authorizer('volume', 'types_extra_specs') - class VolumeTypeExtraSpecsController(wsgi.Controller): """The volume type extra specs API controller for the OpenStack API.""" @@ -66,7 +65,7 @@ class VolumeTypeExtraSpecsController(wsgi.Controller): def index(self, req, type_id): """Returns the list of extra specs for a given volume type.""" context = req.environ['cinder.context'] - authorize(context, action="index") + context.authorize(policy.GET_ALL_POLICY) self._check_type(context, type_id) return self._get_extra_specs(context, type_id) @@ -89,7 +88,7 @@ class VolumeTypeExtraSpecsController(wsgi.Controller): def create(self, req, type_id, body=None): context = req.environ['cinder.context'] - authorize(context, action='create') + context.authorize(policy.CREATE_POLICY) self._allow_update(context, type_id) self.assert_valid_body(body, 'extra_specs') @@ -114,7 +113,7 @@ class VolumeTypeExtraSpecsController(wsgi.Controller): def update(self, req, type_id, id, body=None): context = req.environ['cinder.context'] - authorize(context, action='update') + context.authorize(policy.UPDATE_POLICY) self._allow_update(context, type_id) if not body: @@ -147,7 +146,7 @@ class VolumeTypeExtraSpecsController(wsgi.Controller): def show(self, req, type_id, id): """Return a single extra spec item.""" context = req.environ['cinder.context'] - authorize(context, action='show') + context.authorize(policy.GET_POLICY) self._check_type(context, type_id) specs = self._get_extra_specs(context, type_id) if id in specs['extra_specs']: @@ -160,7 +159,7 @@ class VolumeTypeExtraSpecsController(wsgi.Controller): """Deletes an existing extra spec.""" context = req.environ['cinder.context'] self._check_type(context, type_id) - authorize(context, action='delete') + context.authorize(policy.DELETE_POLICY) self._allow_update(context, type_id) # Not found exception will be handled at the wsgi level diff --git a/cinder/api/contrib/types_manage.py b/cinder/api/contrib/types_manage.py index 756f9dc2731..5b9654ebb2f 100644 --- a/cinder/api/contrib/types_manage.py +++ b/cinder/api/contrib/types_manage.py @@ -24,14 +24,12 @@ from cinder.api.openstack import wsgi from cinder.api.views import types as views_types from cinder import exception from cinder.i18n import _ +from cinder.policies import volume_type as policy from cinder import rpc from cinder import utils from cinder.volume import volume_types -authorize = extensions.extension_authorizer('volume', 'types_manage') - - class VolumeTypesManageController(wsgi.Controller): """The volume types API controller for the OpenStack API.""" @@ -53,7 +51,7 @@ class VolumeTypesManageController(wsgi.Controller): def _create(self, req, body): """Creates a new volume type.""" context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.MANAGE_POLICY) self.assert_valid_body(body, 'volume_type') @@ -103,7 +101,7 @@ class VolumeTypesManageController(wsgi.Controller): def _update(self, req, id, body): # Update description for a given volume type. context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.MANAGE_POLICY) self.assert_valid_body(body, 'volume_type') @@ -164,7 +162,7 @@ class VolumeTypesManageController(wsgi.Controller): def _delete(self, req, id): """Deletes an existing volume type.""" context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.MANAGE_POLICY) try: vol_type = volume_types.get_volume_type(context, id) diff --git a/cinder/api/contrib/volume_actions.py b/cinder/api/contrib/volume_actions.py index e91c0d3cd6c..de47a0976b3 100644 --- a/cinder/api/contrib/volume_actions.py +++ b/cinder/api/contrib/volume_actions.py @@ -28,6 +28,7 @@ from cinder import exception from cinder.i18n import _ from cinder.image import image_utils from cinder import keymgr +from cinder.policies import volume_actions as policy from cinder import utils from cinder import volume @@ -35,11 +36,6 @@ from cinder import volume CONF = cfg.CONF -def authorize(context, action_name): - action = 'volume_actions:%s' % action_name - extensions.extension_authorizer('volume', action)(context) - - class VolumeActionsController(wsgi.Controller): def __init__(self, *args, **kwargs): super(VolumeActionsController, self).__init__(*args, **kwargs) @@ -239,7 +235,7 @@ class VolumeActionsController(wsgi.Controller): # Not found exception will be handled at the wsgi level volume = self.volume_api.get(context, id) - authorize(context, "upload_image") + context.authorize(policy.UPLOAD_IMAGE_POLICY) # check for valid disk-format disk_format = params.get("disk_format", "raw") if not image_utils.validate_disk_format(disk_format): @@ -278,7 +274,7 @@ class VolumeActionsController(wsgi.Controller): image_metadata['protected'] = params.get('protected', 'False') if image_metadata['visibility'] == 'public': - authorize(context, 'upload_public') + context.authorize(policy.UPLOAD_PUBLIC_POLICY) image_metadata['protected'] = ( utils.get_bool_param('protected', image_metadata)) diff --git a/cinder/api/contrib/volume_encryption_metadata.py b/cinder/api/contrib/volume_encryption_metadata.py index 66f18277a5f..0236d7a2be9 100644 --- a/cinder/api/contrib/volume_encryption_metadata.py +++ b/cinder/api/contrib/volume_encryption_metadata.py @@ -18,9 +18,7 @@ from cinder.api import extensions from cinder.api.openstack import wsgi from cinder import db - -authorize = extensions.extension_authorizer('volume', - 'volume_encryption_metadata') +from cinder.policies import volumes as policy class VolumeEncryptionMetadataController(wsgi.Controller): @@ -29,7 +27,7 @@ class VolumeEncryptionMetadataController(wsgi.Controller): def index(self, req, volume_id): """Returns the encryption metadata for a given volume.""" context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.ENCRYPTION_METADATA_POLICY) return db.volume_encryption_metadata_get(context, volume_id) def show(self, req, volume_id, id): diff --git a/cinder/api/contrib/volume_host_attribute.py b/cinder/api/contrib/volume_host_attribute.py index efe7cfefb8c..74e1ffd1b2e 100644 --- a/cinder/api/contrib/volume_host_attribute.py +++ b/cinder/api/contrib/volume_host_attribute.py @@ -14,10 +14,7 @@ from cinder.api import extensions from cinder.api.openstack import wsgi - - -authorize = extensions.soft_extension_authorizer('volume', - 'volume_host_attribute') +from cinder.policies import volumes as policy class VolumeHostAttributeController(wsgi.Controller): @@ -29,14 +26,14 @@ class VolumeHostAttributeController(wsgi.Controller): @wsgi.extends def show(self, req, resp_obj, id): context = req.environ['cinder.context'] - if authorize(context): + if context.authorize(policy.HOST_ATTRIBUTE_POLICY, fatal=False): volume = resp_obj.obj['volume'] self._add_volume_host_attribute(req, volume) @wsgi.extends def detail(self, req, resp_obj): context = req.environ['cinder.context'] - if authorize(context): + if context.authorize(policy.HOST_ATTRIBUTE_POLICY, fatal=False): for vol in list(resp_obj.obj['volumes']): self._add_volume_host_attribute(req, vol) diff --git a/cinder/api/contrib/volume_image_metadata.py b/cinder/api/contrib/volume_image_metadata.py index db613746f5a..2a7b8ae9be1 100644 --- a/cinder/api/contrib/volume_image_metadata.py +++ b/cinder/api/contrib/volume_image_metadata.py @@ -23,14 +23,12 @@ from cinder.api import extensions from cinder.api.openstack import wsgi from cinder import exception from cinder.i18n import _ +from cinder.policies import volume_metadata as policy from cinder import volume LOG = logging.getLogger(__name__) -authorize = extensions.soft_extension_authorizer('volume', - 'volume_image_metadata') - class VolumeImageMetadataController(wsgi.Controller): def __init__(self, *args, **kwargs): @@ -72,13 +70,13 @@ class VolumeImageMetadataController(wsgi.Controller): @wsgi.extends def show(self, req, resp_obj, id): context = req.environ['cinder.context'] - if authorize(context): + if context.authorize(policy.IMAGE_METADATA_POLICY, fatal=False): self._add_image_metadata(context, [resp_obj.obj['volume']]) @wsgi.extends def detail(self, req, resp_obj): context = req.environ['cinder.context'] - if authorize(context): + if context.authorize(policy.IMAGE_METADATA_POLICY, fatal=False): # Just get the image metadata of those volumes in response. volumes = list(resp_obj.obj.get('volumes', [])) if volumes: @@ -87,7 +85,7 @@ class VolumeImageMetadataController(wsgi.Controller): @wsgi.action("os-set_image_metadata") def create(self, req, id, body): context = req.environ['cinder.context'] - if authorize(context): + if context.authorize(policy.IMAGE_METADATA_POLICY, fatal=False): try: metadata = body['os-set_image_metadata']['metadata'] except (KeyError, TypeError): @@ -130,7 +128,7 @@ class VolumeImageMetadataController(wsgi.Controller): def delete(self, req, id, body): """Deletes an existing image metadata.""" context = req.environ['cinder.context'] - if authorize(context): + if context.authorize(policy.IMAGE_METADATA_POLICY, fatal=False): try: key = body['os-unset_image_metadata']['key'] except (KeyError, TypeError): diff --git a/cinder/api/contrib/volume_manage.py b/cinder/api/contrib/volume_manage.py index f1089a1c2bb..bdb4c886cce 100644 --- a/cinder/api/contrib/volume_manage.py +++ b/cinder/api/contrib/volume_manage.py @@ -24,14 +24,12 @@ from cinder.api.v2.views import volumes as volume_views from cinder.api.views import manageable_volumes as list_manageable_view from cinder import exception from cinder.i18n import _ +from cinder.policies import manageable_volumes as policy from cinder import utils from cinder import volume as cinder_volume from cinder.volume import volume_types LOG = logging.getLogger(__name__) -authorize_manage = extensions.extension_authorizer('volume', 'volume_manage') -authorize_list_manageable = extensions.extension_authorizer('volume', - 'list_manageable') class VolumeManageController(wsgi.Controller): @@ -99,7 +97,7 @@ class VolumeManageController(wsgi.Controller): """ context = req.environ['cinder.context'] - authorize_manage(context) + context.authorize(policy.MANAGE_POLICY) self.assert_valid_body(body, 'volume') @@ -155,7 +153,7 @@ class VolumeManageController(wsgi.Controller): def index(self, req): """Returns a summary list of volumes available to manage.""" context = req.environ['cinder.context'] - authorize_list_manageable(context) + context.authorize(policy.LIST_MANAGEABLE_POLICY) return resource_common_manage.get_manageable_resources( req, False, self.volume_api.get_manageable_volumes, self._list_manageable_view) @@ -164,7 +162,7 @@ class VolumeManageController(wsgi.Controller): def detail(self, req): """Returns a detailed list of volumes available to manage.""" context = req.environ['cinder.context'] - authorize_list_manageable(context) + context.authorize(policy.LIST_MANAGEABLE_POLICY) return resource_common_manage.get_manageable_resources( req, True, self.volume_api.get_manageable_volumes, self._list_manageable_view) diff --git a/cinder/api/contrib/volume_mig_status_attribute.py b/cinder/api/contrib/volume_mig_status_attribute.py index ce2e6d1fa4c..47750ce2f15 100644 --- a/cinder/api/contrib/volume_mig_status_attribute.py +++ b/cinder/api/contrib/volume_mig_status_attribute.py @@ -14,9 +14,7 @@ from cinder.api import extensions from cinder.api.openstack import wsgi - -authorize = extensions.soft_extension_authorizer('volume', - 'volume_mig_status_attribute') +from cinder.policies import volumes as policy class VolumeMigStatusAttributeController(wsgi.Controller): @@ -30,13 +28,13 @@ class VolumeMigStatusAttributeController(wsgi.Controller): @wsgi.extends def show(self, req, resp_obj, id): context = req.environ['cinder.context'] - if authorize(context): + if context.authorize(policy.MIG_ATTRIBUTE_POLICY, fatal=False): self._add_volume_mig_status_attribute(req, resp_obj.obj['volume']) @wsgi.extends def detail(self, req, resp_obj): context = req.environ['cinder.context'] - if authorize(context): + if context.authorize(policy.MIG_ATTRIBUTE_POLICY, fatal=False): for vol in list(resp_obj.obj['volumes']): self._add_volume_mig_status_attribute(req, vol) diff --git a/cinder/api/contrib/volume_tenant_attribute.py b/cinder/api/contrib/volume_tenant_attribute.py index 263704abc40..5362928e801 100644 --- a/cinder/api/contrib/volume_tenant_attribute.py +++ b/cinder/api/contrib/volume_tenant_attribute.py @@ -14,10 +14,7 @@ from cinder.api import extensions from cinder.api.openstack import wsgi - - -authorize = extensions.soft_extension_authorizer('volume', - 'volume_tenant_attribute') +from cinder.policies import volumes as policy class VolumeTenantAttributeController(wsgi.Controller): @@ -29,14 +26,14 @@ class VolumeTenantAttributeController(wsgi.Controller): @wsgi.extends def show(self, req, resp_obj, id): context = req.environ['cinder.context'] - if authorize(context): + if context.authorize(policy.TENANT_ATTRIBUTE_POLICY, fatal=False): volume = resp_obj.obj['volume'] self._add_volume_tenant_attribute(req, volume) @wsgi.extends def detail(self, req, resp_obj): context = req.environ['cinder.context'] - if authorize(context): + if context.authorize(policy.TENANT_ATTRIBUTE_POLICY, fatal=False): for vol in list(resp_obj.obj['volumes']): self._add_volume_tenant_attribute(req, vol) diff --git a/cinder/api/contrib/volume_type_access.py b/cinder/api/contrib/volume_type_access.py index d778eb3ec01..e0737a03c85 100644 --- a/cinder/api/contrib/volume_type_access.py +++ b/cinder/api/contrib/volume_type_access.py @@ -22,14 +22,10 @@ from cinder.api import extensions from cinder.api.openstack import wsgi from cinder import exception from cinder.i18n import _ +from cinder.policies import volume_access as policy from cinder.volume import volume_types -soft_authorize = extensions.soft_extension_authorizer('volume', - 'volume_type_access') -authorize = extensions.extension_authorizer('volume', 'volume_type_access') - - def _marshall_volume_type_access(vol_type): rval = [] for project_id in vol_type['projects']: @@ -44,7 +40,7 @@ class VolumeTypeAccessController(object): def index(self, req, type_id): context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.TYPE_ACCESS_POLICY) # Not found exception will be handled at the wsgi level vol_type = volume_types.get_volume_type( @@ -77,14 +73,14 @@ class VolumeTypeActionController(wsgi.Controller): @wsgi.extends def show(self, req, resp_obj, id): context = req.environ['cinder.context'] - if soft_authorize(context): + if context.authorize(policy.TYPE_ACCESS_POLICY, fatal=False): vol_type = req.cached_resource_by_id(id, name='types') self._extend_vol_type(resp_obj.obj['volume_type'], vol_type) @wsgi.extends def index(self, req, resp_obj): context = req.environ['cinder.context'] - if soft_authorize(context): + if context.authorize(policy.TYPE_ACCESS_POLICY, fatal=False): for vol_type_rval in list(resp_obj.obj['volume_types']): type_id = vol_type_rval['id'] vol_type = req.cached_resource_by_id(type_id, name='types') @@ -93,7 +89,7 @@ class VolumeTypeActionController(wsgi.Controller): @wsgi.extends def detail(self, req, resp_obj): context = req.environ['cinder.context'] - if soft_authorize(context): + if context.authorize(policy.TYPE_ACCESS_POLICY, fatal=False): for vol_type_rval in list(resp_obj.obj['volume_types']): type_id = vol_type_rval['id'] vol_type = req.cached_resource_by_id(type_id, name='types') @@ -102,7 +98,7 @@ class VolumeTypeActionController(wsgi.Controller): @wsgi.extends(action='create') def create(self, req, body, resp_obj): context = req.environ['cinder.context'] - if soft_authorize(context): + if context.authorize(policy.TYPE_ACCESS_POLICY, fatal=False): type_id = resp_obj.obj['volume_type']['id'] vol_type = req.cached_resource_by_id(type_id, name='types') self._extend_vol_type(resp_obj.obj['volume_type'], vol_type) @@ -110,7 +106,7 @@ class VolumeTypeActionController(wsgi.Controller): @wsgi.action('addProjectAccess') def _addProjectAccess(self, req, id, body): context = req.environ['cinder.context'] - authorize(context, action="addProjectAccess") + context.authorize(policy.ADD_PROJECT_POLICY) self._check_body(body, 'addProjectAccess') project = body['addProjectAccess']['project'] @@ -124,7 +120,7 @@ class VolumeTypeActionController(wsgi.Controller): @wsgi.action('removeProjectAccess') def _removeProjectAccess(self, req, id, body): context = req.environ['cinder.context'] - authorize(context, action="removeProjectAccess") + context.authorize(policy.REMOVE_PROJECT_POLICY) self._check_body(body, 'removeProjectAccess') project = body['removeProjectAccess']['project'] diff --git a/cinder/api/contrib/volume_type_encryption.py b/cinder/api/contrib/volume_type_encryption.py index 4cc4e7020f3..311d6449a86 100644 --- a/cinder/api/contrib/volume_type_encryption.py +++ b/cinder/api/contrib/volume_type_encryption.py @@ -23,12 +23,11 @@ from cinder.api.openstack import wsgi from cinder import db from cinder import exception from cinder.i18n import _ +from cinder.policies import volume_type as policy from cinder import rpc from cinder import utils from cinder.volume import volume_types -authorize = extensions.extension_authorizer('volume', - 'volume_type_encryption') CONTROL_LOCATION = ['front-end', 'back-end'] @@ -84,14 +83,14 @@ class VolumeTypeEncryptionController(wsgi.Controller): def index(self, req, type_id): """Returns the encryption specs for a given volume type.""" context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.ENCRYPTION_POLICY) self._check_type(context, type_id) return self._get_volume_type_encryption(context, type_id) def create(self, req, type_id, body=None): """Create encryption specs for an existing volume type.""" context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.ENCRYPTION_POLICY) if self._encrypted_type_in_use(context, type_id): expl = _('Cannot create encryption specs. Volume type in use.') @@ -118,7 +117,7 @@ class VolumeTypeEncryptionController(wsgi.Controller): def update(self, req, type_id, id, body=None): """Update encryption specs for a given volume type.""" context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.ENCRYPTION_POLICY) self.assert_valid_body(body, 'encryption') @@ -145,7 +144,7 @@ class VolumeTypeEncryptionController(wsgi.Controller): def show(self, req, type_id, id): """Return a single encryption item.""" context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.ENCRYPTION_POLICY) self._check_type(context, type_id) @@ -159,7 +158,7 @@ class VolumeTypeEncryptionController(wsgi.Controller): def delete(self, req, type_id, id): """Delete encryption specs for a given volume type.""" context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.ENCRYPTION_POLICY) if self._encrypted_type_in_use(context, type_id): expl = _('Cannot delete encryption specs. Volume type in use.') diff --git a/cinder/api/contrib/volume_unmanage.py b/cinder/api/contrib/volume_unmanage.py index c16651527c7..2983de5bda6 100644 --- a/cinder/api/contrib/volume_unmanage.py +++ b/cinder/api/contrib/volume_unmanage.py @@ -18,10 +18,10 @@ import webob from cinder.api import extensions from cinder.api.openstack import wsgi +from cinder.policies import manageable_volumes as policy from cinder import volume LOG = logging.getLogger(__name__) -authorize = extensions.extension_authorizer('volume', 'volume_unmanage') class VolumeUnmanageController(wsgi.Controller): @@ -47,7 +47,7 @@ class VolumeUnmanageController(wsgi.Controller): attached to an instance. """ context = req.environ['cinder.context'] - authorize(context) + context.authorize(policy.UNMANAGE_POLICY) LOG.info("Unmanage volume with id: %s", id) diff --git a/cinder/api/extensions.py b/cinder/api/extensions.py index 3733bc30147..0d750e57436 100644 --- a/cinder/api/extensions.py +++ b/cinder/api/extensions.py @@ -329,17 +329,5 @@ def extension_authorizer(api_name, extension_name): act = '%s_extension:%s' % (api_name, extension_name) else: act = '%s_extension:%s:%s' % (api_name, extension_name, action) - cinder.policy.enforce(context, act, target) - return authorize - - -def soft_extension_authorizer(api_name, extension_name): - hard_authorize = extension_authorizer(api_name, extension_name) - - def authorize(context): - try: - hard_authorize(context) - return True - except exception.NotAuthorized: - return False + cinder.policy.authorize(context, act, target) return authorize diff --git a/cinder/api/openstack/wsgi.py b/cinder/api/openstack/wsgi.py index 429ace7e7cb..0fae7697d57 100644 --- a/cinder/api/openstack/wsgi.py +++ b/cinder/api/openstack/wsgi.py @@ -37,7 +37,6 @@ from cinder import i18n i18n.enable_lazy() from cinder.i18n import _ -from cinder import policy from cinder import utils from cinder.wsgi import common as wsgi @@ -1285,23 +1284,6 @@ class Controller(object): except exception.InvalidInput as error: raise webob.exc.HTTPBadRequest(explanation=error.msg) - @staticmethod - def get_policy_checker(prefix): - @staticmethod - def policy_checker(req, action, resource=None): - ctxt = req.environ['cinder.context'] - target = { - 'project_id': ctxt.project_id, - 'user_id': ctxt.user_id, - } - if resource: - target.update(resource) - - _action = '%s:%s' % (prefix, action) - policy.enforce(ctxt, _action, target) - return ctxt - return policy_checker - class Fault(webob.exc.HTTPException): """Wrap webob.exc.HTTPException to provide API friendly response.""" diff --git a/cinder/api/v2/views/types.py b/cinder/api/v2/views/types.py index fbf0631110e..e9064b7b686 100644 --- a/cinder/api/v2/views/types.py +++ b/cinder/api/v2/views/types.py @@ -15,6 +15,7 @@ # under the License. from cinder.api import common +from cinder.policies import volume_type as policy class ViewBuilder(common.ViewBuilder): @@ -26,13 +27,9 @@ class ViewBuilder(common.ViewBuilder): name=volume_type.get('name'), is_public=volume_type.get('is_public'), description=volume_type.get('description')) - if common.validate_policy( - context, - 'volume_extension:access_types_extra_specs'): + if context.authorize(policy.EXTRA_SPEC_POLICY, fatal=False): trimmed['extra_specs'] = volume_type.get('extra_specs') - if common.validate_policy( - context, - 'volume_extension:access_types_qos_specs_id'): + if context.authorize(policy.QOS_POLICY, fatal=False): trimmed['qos_specs_id'] = volume_type.get('qos_specs_id') return trimmed if brief else dict(volume_type=trimmed) diff --git a/cinder/api/v3/volumes.py b/cinder/api/v3/volumes.py index 8f41694a5da..10615265369 100644 --- a/cinder/api/v3/volumes.py +++ b/cinder/api/v3/volumes.py @@ -29,23 +29,12 @@ from cinder import exception from cinder import group as group_api from cinder.i18n import _ from cinder import objects -import cinder.policy +from cinder.policies import volumes as policy from cinder import utils LOG = logging.getLogger(__name__) -def check_policy(context, action, target_obj=None): - target = { - 'project_id': context.project_id, - 'user_id': context.user_id - } - target.update(target_obj or {}) - - _action = 'volume:%s' % action - cinder.policy.enforce(context, _action, target) - - class VolumeController(volumes_v2.VolumeController): """The Volumes API controller for the OpenStack API V3.""" @@ -74,7 +63,7 @@ class VolumeController(volumes_v2.VolumeController): {'id': id, 'params': params}, context=context) if force: - check_policy(context, 'force_delete') + context.authorize(policy.FORCE_DELETE_POLICY) volume = self.volume_api.get(context, id) diff --git a/cinder/policies/__init__.py b/cinder/policies/__init__.py index c17569779b3..9cfc4acbbd4 100644 --- a/cinder/policies/__init__.py +++ b/cinder/policies/__init__.py @@ -29,6 +29,7 @@ from cinder.policies import groups from cinder.policies import hosts from cinder.policies import limits from cinder.policies import manageable_snapshots +from cinder.policies import manageable_volumes from cinder.policies import messages from cinder.policies import qos_specs from cinder.policies import quota_class @@ -38,6 +39,13 @@ from cinder.policies import services from cinder.policies import snapshot_actions from cinder.policies import snapshot_metadata from cinder.policies import snapshots +from cinder.policies import type_extra_specs +from cinder.policies import volume_access +from cinder.policies import volume_actions +from cinder.policies import volume_metadata +from cinder.policies import volume_transfer +from cinder.policies import volume_type +from cinder.policies import volumes from cinder.policies import workers @@ -67,4 +75,12 @@ def list_rules(): scheduler_stats.list_rules(), hosts.list_rules(), limits.list_rules(), + manageable_volumes.list_rules(), + volume_type.list_rules(), + volume_access.list_rules(), + volume_actions.list_rules(), + volume_transfer.list_rules(), + volume_metadata.list_rules(), + type_extra_specs.list_rules(), + volumes.list_rules(), ) diff --git a/cinder/policies/base.py b/cinder/policies/base.py index 26f32f84bf7..960f0272edf 100644 --- a/cinder/policies/base.py +++ b/cinder/policies/base.py @@ -23,8 +23,6 @@ rules = [ policy.RuleDefault('admin_or_owner', 'is_admin:True or (role:admin and ' 'is_admin_project:True) or project_id:%(project_id)s'), - policy.RuleDefault('default', - 'rule:admin_or_owner'), policy.RuleDefault('admin_api', 'is_admin:True or (role:admin and ' 'is_admin_project:True)'), diff --git a/cinder/policies/manageable_volumes.py b/cinder/policies/manageable_volumes.py new file mode 100644 index 00000000000..f415f6360a2 --- /dev/null +++ b/cinder/policies/manageable_volumes.py @@ -0,0 +1,66 @@ +# Copyright (c) 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from cinder.policies import base + + +MANAGE_POLICY = "volume_extension:volume_manage" +UNMANAGE_POLICY = "volume_extension:volume_unmanage" +LIST_MANAGEABLE_POLICY = "volume_extension:list_manageable" + + +manageable_volumes_policies = [ + policy.DocumentedRuleDefault( + name=LIST_MANAGEABLE_POLICY, + check_str=base.RULE_ADMIN_API, + description= + """List (in detail) of volumes which are available to manage.""", + operations=[ + { + 'method': 'GET', + 'path': '/manageable_volumes' + }, + { + 'method': 'GET', + 'path': '/manageable_volumes/detail' + } + ]), + policy.DocumentedRuleDefault( + name=MANAGE_POLICY, + check_str=base.RULE_ADMIN_API, + description="""Manage existing volumes.""", + operations=[ + { + 'method': 'POST', + 'path': '/manageable_volumes' + } + ]), + policy.DocumentedRuleDefault( + name=UNMANAGE_POLICY, + check_str=base.RULE_ADMIN_API, + description="""Stop managing a volume.""", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-unmanage)' + } + ]), +] + + +def list_rules(): + return manageable_volumes_policies diff --git a/cinder/policies/type_extra_specs.py b/cinder/policies/type_extra_specs.py new file mode 100644 index 00000000000..a9eacdcd21f --- /dev/null +++ b/cinder/policies/type_extra_specs.py @@ -0,0 +1,83 @@ +# Copyright (c) 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from cinder.policies import base + + +CREATE_POLICY = "volume_extension:types_extra_specs:create" +DELETE_POLICY = "volume_extension:types_extra_specs:delete" +GET_ALL_POLICY = "volume_extension:types_extra_specs:index" +GET_POLICY = "volume_extension:types_extra_specs:show" +UPDATE_POLICY = "volume_extension:types_extra_specs:update" + + +type_extra_specs_policies = [ + policy.DocumentedRuleDefault( + name=GET_ALL_POLICY, + check_str=base.RULE_ADMIN_API, + description="List type extra specs.", + operations=[ + { + 'method': 'GET', + 'path': '/types/{type_id}/extra_specs' + } + ]), + policy.DocumentedRuleDefault( + name=CREATE_POLICY, + check_str=base.RULE_ADMIN_API, + description="Create type extra specs.", + operations=[ + { + 'method': 'POST', + 'path': '/types/{type_id}/extra_specs' + } + ]), + policy.DocumentedRuleDefault( + name=GET_POLICY, + check_str=base.RULE_ADMIN_API, + description="Show one specified type extra specs.", + operations=[ + { + 'method': 'GET', + 'path': '/types/{type_id}/extra_specs/{extra_spec_key}' + } + ]), + policy.DocumentedRuleDefault( + name=UPDATE_POLICY, + check_str=base.RULE_ADMIN_API, + description="Update type extra specs.", + operations=[ + { + 'method': 'PUT', + 'path': '/types/{type_id}/extra_specs/{extra_spec_key}' + } + ]), + policy.DocumentedRuleDefault( + name=DELETE_POLICY, + check_str=base.RULE_ADMIN_API, + description="Delete type extra specs.", + operations=[ + { + 'method': 'DELETE', + 'path': '/types/{type_id}/extra_specs/{extra_spec_key}' + } + ]), +] + + +def list_rules(): + return type_extra_specs_policies diff --git a/cinder/policies/volume_access.py b/cinder/policies/volume_access.py new file mode 100644 index 00000000000..78aeeac46f8 --- /dev/null +++ b/cinder/policies/volume_access.py @@ -0,0 +1,73 @@ +# Copyright (c) 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from cinder.policies import base + + +ADD_PROJECT_POLICY = "volume_extension:volume_type_access:addProjectAccess" +REMOVE_PROJECT_POLICY = \ + "volume_extension:volume_type_access:removeProjectAccess" +TYPE_ACCESS_POLICY = "volume_extension:volume_type_access" + +volume_access_policies = [ + policy.DocumentedRuleDefault( + name=TYPE_ACCESS_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Volume type access related APIs.", + operations=[ + { + 'method': 'GET', + 'path': '/types' + }, + { + 'method': 'GET', + 'path': '/types/detail' + }, + { + 'method': 'GET', + 'path': '/types/{type_id}' + }, + { + 'method': 'POST', + 'path': '/types' + } + ]), + policy.DocumentedRuleDefault( + name=ADD_PROJECT_POLICY, + check_str=base.RULE_ADMIN_API, + description="Add volume type access for project.", + operations=[ + { + 'method': 'POST', + 'path': '/types/{type_id}/action (addProjectAccess)' + } + ]), + policy.DocumentedRuleDefault( + name=REMOVE_PROJECT_POLICY, + check_str=base.RULE_ADMIN_API, + description="Remove volume type access for project.", + operations=[ + { + 'method': 'POST', + 'path': '/types/{type_id}/action (removeProjectAccess)' + } + ]), +] + + +def list_rules(): + return volume_access_policies diff --git a/cinder/policies/volume_actions.py b/cinder/policies/volume_actions.py new file mode 100644 index 00000000000..28bb7ac553f --- /dev/null +++ b/cinder/policies/volume_actions.py @@ -0,0 +1,239 @@ +# Copyright (c) 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from cinder.policies import base + + +EXTEND_POLICY = "volume:extend" +EXTEND_ATTACHED_POLICY = "volume:extend_attached_volume" +REVERT_POLICY = "volume:revert_to_snapshot" +RESET_STATUS = "volume_extension:volume_admin_actions:reset_status" +RETYPE_POLICY = "volume:retype" +UPDATE_READONLY_POLICY = "volume:update_readonly_flag" +FORCE_DELETE_POLICY = "volume_extension:volume_admin_actions:force_delete" +FORCE_DETACH_POLICY = "volume_extension:volume_admin_actions:force_detach" +UPLOAD_PUBLIC_POLICY = "volume_extension:volume_actions:upload_public" +UPLOAD_IMAGE_POLICY = "volume_extension:volume_actions:upload_image" +MIGRATE_POLICY = "volume_extension:volume_admin_actions:migrate_volume" +MIGRATE_COMPLETE_POLICY = \ + "volume_extension:volume_admin_actions:migrate_volume_completion" +DETACH_POLICY = "volume_extension:volume_actions:detach" +ATTACH_POLICY = "volume_extension:volume_actions:attach" +BEGIN_DETACHING_POLICY = "volume_extension:volume_actions:begin_detaching" +UNRESERVE_POLICY = "volume_extension:volume_actions:unreserve" +RESERVE_POLICY = "volume_extension:volume_actions:reserve" +ROLL_DETACHING_POLICY = "volume_extension:volume_actions:roll_detaching" +TERMINATE_POLICY = "volume_extension:volume_actions:terminate_connection" +INITIALIZE_POLICY = "volume_extension:volume_actions:initialize_connection" + +volume_action_policies = [ + policy.DocumentedRuleDefault( + name=EXTEND_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Extend a volume.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-extend)' + } + ]), + policy.DocumentedRuleDefault( + name=EXTEND_ATTACHED_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Extend a attached volume.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-extend)' + } + ]), + policy.DocumentedRuleDefault( + name=REVERT_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Revert a volume to a snapshot.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (revert)' + } + ]), + policy.DocumentedRuleDefault( + name=RESET_STATUS, + check_str=base.RULE_ADMIN_API, + description="Reset status of a volume.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-reset_status)' + } + ]), + policy.DocumentedRuleDefault( + name=RETYPE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Retype a volume.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-retype)' + } + ]), + policy.DocumentedRuleDefault( + name=UPDATE_READONLY_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Update a volume's readonly flag.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-update_readonly_flag)' + } + ]), + policy.DocumentedRuleDefault( + name=FORCE_DELETE_POLICY, + check_str=base.RULE_ADMIN_API, + description="Force delete a volume.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-force_delete)' + } + ]), + policy.DocumentedRuleDefault( + name=UPLOAD_PUBLIC_POLICY, + check_str=base.RULE_ADMIN_API, + description="Upload a volume to image with public visibility.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-volume_upload_image)' + } + ]), + policy.DocumentedRuleDefault( + name=UPLOAD_IMAGE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Upload a volume to image.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-volume_upload_image)' + } + ]), + policy.DocumentedRuleDefault( + name=FORCE_DETACH_POLICY, + check_str=base.RULE_ADMIN_API, + description="Force detach a volume.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-force_detach)' + } + ]), + policy.DocumentedRuleDefault( + name=MIGRATE_POLICY, + check_str=base.RULE_ADMIN_API, + description="migrate a volume to a specified host.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-migrate_volume)' + } + ]), + policy.DocumentedRuleDefault( + name=MIGRATE_COMPLETE_POLICY, + check_str=base.RULE_ADMIN_API, + description="Complete a volume migration.", + operations=[{ + 'method': 'POST', + 'path': + '/volumes/{volume_id}/action (os-migrate_volume_completion)'} + ]), + policy.DocumentedRuleDefault( + name=INITIALIZE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Initialize volume attachment.", + operations=[{ + 'method': 'POST', + 'path': + '/volumes/{volume_id}/action (os-initialize_connection)'} + ]), + policy.DocumentedRuleDefault( + name=TERMINATE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Terminate volume attachment.", + operations=[{ + 'method': 'POST', + 'path': + '/volumes/{volume_id}/action (os-terminate_connection)'} + ]), + policy.DocumentedRuleDefault( + name=ROLL_DETACHING_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Roll back volume status to 'in-use'.", + operations=[{ + 'method': 'POST', + 'path': + '/volumes/{volume_id}/action (os-roll_detaching)'} + ]), + policy.DocumentedRuleDefault( + name=RESERVE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Mark volume as reserved.", + operations=[{ + 'method': 'POST', + 'path': + '/volumes/{volume_id}/action (os-reserve)'} + ]), + policy.DocumentedRuleDefault( + name=UNRESERVE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Unmark volume as reserved.", + operations=[{ + 'method': 'POST', + 'path': + '/volumes/{volume_id}/action (os-unreserve)'} + ]), + policy.DocumentedRuleDefault( + name=BEGIN_DETACHING_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Begin detach volumes.", + operations=[{ + 'method': 'POST', + 'path': + '/volumes/{volume_id}/action (os-begin_detaching)'} + ]), + policy.DocumentedRuleDefault( + name=ATTACH_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Add attachment metadata.", + operations=[{ + 'method': 'POST', + 'path': + '/volumes/{volume_id}/action (os-attach)'} + ]), + policy.DocumentedRuleDefault( + name=DETACH_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Clear attachment metadata.", + operations=[{ + 'method': 'POST', + 'path': + '/volumes/{volume_id}/action (os-detach)'} + ]), +] + + +def list_rules(): + return volume_action_policies diff --git a/cinder/policies/volume_metadata.py b/cinder/policies/volume_metadata.py new file mode 100644 index 00000000000..01ea71ac61b --- /dev/null +++ b/cinder/policies/volume_metadata.py @@ -0,0 +1,128 @@ +# Copyright (c) 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from cinder.policies import base + +GET_POLICY = "volume:get_volume_metadata" +CREATE_POLICY = "volume:create_volume_metadata" +DELETE_POLICY = "volume:delete_volume_metadata" +UPDATE_POLICY = "volume:update_volume_metadata" +IMAGE_METADATA_POLICY = "volume_extension:volume_image_metadata" +UPDATE_ADMIN_METADATA_POLICY = "volume:update_volume_admin_metadata" + + +BASE_POLICY_NAME = 'volume:volume_metadata:%s' + + +volume_metadata_policies = [ + policy.DocumentedRuleDefault( + name=GET_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Show volume's metadata or one specified metadata " + "with a given key.", + operations=[ + { + 'method': 'GET', + 'path': '/volumes/{volume_id}/metadata' + }, + { + 'method': 'GET', + 'path': '/volumes/{volume_id}/metadata/{key}' + } + ]), + policy.DocumentedRuleDefault( + name=CREATE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Create volume metadata.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/metadata' + } + ]), + policy.DocumentedRuleDefault( + name=UPDATE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Update volume's metadata or one specified " + "metadata with a given key.", + operations=[ + { + 'method': 'PUT', + 'path': '/volumes/{volume_id}/metadata' + }, + { + 'method': 'PUT', + 'path': '/volumes/{volume_id}/metadata/{key}' + } + ]), + policy.DocumentedRuleDefault( + name=DELETE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Delete volume's specified metadata with a given key.", + operations=[ + { + 'method': 'DELETE', + 'path': '/volumes/{volume_id}/metadata/{key}' + } + ]), + policy.DocumentedRuleDefault( + name=IMAGE_METADATA_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Volume's image metadata related operation, create, " + "delete, show and list.", + operations=[ + { + 'method': 'GET', + 'path': '/volumes/detail' + }, + { + 'method': 'GET', + 'path': '/volumes/{volume_id}' + }, + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-set_image_metadata)' + }, + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-show_image_metadata)' + }, + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-unset_image_metadata)' + } + ]), + policy.DocumentedRuleDefault( + name=UPDATE_ADMIN_METADATA_POLICY, + check_str=base.RULE_ADMIN_API, + description="Update volume admin metadata. It's used in `attach` " + "and `os-update_readonly_flag` APIs", + operations=[ + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-update_readonly_flag)' + }, + { + 'method': 'POST', + 'path': '/volumes/{volume_id}/action (os-attach)' + } + ]), +] + + +def list_rules(): + return volume_metadata_policies diff --git a/cinder/policies/volume_transfer.py b/cinder/policies/volume_transfer.py new file mode 100644 index 00000000000..69971739d29 --- /dev/null +++ b/cinder/policies/volume_transfer.py @@ -0,0 +1,87 @@ +# Copyright (c) 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from cinder.policies import base + + +CREATE_POLICY = "volume:create_transfer" +ACCEPT_POLICY = "volume:accept_transfer" +DELETE_POLICY = "volume:delete_transfer" +GET_POLICY = "volume:get_transfer" +GET_ALL_POLICY = "volume:get_all_transfers" + + +volume_transfer_policies = [ + policy.DocumentedRuleDefault( + name=GET_ALL_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="List volume transfer.", + operations=[ + { + 'method': 'GET', + 'path': '/os-volume-transfer' + }, + { + 'method': 'GET', + 'path': '/os-volume-transfer/detail' + } + ]), + policy.DocumentedRuleDefault( + name=CREATE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Create a volume transfer.", + operations=[ + { + 'method': 'POST', + 'path': '/os-volume-transfer' + } + ]), + policy.DocumentedRuleDefault( + name=GET_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Show one specified volume transfer.", + operations=[ + { + 'method': 'GET', + 'path': '/os-volume-transfer/{transfer_id}' + } + ]), + policy.DocumentedRuleDefault( + name=ACCEPT_POLICY, + check_str="", + description="Accept a volume transfer.", + operations=[ + { + 'method': 'POST', + 'path': '/os-volume-transfer/{transfer_id}/accept' + } + ]), + policy.DocumentedRuleDefault( + name=DELETE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Delete volume transfer.", + operations=[ + { + 'method': 'DELETE', + 'path': '/os-volume-transfer/{transfer_id}' + } + ]), +] + + +def list_rules(): + return volume_transfer_policies diff --git a/cinder/policies/volume_type.py b/cinder/policies/volume_type.py new file mode 100644 index 00000000000..381bcfc9173 --- /dev/null +++ b/cinder/policies/volume_type.py @@ -0,0 +1,106 @@ +# Copyright (c) 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from cinder.policies import base + + +MANAGE_POLICY = "volume_extension:types_manage" +ENCRYPTION_POLICY = "volume_extension:volume_type_encryption" +QOS_POLICY = "volume_extension:access_types_qos_specs_id" +EXTRA_SPEC_POLICY = "volume_extension:access_types_extra_specs" + +volume_type_policies = [ + policy.DocumentedRuleDefault( + name=MANAGE_POLICY, + check_str=base.RULE_ADMIN_API, + description="""Create, update and delete volume type.""", + operations=[ + { + 'method': 'POST', + 'path': '/types' + }, + { + 'method': 'PUT', + 'path': '/types' + }, + { + 'method': 'DELETE', + 'path': '/types' + } + ]), + policy.DocumentedRuleDefault( + name=ENCRYPTION_POLICY, + check_str=base.RULE_ADMIN_API, + description="""List, show, create, update and delete volume +type encryption.""", + operations=[ + { + 'method': 'POST', + 'path': '/types/{type_id}/encryption' + }, + { + 'method': 'PUT', + 'path': '/types/{type_id}/encryption/{encryption_id}' + }, + { + 'method': 'GET', + 'path': '/types/{type_id}/encryption' + }, + { + 'method': 'GET', + 'path': '/types/{type_id}/encryption/{encryption_id}' + }, + { + 'method': 'DELETE', + 'path': '/types/{type_id}/encryption/{encryption_id}' + } + ]), + policy.DocumentedRuleDefault( + name=EXTRA_SPEC_POLICY, + check_str=base.RULE_ADMIN_API, + description="""List or show volume type with access type extra +specs attribute.""", + operations=[ + { + 'method': 'GET', + 'path': '/types/{type_id}' + }, + { + 'method': 'GET', + 'path': '/types' + } + ]), + policy.DocumentedRuleDefault( + name=QOS_POLICY, + check_str=base.RULE_ADMIN_API, + description="""List or show volume type with access type qos specs +id attribute.""", + operations=[ + { + 'method': 'GET', + 'path': '/types/{type_id}' + }, + { + 'method': 'GET', + 'path': '/types' + } + ]), +] + + +def list_rules(): + return volume_type_policies diff --git a/cinder/policies/volumes.py b/cinder/policies/volumes.py new file mode 100644 index 00000000000..15b9245be11 --- /dev/null +++ b/cinder/policies/volumes.py @@ -0,0 +1,168 @@ +# Copyright (c) 2017 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from cinder.policies import base + + +CREATE_POLICY = "volume:create" +CREATE_FROM_IMAGE_POLICY = "volume:create_from_image" +GET_POLICY = "volume:get" +GET_ALL_POLICY = "volume:get_all" +UPDATE_POLICY = "volume:update" +DELETE_POLICY = "volume:delete" +FORCE_DELETE_POLICY = "volume:force_delete" +HOST_ATTRIBUTE_POLICY = "volume_extension:volume_host_attribute" +TENANT_ATTRIBUTE_POLICY = "volume_extension:volume_tenant_attribute" +MIG_ATTRIBUTE_POLICY = "volume_extension:volume_mig_status_attribute" +ENCRYPTION_METADATA_POLICY = "volume_extension:volume_encryption_metadata" + +volumes_policies = [ + policy.DocumentedRuleDefault( + name=CREATE_POLICY, + check_str="", + description="Create volume.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes' + } + ]), + policy.DocumentedRuleDefault( + name=CREATE_FROM_IMAGE_POLICY, + check_str="", + description="Create volume from image.", + operations=[ + { + 'method': 'POST', + 'path': '/volumes' + } + ]), + policy.DocumentedRuleDefault( + name=GET_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Show volume.", + operations=[ + { + 'method': 'GET', + 'path': '/volumes/{volume_id}' + } + ]), + policy.DocumentedRuleDefault( + name=GET_ALL_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="List volumes.", + operations=[ + { + 'method': 'GET', + 'path': '/volumes' + }, + { + 'method': 'GET', + 'path': '/volumes/detail' + } + ]), + policy.DocumentedRuleDefault( + name=UPDATE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Update volume.", + operations=[ + { + 'method': 'PUT', + 'path': '/volumes' + } + ]), + policy.DocumentedRuleDefault( + name=DELETE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Delete volume.", + operations=[ + { + 'method': 'DELETE', + 'path': '/volumes/{volume_id}' + } + ]), + policy.DocumentedRuleDefault( + name=FORCE_DELETE_POLICY, + check_str=base.RULE_ADMIN_API, + description="Force Delete a volume.", + operations=[ + { + 'method': 'DELETE', + 'path': '/volumes/{volume_id}' + } + ]), + policy.DocumentedRuleDefault( + name=HOST_ATTRIBUTE_POLICY, + check_str=base.RULE_ADMIN_API, + description="List or show volume with host attribute.", + operations=[ + { + 'method': 'GET', + 'path': '/volumes/{volume_id}' + }, + { + 'method': 'GET', + 'path': '/volumes/detail' + } + ]), + policy.DocumentedRuleDefault( + name=TENANT_ATTRIBUTE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="List or show volume with tenant attribute.", + operations=[ + { + 'method': 'GET', + 'path': '/volumes/{volume_id}' + }, + { + 'method': 'GET', + 'path': '/volumes/detail' + } + ]), + policy.DocumentedRuleDefault( + name=MIG_ATTRIBUTE_POLICY, + check_str=base.RULE_ADMIN_API, + description="List or show volume with migration status attribute.", + operations=[ + { + 'method': 'GET', + 'path': '/volumes/{volume_id}' + }, + { + 'method': 'GET', + 'path': '/volumes/detail' + } + ]), + policy.DocumentedRuleDefault( + name=ENCRYPTION_METADATA_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="Show volume's encryption metadata.", + operations=[ + { + 'method': 'GET', + 'path': '/volumes/{volume_id}/encryption' + }, + { + 'method': 'GET', + 'path': '/volumes/{volume_id}/encryption/{encryption_key}' + } + ]), +] + + +def list_rules(): + return volumes_policies diff --git a/cinder/policy.py b/cinder/policy.py index 13fb02201dd..06e8ff8ec06 100644 --- a/cinder/policy.py +++ b/cinder/policy.py @@ -63,17 +63,6 @@ def init(policy_file=None, rules=None, default_rule=None, use_conf=True): _ENFORCER.load_rules() -def enforce_action(context, action): - """Checks that the action can be done by the given context. - - Applies a check to ensure the context's project_id and user_id can be - applied to the given action using the policy enforcement api. - """ - - return enforce(context, action, {'project_id': context.project_id, - 'user_id': context.user_id}) - - def enforce(context, action, target): """Verifies that the action is valid on the target in this context. diff --git a/cinder/tests/unit/api/contrib/test_types_manage.py b/cinder/tests/unit/api/contrib/test_types_manage.py index 5f7b77080b7..c3f31507e3b 100644 --- a/cinder/tests/unit/api/contrib/test_types_manage.py +++ b/cinder/tests/unit/api/contrib/test_types_manage.py @@ -183,12 +183,12 @@ class VolumeTypesManageApiTest(test.TestCase): @mock.patch('cinder.volume.volume_types.destroy') @mock.patch('cinder.volume.volume_types.get_volume_type') - @mock.patch('cinder.policy.enforce') - def test_volume_types_delete_with_non_admin(self, mock_policy_enforce, + @mock.patch('cinder.policy.authorize') + def test_volume_types_delete_with_non_admin(self, mock_policy_authorize, mock_get, mock_destroy): # allow policy authorized user to delete type - mock_policy_enforce.return_value = None + mock_policy_authorize.return_value = None mock_get.return_value = \ {'extra_specs': {"key1": "value1"}, 'id': DEFAULT_VOLUME_TYPE, @@ -203,7 +203,7 @@ class VolumeTypesManageApiTest(test.TestCase): self.controller._delete(req, DEFAULT_VOLUME_TYPE) self.assertEqual(1, len(self.notifier.notifications)) # non policy authorized user fails to delete type - mock_policy_enforce.side_effect = ( + mock_policy_authorize.side_effect = ( exception.PolicyNotAuthorized(action='type_delete')) self.assertRaises(exception.PolicyNotAuthorized, self.controller._delete, @@ -329,13 +329,13 @@ class VolumeTypesManageApiTest(test.TestCase): @mock.patch('cinder.volume.volume_types.create') @mock.patch('cinder.volume.volume_types.get_volume_type_by_name') - @mock.patch('cinder.policy.enforce') - def test_create_with_none_admin(self, mock_policy_enforce, + @mock.patch('cinder.policy.authorize') + def test_create_with_none_admin(self, mock_policy_authorize, mock_get_volume_type_by_name, mock_create_type): # allow policy authorized user to create type - mock_policy_enforce.return_value = None + mock_policy_authorize.return_value = None mock_get_volume_type_by_name.return_value = \ {'extra_specs': {"key1": "value1"}, 'id': DEFAULT_VOLUME_TYPE, @@ -356,7 +356,7 @@ class VolumeTypesManageApiTest(test.TestCase): 'expected_name': 'vol_type_1', 'expected_desc': 'vol_type_desc_1'}) # non policy authorized user fails to create type - mock_policy_enforce.side_effect = ( + mock_policy_authorize.side_effect = ( exception.PolicyNotAuthorized(action='type_create')) self.assertRaises(exception.PolicyNotAuthorized, self.controller._create, @@ -651,12 +651,12 @@ class VolumeTypesManageApiTest(test.TestCase): @mock.patch('cinder.volume.volume_types.update') @mock.patch('cinder.volume.volume_types.get_volume_type') - @mock.patch('cinder.policy.enforce') - def test_update_with_non_admin(self, mock_policy_enforce, mock_get, + @mock.patch('cinder.policy.authorize') + def test_update_with_non_admin(self, mock_policy_authorize, mock_get, mock_update): # allow policy authorized user to update type - mock_policy_enforce.return_value = None + mock_policy_authorize.return_value = None mock_get.return_value = return_volume_types_get_volume_type_updated( DEFAULT_VOLUME_TYPE, is_public=False) name = "vol_type_%s" % DEFAULT_VOLUME_TYPE @@ -681,7 +681,7 @@ class VolumeTypesManageApiTest(test.TestCase): 'is_public': False}) # non policy authorized user fails to update type - mock_policy_enforce.side_effect = ( + mock_policy_authorize.side_effect = ( exception.PolicyNotAuthorized(action='type_update')) self.assertRaises(exception.PolicyNotAuthorized, self.controller._update, diff --git a/cinder/tests/unit/api/contrib/test_volume_type_access.py b/cinder/tests/unit/api/contrib/test_volume_type_access.py index 8f56f070146..40e3acf1f18 100644 --- a/cinder/tests/unit/api/contrib/test_volume_type_access.py +++ b/cinder/tests/unit/api/contrib/test_volume_type_access.py @@ -12,6 +12,7 @@ # under the License. import datetime +import mock from six.moves import http_client import webob @@ -153,11 +154,11 @@ class VolumeTypeAccessTest(test.TestCase): def fake_authorize(context, target=None, action=None): raise exception.PolicyNotAuthorized(action='index') - self.mock_object(type_access, 'authorize', fake_authorize) - - self.assertRaises(exception.PolicyNotAuthorized, - self.type_access_controller.index, - req, fake.PROJECT_ID) + with mock.patch('cinder.context.RequestContext.authorize', + fake_authorize): + self.assertRaises(exception.PolicyNotAuthorized, + self.type_access_controller.index, + req, fake.PROJECT_ID) def test_list_type_with_admin_default_proj1(self): expected = {'volume_types': [{'id': fake.VOLUME_TYPE_ID}, diff --git a/cinder/tests/unit/api/v2/test_types.py b/cinder/tests/unit/api/v2/test_types.py index df50b835fae..527d2c4b8c5 100644 --- a/cinder/tests/unit/api/v2/test_types.py +++ b/cinder/tests/unit/api/v2/test_types.py @@ -20,7 +20,6 @@ from oslo_utils import timeutils import six import webob -import cinder.api.common as common from cinder.api.v2 import types from cinder.api.v2.views import types as views_types from cinder import context @@ -334,9 +333,8 @@ class VolumeTypesApiTest(test.TestCase): self.assertDictEqual(expected_volume_type, output['volume_type']) def test_view_builder_show_qos_specs_id_policy(self): - with mock.patch.object(common, - 'validate_policy', - side_effect=[False, True]): + with mock.patch('cinder.context.RequestContext.authorize', + side_effect=[False, True]): view_builder = views_types.ViewBuilder() now = timeutils.utcnow().isoformat() raw_volume_type = dict( @@ -366,9 +364,8 @@ class VolumeTypesApiTest(test.TestCase): self.assertDictEqual(expected_volume_type, output['volume_type']) def test_view_builder_show_extra_specs_policy(self): - with mock.patch.object(common, - 'validate_policy', - side_effect=[True, False]): + with mock.patch('cinder.context.RequestContext.authorize', + side_effect=[True, False]): view_builder = views_types.ViewBuilder() now = timeutils.utcnow().isoformat() raw_volume_type = dict( @@ -397,9 +394,8 @@ class VolumeTypesApiTest(test.TestCase): ) self.assertDictEqual(expected_volume_type, output['volume_type']) - with mock.patch.object(common, - 'validate_policy', - side_effect=[False, False]): + with mock.patch('cinder.context.RequestContext.authorize', + side_effect=[False, False]): view_builder = views_types.ViewBuilder() now = timeutils.utcnow().isoformat() raw_volume_type = dict( @@ -428,9 +424,8 @@ class VolumeTypesApiTest(test.TestCase): self.assertDictEqual(expected_volume_type, output['volume_type']) def test_view_builder_show_pass_all_policy(self): - with mock.patch.object(common, - 'validate_policy', - side_effect=[True, True]): + with mock.patch('cinder.context.RequestContext.authorize', + side_effect=[True, True]): view_builder = views_types.ViewBuilder() now = timeutils.utcnow().isoformat() raw_volume_type = dict( diff --git a/cinder/tests/unit/attachments/test_attachments_api.py b/cinder/tests/unit/attachments/test_attachments_api.py index 24dd1a7592b..0bcf15b6ded 100644 --- a/cinder/tests/unit/attachments/test_attachments_api.py +++ b/cinder/tests/unit/attachments/test_attachments_api.py @@ -39,8 +39,7 @@ class AttachmentManagerTestCase(test.TestCase): self.context.project_id = self.project_id self.volume_api = volume_api.API() - @mock.patch('cinder.volume.api.check_policy') - def test_attachment_create_no_connector(self, mock_policy): + def test_attachment_create_no_connector(self): """Test attachment_create no connector.""" volume_params = {'status': 'available'} @@ -55,11 +54,9 @@ class AttachmentManagerTestCase(test.TestCase): self.assertEqual(vref.id, aref.volume_id) self.assertEqual({}, aref.connection_info) - @mock.patch('cinder.volume.api.check_policy') @mock.patch('cinder.volume.rpcapi.VolumeAPI.attachment_update') def test_attachment_create_with_connector(self, - mock_rpc_attachment_update, - mock_policy): + mock_rpc_attachment_update): """Test attachment_create with connector.""" volume_params = {'status': 'available'} connection_info = {'fake_key': 'fake_value', @@ -80,11 +77,9 @@ class AttachmentManagerTestCase(test.TestCase): attachment.id) self.assertEqual(connection_info, new_attachment.connection_info) - @mock.patch('cinder.volume.api.check_policy') @mock.patch('cinder.volume.rpcapi.VolumeAPI.attachment_delete') def test_attachment_delete_reserved(self, - mock_rpc_attachment_delete, - mock_policy): + mock_rpc_attachment_delete): """Test attachment_delete with reserved.""" volume_params = {'status': 'available'} @@ -103,14 +98,12 @@ class AttachmentManagerTestCase(test.TestCase): # rpc call mock_rpc_attachment_delete.assert_not_called() - @mock.patch('cinder.volume.api.check_policy') @mock.patch('cinder.volume.rpcapi.VolumeAPI.attachment_delete') @mock.patch('cinder.volume.rpcapi.VolumeAPI.attachment_update') def test_attachment_create_update_and_delete( self, mock_rpc_attachment_update, - mock_rpc_attachment_delete, - mock_policy): + mock_rpc_attachment_delete): """Test attachment_delete.""" volume_params = {'status': 'available'} connection_info = {'fake_key': 'fake_value', @@ -151,8 +144,7 @@ class AttachmentManagerTestCase(test.TestCase): aref.id, mock.ANY) - @mock.patch('cinder.volume.api.check_policy') - def test_additional_attachment_create_no_connector(self, mock_policy): + def test_additional_attachment_create_no_connector(self): """Test attachment_create no connector.""" volume_params = {'status': 'available'} @@ -179,12 +171,10 @@ class AttachmentManagerTestCase(test.TestCase): vref.id) self.assertEqual(2, len(vref.volume_attachment)) - @mock.patch('cinder.volume.api.check_policy') @mock.patch('cinder.volume.rpcapi.VolumeAPI.attachment_update') def test_attachment_create_reserve_delete( self, - mock_rpc_attachment_update, - mock_policy): + mock_rpc_attachment_update): volume_params = {'status': 'available'} connector = { "initiator": "iqn.1993-08.org.debian:01:cad181614cec", @@ -223,8 +213,7 @@ class AttachmentManagerTestCase(test.TestCase): vref.id) self.assertEqual('reserved', vref.status) - @mock.patch('cinder.volume.api.check_policy') - def test_reserve_reserve_delete(self, mock_policy): + def test_reserve_reserve_delete(self): """Test that we keep reserved status across multiple reserves.""" volume_params = {'status': 'available'} diff --git a/cinder/tests/unit/policy.json b/cinder/tests/unit/policy.json index 45f33077902..be9c5a5c070 100644 --- a/cinder/tests/unit/policy.json +++ b/cinder/tests/unit/policy.json @@ -11,8 +11,6 @@ "volume:create_volume_metadata": "", "volume:delete_volume_metadata": "", "volume:update_volume_metadata": "", - "volume:get_volume_admin_metadata": "rule:admin_api", - "volume:update_volume_admin_metadata": "rule:admin_api", "volume:delete": "", "volume:force_delete": "rule:admin_api", "volume:update": "", @@ -34,47 +32,26 @@ "volume:update_snapshot_metadata": "", "volume:extend": "", "volume:extend_attached_volume": "", - "volume:migrate_volume": "rule:admin_api", - "volume:migrate_volume_completion": "rule:admin_api", "volume:update_readonly_flag": "", "volume:retype": "", - "volume:copy_volume_to_image": "", "volume:revert_to_snapshot": "", - "volume_extension:volume_admin_actions:reset_status": "rule:admin_api", - "volume_extension:volume_admin_actions:force_delete": "rule:admin_api", - "volume_extension:volume_admin_actions:force_detach": "rule:admin_api", - "volume_extension:volume_admin_actions:migrate_volume": "rule:admin_api", - "volume_extension:volume_admin_actions:migrate_volume_completion": "rule:admin_api", "volume_extension:volume_actions:upload_image": "", - "volume_extension:volume_actions:upload_public": "rule:admin_api", "volume_extension:types_manage": "", "volume_extension:types_extra_specs:create": "", "volume_extension:types_extra_specs:delete": "", "volume_extension:types_extra_specs:index": "", "volume_extension:types_extra_specs:show": "", "volume_extension:types_extra_specs:update": "", - "volume_extension:access_types_qos_specs_id": "rule:admin_api", - "volume_extension:access_types_extra_specs": "rule:admin_api", "volume_extension:volume_type_access": "", - "volume_extension:volume_type_access:addProjectAccess": "rule:admin_api", - "volume_extension:volume_type_access:removeProjectAccess": "rule:admin_api", - "volume_extension:volume_type_encryption": "rule:admin_api", - "volume_extension:volume_encryption_metadata": "rule:admin_or_owner", "volume_extension:extended_snapshot_attributes": "", "volume_extension:volume_image_metadata": "", - "volume_extension:volume_host_attribute": "rule:admin_api", "volume_extension:volume_tenant_attribute": "rule:admin_api", - "volume_extension:volume_mig_status_attribute": "rule:admin_api", "volume_extension:services:index": "", "volume_extension:services:update" : "rule:admin_api", - "volume_extension:volume_manage": "rule:admin_api", - "volume_extension:volume_unmanage": "rule:admin_api", - "volume_extension:list_manageable": "rule:admin_api", "limits_extension:used_limits": "", "volume:create_transfer": "", - "volume:accept_transfer": "", "volume:delete_transfer": "", "volume:get_transfer": "", "volume:get_all_transfers": "", diff --git a/cinder/transfer/api.py b/cinder/transfer/api.py index cd0dfa186dc..19550eacce0 100644 --- a/cinder/transfer/api.py +++ b/cinder/transfer/api.py @@ -31,6 +31,7 @@ from cinder.db import base from cinder import exception from cinder.i18n import _ from cinder import objects +from cinder.policies import volume_transfer as policy from cinder import quota from cinder import quota_utils from cinder.volume import api as volume_api @@ -59,13 +60,13 @@ class API(base.Base): super(API, self).__init__(db_driver) def get(self, context, transfer_id): - volume_api.check_policy(context, 'get_transfer') + context.authorize(policy.GET_POLICY) rv = self.db.transfer_get(context, transfer_id) return dict(rv) def delete(self, context, transfer_id): """Make the RPC call to delete a volume transfer.""" - volume_api.check_policy(context, 'delete_transfer') + context.authorize(policy.DELETE_POLICY) transfer = self.db.transfer_get(context, transfer_id) volume_ref = self.db.volume_get(context, transfer.volume_id) @@ -79,7 +80,7 @@ class API(base.Base): def get_all(self, context, filters=None): filters = filters or {} - volume_api.check_policy(context, 'get_all_transfers') + context.authorize(policy.GET_ALL_POLICY) if context.is_admin and 'all_tenants' in filters: transfers = self.db.transfer_get_all(context) else: @@ -114,7 +115,7 @@ class API(base.Base): def create(self, context, volume_id, display_name): """Creates an entry in the transfers table.""" - volume_api.check_policy(context, 'create_transfer') + context.authorize(policy.CREATE_POLICY) LOG.info("Generating transfer record for volume %s", volume_id) volume_ref = self.db.volume_get(context, volume_id) if volume_ref['status'] != "available": @@ -151,7 +152,7 @@ class API(base.Base): """Accept a volume that has been offered for transfer.""" # We must use an elevated context to see the volume that is still # owned by the donor. - volume_api.check_policy(context, 'accept_transfer') + context.authorize(policy.ACCEPT_POLICY) transfer = self.db.transfer_get(context.elevated(), transfer_id) crypt_hash = self._get_crypt_hash(transfer['salt'], auth_key) diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 4e97b786c4c..87365c84fda 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -19,7 +19,6 @@ import ast import collections import datetime -import functools from oslo_config import cfg from oslo_log import log as logging @@ -47,7 +46,10 @@ from cinder.policies import attachments as attachment_policy from cinder.policies import services as svr_policy from cinder.policies import snapshot_metadata as s_meta_policy from cinder.policies import snapshots as snapshot_policy -import cinder.policy +from cinder.policies import volume_actions as vol_action_policy +from cinder.policies import volume_metadata as vol_meta_policy +from cinder.policies import volume_transfer as vol_transfer_policy +from cinder.policies import volumes as vol_policy from cinder import quota from cinder import quota_utils from cinder.scheduler import rpcapi as scheduler_rpcapi @@ -93,36 +95,6 @@ QUOTAS = quota.QUOTAS AO_LIST = objects.VolumeAttachmentList -def wrap_check_policy(func): - """Check policy corresponding to the wrapped methods prior to execution - - This decorator requires the first 3 args of the wrapped function - to be (self, context, volume) - """ - @functools.wraps(func) - def wrapped(self, context, target_obj, *args, **kwargs): - check_policy(context, func.__name__, target_obj) - return func(self, context, target_obj, *args, **kwargs) - return wrapped - - -def check_policy(context, action, target_obj=None): - target = { - 'project_id': context.project_id, - 'user_id': context.user_id, - } - - if isinstance(target_obj, objects_base.CinderObject): - # Turn object into dict so target.update can work - target.update( - target_obj.obj_to_primitive()['versioned_object.data'] or {}) - else: - target.update(target_obj or {}) - - _action = 'volume:%s' % action - cinder.policy.enforce(context, _action, target) - - class API(base.Base): """API for interacting with the volume manager.""" @@ -227,7 +199,7 @@ class API(base.Base): cgsnapshot=None, multiattach=False, source_cg=None, group=None, group_snapshot=None, source_group=None): - check_policy(context, 'create_from_image' if image_id else 'create') + context.authorize(vol_policy.CREATE_FROM_IMAGE_POLICY) # Check up front for legacy replication parameters to quick fail if source_replica: @@ -368,10 +340,10 @@ class API(base.Base): self.list_availability_zones(enable_cache=True, refresh_cache=True) - @wrap_check_policy def revert_to_snapshot(self, context, volume, snapshot): """revert a volume to a snapshot""" - + context.authorize(vol_action_policy.REVERT_POLICY, + target_obj=volume) v_res = volume.update_single_status_where( 'reverting', 'available') if not v_res: @@ -388,11 +360,11 @@ class API(base.Base): self.volume_rpcapi.revert_to_snapshot(context, volume, snapshot) - @wrap_check_policy def delete(self, context, volume, force=False, unmanage_only=False, cascade=False): + context.authorize(vol_policy.DELETE_POLICY, target_obj=volume) if context.is_admin and context.project_id != volume.project_id: project_id = volume.project_id else: @@ -522,8 +494,8 @@ class API(base.Base): LOG.info("Delete volume request issued successfully.", resource=volume) - @wrap_check_policy def update(self, context, volume, fields): + context.authorize(vol_policy.UPDATE_POLICY, target_obj=volume) # TODO(karthikp): Making sure volume is always oslo-versioned # If not we convert it at the start of update method. This check # needs to be removed once we have moved to ovo. @@ -547,7 +519,7 @@ class API(base.Base): volume = objects.Volume.get_by_id(context, volume_id) try: - check_policy(context, 'get', volume) + context.authorize(vol_policy.GET_POLICY, target_obj=volume) except exception.PolicyNotAuthorized: # raise VolumeNotFound to avoid providing info about # the existence of an unauthorized volume id @@ -566,7 +538,7 @@ class API(base.Base): def get_all(self, context, marker=None, limit=None, sort_keys=None, sort_dirs=None, filters=None, viewable_admin_meta=False, offset=None): - check_policy(context, 'get_all') + context.authorize(vol_policy.GET_ALL_POLICY) if filters is None: filters = {} @@ -613,7 +585,7 @@ class API(base.Base): return volumes def get_volume_summary(self, context, filters=None): - check_policy(context, 'get_all') + context.authorize(vol_policy.GET_ALL_POLICY) if filters is None: filters = {} @@ -638,7 +610,7 @@ class API(base.Base): return snapshot def get_volume(self, context, volume_id): - check_policy(context, 'get_volume') + context.authorize(vol_policy.GET_POLICY) volume = objects.Volume.get_by_id(context, volume_id) LOG.info("Volume retrieved successfully.", resource=volume) return volume @@ -664,8 +636,8 @@ class API(base.Base): LOG.info("Get all snapshots completed successfully.") return snapshots - @wrap_check_policy def reserve_volume(self, context, volume): + context.authorize(vol_action_policy.RETYPE_POLICY, target_obj=volume) expected = {'multiattach': volume.multiattach, 'status': (('available', 'in-use') if volume.multiattach else 'available')} @@ -683,8 +655,9 @@ class API(base.Base): LOG.info("Reserve volume completed successfully.", resource=volume) - @wrap_check_policy def unreserve_volume(self, context, volume): + context.authorize(vol_action_policy.UNRESERVE_POLICY, + target_obj=volume) expected = {'status': 'attaching'} # Status change depends on whether it has attachments (in-use) or not # (available) @@ -701,8 +674,9 @@ class API(base.Base): LOG.info("Unreserve volume completed successfully.", resource=volume) - @wrap_check_policy def begin_detaching(self, context, volume): + context.authorize(vol_action_policy.BEGIN_DETACHING_POLICY, + target_obj=volume) # If we are in the middle of a volume migration, we don't want the # user to see that the volume is 'detaching'. Having # 'migration_status' set will have the same effect internally. @@ -721,16 +695,18 @@ class API(base.Base): LOG.info("Begin detaching volume completed successfully.", resource=volume) - @wrap_check_policy def roll_detaching(self, context, volume): + context.authorize(vol_action_policy.ROLL_DETACHING_POLICY, + target_obj=volume) volume.conditional_update({'status': 'in-use'}, {'status': 'detaching'}) LOG.info("Roll detaching of volume completed successfully.", resource=volume) - @wrap_check_policy def attach(self, context, volume, instance_uuid, host_name, mountpoint, mode): + context.authorize(vol_action_policy.ATTACH_POLICY, + target_obj=volume) if volume.status == 'maintenance': LOG.info('Unable to attach volume, ' 'because it is in maintenance.', resource=volume) @@ -756,8 +732,9 @@ class API(base.Base): resource=volume) return attach_results - @wrap_check_policy def detach(self, context, volume, attachment_id): + context.authorize(vol_action_policy.DETACH_POLICY, + target_obj=volume) if volume['status'] == 'maintenance': LOG.info('Unable to detach volume, ' 'because it is in maintenance.', resource=volume) @@ -769,8 +746,9 @@ class API(base.Base): resource=volume) return detach_results - @wrap_check_policy def initialize_connection(self, context, volume, connector): + context.authorize(vol_action_policy.INITIALIZE_POLICY, + target_obj=volume) if volume.status == 'maintenance': LOG.info('Unable to initialize the connection for ' 'volume, because it is in ' @@ -785,8 +763,9 @@ class API(base.Base): resource=volume) return init_results - @wrap_check_policy def terminate_connection(self, context, volume, connector, force=False): + context.authorize(vol_action_policy.TERMINATE_POLICY, + target_obj=volume) self.volume_rpcapi.terminate_connection(context, volume, connector, @@ -795,8 +774,9 @@ class API(base.Base): resource=volume) self.unreserve_volume(context, volume) - @wrap_check_policy def accept_transfer(self, context, volume, new_user, new_project): + context.authorize(vol_transfer_policy.ACCEPT_POLICY, + target_obj=volume) if volume['status'] == 'maintenance': LOG.info('Unable to accept transfer for volume, ' 'because it is in maintenance.', resource=volume) @@ -948,7 +928,7 @@ class API(base.Base): return snapshot_list def _create_snapshot_in_db_validate(self, context, volume): - check_policy(context, 'create_snapshot', volume) + context.authorize(snapshot_policy.CREATE_POLICY, target_obj=volume) if volume['status'] == 'maintenance': LOG.info('Unable to create the snapshot for volume, ' @@ -1071,27 +1051,27 @@ class API(base.Base): snapshot.update(fields) snapshot.save() - @wrap_check_policy def get_volume_metadata(self, context, volume): """Get all metadata associated with a volume.""" + context.authorize(vol_meta_policy.GET_POLICY, target_obj=volume) rv = self.db.volume_metadata_get(context, volume['id']) LOG.info("Get volume metadata completed successfully.", resource=volume) return dict(rv) - @wrap_check_policy def create_volume_metadata(self, context, volume, metadata): """Creates volume metadata.""" + context.authorize(vol_meta_policy.CREATE_POLICY, target_obj=volume) db_meta = self._update_volume_metadata(context, volume, metadata) LOG.info("Create volume metadata completed successfully.", resource=volume) return db_meta - @wrap_check_policy def delete_volume_metadata(self, context, volume, key, meta_type=common.METADATA_TYPES.user): """Delete the given metadata item from a volume.""" + context.authorize(vol_meta_policy.DELETE_POLICY, target_obj=volume) if volume.status in ('maintenance', 'uploading'): msg = _('Deleting volume metadata is not allowed for volumes in ' '%s status.') % volume.status @@ -1112,7 +1092,6 @@ class API(base.Base): return self.db.volume_metadata_update(context, volume['id'], metadata, delete, meta_type) - @wrap_check_policy def update_volume_metadata(self, context, volume, metadata, delete=False, meta_type=common.METADATA_TYPES.user): """Updates volume metadata. @@ -1121,6 +1100,7 @@ class API(base.Base): `metadata` argument will be deleted. """ + context.authorize(vol_meta_policy.UPDATE_POLICY, target_obj=volume) db_meta = self._update_volume_metadata(context, volume, metadata, delete, meta_type) @@ -1130,7 +1110,6 @@ class API(base.Base): resource=volume) return db_meta - @wrap_check_policy def get_volume_admin_metadata(self, context, volume): """Get all administration metadata associated with a volume.""" rv = self.db.volume_admin_metadata_get(context, volume['id']) @@ -1138,7 +1117,6 @@ class API(base.Base): resource=volume) return dict(rv) - @wrap_check_policy def update_volume_admin_metadata(self, context, volume, metadata, delete=False, add=True, update=True): """Updates or creates volume administration metadata. @@ -1147,6 +1125,8 @@ class API(base.Base): `metadata` argument will be deleted. """ + context.authorize(vol_meta_policy.UPDATE_ADMIN_METADATA_POLICY, + target_obj=volume) utils.check_metadata_properties(metadata) db_meta = self.db.volume_admin_metadata_update(context, volume.id, metadata, delete, add, @@ -1210,7 +1190,7 @@ class API(base.Base): pass def get_volumes_image_metadata(self, context): - check_policy(context, 'get_volumes_image_metadata') + context.authorize(vol_meta_policy.GET_POLICY) db_data = self.db.volume_glance_metadata_get_all(context) results = collections.defaultdict(dict) for meta_entry in db_data: @@ -1218,8 +1198,8 @@ class API(base.Base): meta_entry['value']}) return results - @wrap_check_policy def get_volume_image_metadata(self, context, volume): + context.authorize(vol_meta_policy.GET_POLICY, target_obj=volume) db_data = self.db.volume_glance_metadata_get(context, volume['id']) LOG.info("Get volume image-metadata completed successfully.", resource=volume) @@ -1234,7 +1214,6 @@ class API(base.Base): meta_entry['value']}) return results - @wrap_check_policy def copy_volume_to_image(self, context, volume, metadata, force): """Create a new image from the specified volume.""" if not CONF.enable_force_upload and force: @@ -1407,22 +1386,25 @@ class API(base.Base): LOG.info("Extend volume request issued successfully.", resource=volume) - @wrap_check_policy def extend(self, context, volume, new_size): + context.authorize(vol_action_policy.EXTEND_POLICY, + target_obj=volume) self._extend(context, volume, new_size, attached=False) # NOTE(tommylikehu): New method is added here so that administrator # can enable/disable this ability by editing the policy file if the # cloud environment doesn't allow this operation. - @wrap_check_policy def extend_attached_volume(self, context, volume, new_size): + context.authorize(vol_action_policy.EXTEND_ATTACHED_POLICY, + target_obj=volume) self._extend(context, volume, new_size, attached=True) - @wrap_check_policy def migrate_volume(self, context, volume, host, cluster_name, force_copy, lock_volume): """Migrate the volume to the specified host or cluster.""" elevated = context.elevated() + context.authorize(vol_action_policy.MIGRATE_POLICY, + target_obj=volume) # If we received a request to migrate to a host # Look for the service - must be up and enabled @@ -1504,8 +1486,9 @@ class API(base.Base): LOG.info("Migrate volume request issued successfully.", resource=volume) - @wrap_check_policy def migrate_volume_completion(self, context, volume, new_volume, error): + context.authorize(vol_action_policy.MIGRATE_COMPLETE_POLICY, + target_obj=volume) if not (volume.migration_status or new_volume.migration_status): # When we're not migrating and haven't hit any errors, we issue # volume attach and detach requests so the volumes don't end in @@ -1543,8 +1526,9 @@ class API(base.Base): return self.volume_rpcapi.migrate_volume_completion(context, volume, new_volume, error) - @wrap_check_policy def update_readonly_flag(self, context, volume, flag): + context.authorize(vol_action_policy.UPDATE_READONLY_POLICY, + target_obj=volume) if volume['status'] != 'available': msg = _('Volume %(vol_id)s status must be available ' 'to update readonly flag, but current status is: ' @@ -1557,9 +1541,9 @@ class API(base.Base): "completed successfully.", resource=volume) - @wrap_check_policy def retype(self, context, volume, new_type, migration_policy=None): """Attempt to modify the type associated with an existing volume.""" + context.authorize(vol_action_policy.RETYPE_POLICY, target_obj=volume) if migration_policy and migration_policy not in ('on-demand', 'never'): msg = _('migration_policy must be \'on-demand\' or \'never\', ' 'passed: %s') % new_type diff --git a/cinder/volume/flows/api/create_volume.py b/cinder/volume/flows/api/create_volume.py index 69e783c2a5c..02dd109f679 100644 --- a/cinder/volume/flows/api/create_volume.py +++ b/cinder/volume/flows/api/create_volume.py @@ -26,7 +26,7 @@ from cinder import flow_utils from cinder.i18n import _ from cinder import objects from cinder.objects import fields -from cinder import policy +from cinder.policies import volumes as policy from cinder import quota from cinder import quota_utils from cinder import utils @@ -420,7 +420,7 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask): utils.check_exclusive_options(snapshot=snapshot, imageRef=image_id, source_volume=source_volume) - policy.enforce_action(context, ACTION) + context.authorize(policy.CREATE_POLICY) # TODO(harlowja): what guarantee is there that the snapshot or source # volume will remain available after we do this initial verification?? diff --git a/etc/cinder/policy.json b/etc/cinder/policy.json index c7ede8d822e..c1f95d4d768 100644 --- a/etc/cinder/policy.json +++ b/etc/cinder/policy.json @@ -1,61 +1,4 @@ { - "volume:create": "", - "volume:create_from_image": "", - "volume:delete": "rule:admin_or_owner", - "volume:force_delete": "rule:admin_api", - "volume:get": "rule:admin_or_owner", - "volume:get_all": "rule:admin_or_owner", - "volume:get_volume_metadata": "rule:admin_or_owner", - "volume:create_volume_metadata": "rule:admin_or_owner", - "volume:delete_volume_metadata": "rule:admin_or_owner", - "volume:update_volume_metadata": "rule:admin_or_owner", - "volume:get_volume_admin_metadata": "rule:admin_api", - "volume:update_volume_admin_metadata": "rule:admin_api", - "volume:extend": "rule:admin_or_owner", - "volume:extend_attached_volume": "rule:admin_or_owner", - "volume:update_readonly_flag": "rule:admin_or_owner", - "volume:retype": "rule:admin_or_owner", - "volume:update": "rule:admin_or_owner", - "volume:revert_to_snapshot": "rule:admin_or_owner", - - "volume_extension:types_manage": "rule:admin_api", - "volume_extension:types_extra_specs:create": "rule:admin_api", - "volume_extension:types_extra_specs:delete": "rule:admin_api", - "volume_extension:types_extra_specs:index": "rule:admin_api", - "volume_extension:types_extra_specs:show": "rule:admin_api", - "volume_extension:types_extra_specs:update": "rule:admin_api", - "volume_extension:access_types_qos_specs_id": "rule:admin_api", - "volume_extension:access_types_extra_specs": "rule:admin_api", - "volume_extension:volume_type_access": "rule:admin_or_owner", - "volume_extension:volume_type_access:addProjectAccess": "rule:admin_api", - "volume_extension:volume_type_access:removeProjectAccess": "rule:admin_api", - "volume_extension:volume_type_encryption": "rule:admin_api", - "volume_extension:volume_encryption_metadata": "rule:admin_or_owner", - "volume_extension:volume_image_metadata": "rule:admin_or_owner", - - "volume_extension:volume_admin_actions:reset_status": "rule:admin_api", - "volume_extension:volume_admin_actions:force_delete": "rule:admin_api", - "volume_extension:volume_admin_actions:force_detach": "rule:admin_api", - "volume_extension:backup_admin_actions:force_delete": "rule:admin_api", - "volume_extension:volume_admin_actions:migrate_volume": "rule:admin_api", - "volume_extension:volume_admin_actions:migrate_volume_completion": "rule:admin_api", - - "volume_extension:volume_actions:upload_public": "rule:admin_api", - "volume_extension:volume_actions:upload_image": "rule:admin_or_owner", - - "volume_extension:volume_host_attribute": "rule:admin_api", - "volume_extension:volume_tenant_attribute": "rule:admin_or_owner", - "volume_extension:volume_mig_status_attribute": "rule:admin_api", - - "volume_extension:volume_manage": "rule:admin_api", - "volume_extension:volume_unmanage": "rule:admin_api", - "volume_extension:list_manageable": "rule:admin_api", - - "volume:create_transfer": "rule:admin_or_owner", - "volume:accept_transfer": "", - "volume:delete_transfer": "rule:admin_or_owner", - "volume:get_transfer": "rule:admin_or_owner", - "volume:get_all_transfers": "rule:admin_or_owner", "consistencygroup:create" : "group:nobody", "consistencygroup:delete": "group:nobody", @@ -66,6 +9,6 @@ "consistencygroup:create_cgsnapshot" : "group:nobody", "consistencygroup:delete_cgsnapshot": "group:nobody", "consistencygroup:get_cgsnapshot": "group:nobody", - "consistencygroup:get_all_cgsnapshots": "group:nobody", + "consistencygroup:get_all_cgsnapshots": "group:nobody" }