466 lines
17 KiB
Python
466 lines
17 KiB
Python
# Copyright 2021 Red Hat, Inc.
|
|
# 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_config import cfg
|
|
from oslo_log import log as logging
|
|
import webob.exc
|
|
|
|
from glance.api import policy
|
|
from glance.common import exception
|
|
from glance.i18n import _
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
|
|
|
|
# TODO(danms): Remove this once secure RBAC is fully implemented and
|
|
# used instead of legacy policy checks.
|
|
def check_is_image_mutable(context, image):
|
|
"""Replicate the DB-layer admin-or-owner check for the API.
|
|
|
|
Much of the API code depends on hard-coded admin-or-owner
|
|
enforcement in the DB or authorization layer, as the policy layer
|
|
is largely a no-op by default. During blueprint policy-refactor,
|
|
we are trying to remove as much of that as possible, but in
|
|
certain places we need to do that (if secure_rbac is not
|
|
enabled). This transitional helper provides a way to do that
|
|
enforcement where necessary.
|
|
|
|
:param context: A RequestContext
|
|
:param image: An ImageProxy
|
|
:raises: exception.Forbidden if the context is not the owner or an admin
|
|
"""
|
|
# Is admin == image mutable
|
|
if context.is_admin:
|
|
return
|
|
|
|
# No owner == image not mutable
|
|
# Image only mutable by its owner
|
|
if (image.owner is None or context.owner is None or
|
|
image.owner != context.owner):
|
|
raise exception.Forbidden(_('You do not own this image'))
|
|
|
|
|
|
def check_admin_or_same_owner(context, properties):
|
|
"""Check that legacy behavior on create with owner is preserved.
|
|
|
|
Legacy behavior requires a static check that owner is not
|
|
inconsistent with the context, unless the caller is an
|
|
admin. Enforce that here, if needed.
|
|
|
|
:param context: A RequestContext
|
|
:param properties: The properties being used to create the image, which may
|
|
contain an owner
|
|
:raises: exception.Forbidden if the context is not an admin and owner is
|
|
set to something other than the context's project
|
|
"""
|
|
if context.is_admin:
|
|
return
|
|
|
|
if context.project_id != properties.get('owner', context.project_id):
|
|
msg = _("You are not permitted to create images "
|
|
"owned by '%s'.")
|
|
raise exception.Forbidden(msg % properties['owner'])
|
|
|
|
|
|
class APIPolicyBase(object):
|
|
def __init__(self, context, target=None, enforcer=None):
|
|
self._context = context
|
|
self._target = target or {}
|
|
self.enforcer = enforcer or policy.Enforcer()
|
|
|
|
def _enforce(self, rule_name):
|
|
try:
|
|
self.enforcer.enforce(self._context, rule_name, self._target)
|
|
except exception.Forbidden as e:
|
|
raise webob.exc.HTTPForbidden(explanation=str(e))
|
|
|
|
def check(self, name, *args):
|
|
"""Perform a soft check of a named policy.
|
|
|
|
This is used when you need to check if a policy is allowed for the
|
|
given resource, without needing to catch an exception. If the policy
|
|
check requires args, those are accepted here as well.
|
|
|
|
:param name: Policy name to check
|
|
:returns: bool indicating if the policy is allowed.
|
|
"""
|
|
try:
|
|
getattr(self, name)(*args)
|
|
return True
|
|
except webob.exc.HTTPForbidden:
|
|
return False
|
|
|
|
|
|
class CacheImageAPIPolicy(APIPolicyBase):
|
|
def __init__(self, context, image=None, policy_str=None,
|
|
target=None, enforcer=None):
|
|
self._context = context
|
|
target = {}
|
|
self._image = image
|
|
if self._image:
|
|
target = policy.ImageTarget(self._image)
|
|
|
|
self._target = target
|
|
self.enforcer = enforcer or policy.Enforcer()
|
|
self.policy_str = policy_str
|
|
super(CacheImageAPIPolicy, self).__init__(context, target, enforcer)
|
|
|
|
def manage_image_cache(self):
|
|
self._enforce(self.policy_str)
|
|
|
|
|
|
class ImageAPIPolicy(APIPolicyBase):
|
|
def __init__(self, context, image, enforcer=None):
|
|
"""Image API policy module.
|
|
|
|
:param context: The RequestContext
|
|
:param image: The ImageProxy object in question, or a dict of image
|
|
properties if no image is yet created or needed for
|
|
authorization context.
|
|
:param enforcer: The policy.Enforcer object to use for enforcement
|
|
operations. If not provided (or None), the default
|
|
enforcer will be selected.
|
|
"""
|
|
self._image = image
|
|
if not self.is_created:
|
|
# NOTE(danms): If we are being called with a dict of image
|
|
# properties then we are testing policies that involve
|
|
# creating an image or other image-related resources but
|
|
# without a specific image for context. The target is a
|
|
# dict of proposed image properties, similar to the
|
|
# dict-like interface the ImageTarget provides over
|
|
# a real Image object, with specific keys.
|
|
target = {'project_id': image.get('owner', context.project_id),
|
|
'owner': image.get('owner', context.project_id),
|
|
'visibility': image.get('visibility', 'private')}
|
|
else:
|
|
target = policy.ImageTarget(image)
|
|
super(ImageAPIPolicy, self).__init__(context, target, enforcer)
|
|
|
|
@property
|
|
def is_created(self):
|
|
"""Signal whether the image actually exists or not.
|
|
|
|
False if the image is only being proposed by a create operation,
|
|
True if it has already been created.
|
|
"""
|
|
return not isinstance(self._image, dict)
|
|
|
|
def _enforce(self, rule_name):
|
|
"""Translate Forbidden->NotFound for images."""
|
|
try:
|
|
super(ImageAPIPolicy, self)._enforce(rule_name)
|
|
except webob.exc.HTTPForbidden:
|
|
# If we are checking image policy before creating an
|
|
# image, or without a specific image for context, then we
|
|
# do not need to potentially hide the presence of anything
|
|
# based on visibility, so re-raise immediately.
|
|
if not self.is_created:
|
|
raise
|
|
|
|
# If we are checking get_image, then Forbidden means the
|
|
# user cannot see this image, so raise NotFound. If we are
|
|
# checking anything else and get Forbidden, then raise
|
|
# NotFound in that case as well to avoid exposing images
|
|
# the user can not see, while preserving the Forbidden
|
|
# behavior for the ones they can see.
|
|
if rule_name == 'get_image' or not self.check('get_image'):
|
|
raise webob.exc.HTTPNotFound()
|
|
raise
|
|
|
|
def check(self, name, *args):
|
|
try:
|
|
return super(ImageAPIPolicy, self).check(name, *args)
|
|
except webob.exc.HTTPNotFound:
|
|
# NOTE(danms): Since our _enforce can raise NotFound, that
|
|
# too means a False check response.
|
|
return False
|
|
|
|
def _enforce_visibility(self, visibility):
|
|
# NOTE(danms): Use the existing enforcement routine for now,
|
|
# which shows that we're enforcing the same behavior. In the
|
|
# future, that should probably be moved here.
|
|
try:
|
|
policy._enforce_image_visibility(self.enforcer, self._context,
|
|
visibility, self._target)
|
|
except exception.Forbidden as e:
|
|
raise webob.exc.HTTPForbidden(explanation=str(e))
|
|
|
|
def update_property(self, name, value=None):
|
|
if name == 'visibility':
|
|
# NOTE(danms): Visibility changes have their own policy,
|
|
# so check that first, followed by the general
|
|
# modify_image policy below.
|
|
self._enforce_visibility(value)
|
|
self.modify_image()
|
|
|
|
def update_locations(self):
|
|
self._enforce('set_image_location')
|
|
|
|
def delete_locations(self):
|
|
self._enforce('delete_image_location')
|
|
# TODO(danms): Remove this legacy fallback when secure RBAC
|
|
# replaces the legacy policy.
|
|
if not CONF.enforce_secure_rbac:
|
|
check_is_image_mutable(self._context, self._image)
|
|
|
|
def get_image_location(self):
|
|
self._enforce('get_image_location')
|
|
|
|
def add_image(self):
|
|
try:
|
|
self._enforce('add_image')
|
|
except webob.exc.HTTPForbidden:
|
|
# NOTE(danms): If we fail add_image because the owner is
|
|
# different, alter the message to be informative and
|
|
# in-line with the current message users have been getting
|
|
# in the past.
|
|
if self._target['owner'] != self._context.project_id:
|
|
msg = _("You are not permitted to create images "
|
|
"owned by '%s'" % self._target['owner'])
|
|
raise webob.exc.HTTPForbidden(msg)
|
|
else:
|
|
raise
|
|
if 'visibility' in self._target:
|
|
self._enforce_visibility(self._target['visibility'])
|
|
if not CONF.enforce_secure_rbac:
|
|
check_admin_or_same_owner(self._context, self._target)
|
|
|
|
def get_image(self):
|
|
self._enforce('get_image')
|
|
|
|
def get_images(self):
|
|
self._enforce('get_images')
|
|
|
|
def delete_image(self):
|
|
self._enforce('delete_image')
|
|
# TODO(danms): Remove this legacy fallback when secure RBAC
|
|
# replaces the legacy policy.
|
|
if not CONF.enforce_secure_rbac:
|
|
check_is_image_mutable(self._context, self._image)
|
|
|
|
def upload_image(self):
|
|
self._enforce('upload_image')
|
|
# TODO(danms): Remove this legacy fallback when secure RBAC
|
|
# replaces the legacy policy.
|
|
if not CONF.enforce_secure_rbac:
|
|
check_is_image_mutable(self._context, self._image)
|
|
|
|
def download_image(self):
|
|
self._enforce('download_image')
|
|
|
|
def modify_image(self):
|
|
self._enforce('modify_image')
|
|
# TODO(danms): Remove this legacy fallback when secure RBAC
|
|
# replaces the legacy policy.
|
|
if not CONF.enforce_secure_rbac:
|
|
check_is_image_mutable(self._context, self._image)
|
|
|
|
def deactivate_image(self):
|
|
self._enforce('deactivate')
|
|
# TODO(danms): Remove this legacy fallback when secure RBAC
|
|
# replaces the legacy policy.
|
|
if not CONF.enforce_secure_rbac:
|
|
check_is_image_mutable(self._context, self._image)
|
|
|
|
def reactivate_image(self):
|
|
self._enforce('reactivate')
|
|
# TODO(danms): Remove this legacy fallback when secure RBAC
|
|
# replaces the legacy policy.
|
|
if not CONF.enforce_secure_rbac:
|
|
check_is_image_mutable(self._context, self._image)
|
|
|
|
def copy_image(self):
|
|
self._enforce('copy_image')
|
|
|
|
|
|
class MetadefAPIPolicy(APIPolicyBase):
|
|
def __init__(self, context, md_resource=None, target=None, enforcer=None):
|
|
self._context = context
|
|
self._md_resource = md_resource
|
|
if not target:
|
|
self._target = self._build_target()
|
|
else:
|
|
self._target = target
|
|
self.enforcer = enforcer or policy.Enforcer()
|
|
super(MetadefAPIPolicy, self).__init__(context, target=self._target,
|
|
enforcer=self.enforcer)
|
|
|
|
def _build_target(self):
|
|
target = {
|
|
"project_id": self._context.project_id
|
|
}
|
|
if self._md_resource:
|
|
target['project_id'] = self._md_resource.owner
|
|
target['visibility'] = self._md_resource.visibility
|
|
|
|
return target
|
|
|
|
def _enforce(self, rule_name):
|
|
"""Translate Forbidden->NotFound for images."""
|
|
try:
|
|
super(MetadefAPIPolicy, self)._enforce(rule_name)
|
|
except webob.exc.HTTPForbidden:
|
|
# If we are checking get_metadef_namespace, then Forbidden means
|
|
# the user cannot see this namespace, so raise NotFound. If we are
|
|
# checking anything else and get Forbidden, then raise
|
|
# NotFound in that case as well to avoid exposing namespaces
|
|
# the user can not see, while preserving the Forbidden
|
|
# behavior for the ones they can see.
|
|
if rule_name == 'get_metadef_namespace' or not self.check(
|
|
'get_metadef_namespace'):
|
|
raise webob.exc.HTTPNotFound()
|
|
raise
|
|
|
|
def check(self, name, *args):
|
|
try:
|
|
return super(MetadefAPIPolicy, self).check(name, *args)
|
|
except webob.exc.HTTPNotFound:
|
|
# NOTE(danms): Since our _enforce can raise NotFound, that
|
|
# too means a False check response.
|
|
return False
|
|
|
|
def get_metadef_namespace(self):
|
|
self._enforce('get_metadef_namespace')
|
|
|
|
def get_metadef_namespaces(self):
|
|
self._enforce('get_metadef_namespaces')
|
|
|
|
def add_metadef_namespace(self):
|
|
self._enforce('add_metadef_namespace')
|
|
|
|
def modify_metadef_namespace(self):
|
|
self._enforce('modify_metadef_namespace')
|
|
|
|
def delete_metadef_namespace(self):
|
|
self._enforce('delete_metadef_namespace')
|
|
|
|
def get_metadef_objects(self):
|
|
self._enforce('get_metadef_objects')
|
|
|
|
def add_metadef_object(self):
|
|
self._enforce('add_metadef_object')
|
|
|
|
def get_metadef_object(self):
|
|
self._enforce('get_metadef_object')
|
|
|
|
def modify_metadef_object(self):
|
|
self._enforce('modify_metadef_object')
|
|
|
|
def delete_metadef_object(self):
|
|
self._enforce('delete_metadef_object')
|
|
|
|
def add_metadef_tag(self):
|
|
self._enforce('add_metadef_tag')
|
|
|
|
def get_metadef_tags(self):
|
|
self._enforce('get_metadef_tags')
|
|
|
|
def add_metadef_tags(self):
|
|
self._enforce('add_metadef_tags')
|
|
|
|
def get_metadef_tag(self):
|
|
self._enforce('get_metadef_tag')
|
|
|
|
def modify_metadef_tag(self):
|
|
self._enforce('modify_metadef_tag')
|
|
|
|
def delete_metadef_tag(self):
|
|
self._enforce('delete_metadef_tag')
|
|
|
|
def delete_metadef_tags(self):
|
|
self._enforce('delete_metadef_tags')
|
|
|
|
def add_metadef_property(self):
|
|
self._enforce('add_metadef_property')
|
|
|
|
def get_metadef_properties(self):
|
|
self._enforce('get_metadef_properties')
|
|
|
|
def remove_metadef_property(self):
|
|
self._enforce('remove_metadef_property')
|
|
|
|
def get_metadef_property(self):
|
|
self._enforce('get_metadef_property')
|
|
|
|
def modify_metadef_property(self):
|
|
self._enforce('modify_metadef_property')
|
|
|
|
def add_metadef_resource_type_association(self):
|
|
self._enforce('add_metadef_resource_type_association')
|
|
|
|
def list_metadef_resource_types(self):
|
|
self._enforce('list_metadef_resource_types')
|
|
|
|
def get_metadef_resource_type(self):
|
|
self._enforce('get_metadef_resource_type')
|
|
|
|
def remove_metadef_resource_type_association(self):
|
|
self._enforce('remove_metadef_resource_type_association')
|
|
|
|
|
|
class MemberAPIPolicy(APIPolicyBase):
|
|
def __init__(self, context, image, target=None, enforcer=None):
|
|
self._context = context
|
|
self._image = image
|
|
if not target:
|
|
self._target = self._build_target()
|
|
|
|
self.enforcer = enforcer or policy.Enforcer()
|
|
super(MemberAPIPolicy, self).__init__(context, target=self._target,
|
|
enforcer=self.enforcer)
|
|
|
|
def _build_target(self):
|
|
target = {
|
|
"project_id": self._context.project_id
|
|
}
|
|
if self._image:
|
|
target = policy.ImageTarget(self._image)
|
|
|
|
return target
|
|
|
|
def _enforce(self, rule_name):
|
|
ImageAPIPolicy(self._context, self._image,
|
|
enforcer=self.enforcer).get_image()
|
|
super(MemberAPIPolicy, self)._enforce(rule_name)
|
|
|
|
def get_members(self):
|
|
self._enforce("get_members")
|
|
|
|
def get_member(self):
|
|
self._enforce("get_member")
|
|
|
|
def delete_member(self):
|
|
self._enforce("delete_member")
|
|
|
|
def modify_member(self):
|
|
self._enforce("modify_member")
|
|
|
|
def add_member(self):
|
|
self._enforce("add_member")
|
|
|
|
|
|
class TasksAPIPolicy(APIPolicyBase):
|
|
def __init__(self, context, target=None, enforcer=None):
|
|
self._context = context
|
|
self._target = target or {}
|
|
self.enforcer = enforcer or policy.Enforcer()
|
|
super(TasksAPIPolicy, self).__init__(context, target=self._target,
|
|
enforcer=self.enforcer)
|
|
|
|
def tasks_api_access(self):
|
|
self._enforce('tasks_api_access')
|