"Fossies" - the Fresh Open Source Software Archive

Member "neutron-14.0.3/neutron/objects/rbac_db.py" (22 Oct 2019, 14171 Bytes) of package /linux/misc/openstack/neutron-14.0.3.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "rbac_db.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 14.0.2_vs_14.0.3.

    1 # Copyright 2016 Red Hat, Inc.
    2 # All Rights Reserved.
    3 #
    4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    5 #    not use this file except in compliance with the License. You may obtain
    6 #    a copy of the License at
    7 #
    8 #         http://www.apache.org/licenses/LICENSE-2.0
    9 #
   10 #    Unless required by applicable law or agreed to in writing, software
   11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   13 #    License for the specific language governing permissions and limitations
   14 #    under the License.
   15 import abc
   16 import itertools
   17 
   18 from neutron_lib.callbacks import events
   19 from neutron_lib.callbacks import registry
   20 from neutron_lib.callbacks import resources
   21 from neutron_lib import exceptions
   22 from six import add_metaclass
   23 from six import with_metaclass
   24 from sqlalchemy import and_
   25 
   26 from neutron._i18n import _
   27 from neutron.db import _utils as db_utils
   28 from neutron.db import rbac_db_mixin
   29 from neutron.db import rbac_db_models as models
   30 from neutron.extensions import rbac as ext_rbac
   31 from neutron.objects import base
   32 from neutron.objects.db import api as obj_db_api
   33 
   34 
   35 @add_metaclass(abc.ABCMeta)
   36 class RbacNeutronDbObjectMixin(rbac_db_mixin.RbacPluginMixin,
   37                                base.NeutronDbObject):
   38 
   39     rbac_db_cls = None
   40 
   41     @classmethod
   42     @abc.abstractmethod
   43     def get_bound_tenant_ids(cls, context, obj_id):
   44         """Returns ids of all tenants depending on this db object.
   45 
   46         Has to be implemented by classes using RbacNeutronMetaclass.
   47         The tenants are the ones that need the sharing or 'visibility' of the
   48         object to them. E.g: for QosPolicy that would be the tenants using the
   49         Networks and Ports with the shared QosPolicy applied to them.
   50 
   51         :returns: set -- a set of tenants' ids dependent on this object.
   52         """
   53 
   54     @staticmethod
   55     def is_network_shared(context, rbac_entries):
   56         # NOTE(korzen) this method is copied from db_base_plugin_common.
   57         # The shared attribute for a network now reflects if the network
   58         # is shared to the calling tenant via an RBAC entry.
   59         matches = ('*',) + ((context.tenant_id,) if context else ())
   60         for entry in rbac_entries:
   61             if (entry.action == models.ACCESS_SHARED and
   62                     entry.target_tenant in matches):
   63                 return True
   64         return False
   65 
   66     @staticmethod
   67     def get_shared_with_tenant(context, rbac_db_cls, obj_id, tenant_id):
   68         # NOTE(korzen) This method enables to query within already started
   69         # session
   70         rbac_db_model = rbac_db_cls.db_model
   71         return (db_utils.model_query(context, rbac_db_model).filter(
   72                 and_(rbac_db_model.object_id == obj_id,
   73                      rbac_db_model.action == models.ACCESS_SHARED,
   74                      rbac_db_model.target_tenant.in_(
   75                          ['*', tenant_id]))).count() != 0)
   76 
   77     @classmethod
   78     def is_shared_with_tenant(cls, context, obj_id, tenant_id):
   79         ctx = context.elevated()
   80         with cls.db_context_reader(ctx):
   81             return cls.get_shared_with_tenant(ctx, cls.rbac_db_cls,
   82                                               obj_id, tenant_id)
   83 
   84     @classmethod
   85     def is_accessible(cls, context, db_obj):
   86         return (super(
   87             RbacNeutronDbObjectMixin, cls).is_accessible(context, db_obj) or
   88                 cls.is_shared_with_tenant(context, db_obj.id,
   89                                           context.tenant_id))
   90 
   91     @classmethod
   92     def _get_db_obj_rbac_entries(cls, context, rbac_obj_id, rbac_action):
   93         rbac_db_model = cls.rbac_db_cls.db_model
   94         return db_utils.model_query(context, rbac_db_model).filter(
   95             and_(rbac_db_model.object_id == rbac_obj_id,
   96                  rbac_db_model.action == rbac_action))
   97 
   98     @classmethod
   99     def _get_tenants_with_shared_access_to_db_obj(cls, context, obj_id):
  100         rbac_db_model = cls.rbac_db_cls.db_model
  101         return set(itertools.chain.from_iterable(context.session.query(
  102             rbac_db_model.target_tenant).filter(
  103             and_(rbac_db_model.object_id == obj_id,
  104                  rbac_db_model.action == models.ACCESS_SHARED,
  105                  rbac_db_model.target_tenant != '*'))))
  106 
  107     @classmethod
  108     def _validate_rbac_policy_delete(cls, context, obj_id, target_tenant):
  109         ctx_admin = context.elevated()
  110         rb_model = cls.rbac_db_cls.db_model
  111         bound_tenant_ids = cls.get_bound_tenant_ids(ctx_admin, obj_id)
  112         db_obj_sharing_entries = cls._get_db_obj_rbac_entries(
  113             ctx_admin, obj_id, models.ACCESS_SHARED)
  114 
  115         def raise_policy_in_use():
  116             raise ext_rbac.RbacPolicyInUse(
  117                 object_id=obj_id,
  118                 details='tenant_id={}'.format(target_tenant))
  119 
  120         if target_tenant != '*':
  121             # if there is a wildcard rule, we can return early because it
  122             # shares the object globally
  123             wildcard_sharing_entries = db_obj_sharing_entries.filter(
  124                 rb_model.target_tenant == '*')
  125             if wildcard_sharing_entries.count():
  126                 return
  127             if target_tenant in bound_tenant_ids:
  128                 raise_policy_in_use()
  129             return
  130 
  131         # for the wildcard we need to query all of the rbac entries to
  132         # see if any allow the object sharing
  133         other_target_tenants = cls._get_tenants_with_shared_access_to_db_obj(
  134                 ctx_admin, obj_id)
  135         if not bound_tenant_ids.issubset(other_target_tenants):
  136             raise_policy_in_use()
  137 
  138     @classmethod
  139     def validate_rbac_policy_delete(cls, resource, event, trigger, context,
  140                                     object_type, policy, **kwargs):
  141         """Callback to handle RBAC_POLICY, BEFORE_DELETE callback.
  142 
  143         :raises: RbacPolicyInUse -- in case the policy is in use.
  144         """
  145         if policy['action'] != models.ACCESS_SHARED:
  146             return
  147         target_tenant = policy['target_tenant']
  148         db_obj = obj_db_api.get_object(
  149             cls, context.elevated(), id=policy['object_id'])
  150         if db_obj.tenant_id == target_tenant:
  151             return
  152         cls._validate_rbac_policy_delete(context=context,
  153                                          obj_id=policy['object_id'],
  154                                          target_tenant=target_tenant)
  155 
  156     @classmethod
  157     def validate_rbac_policy_update(cls, resource, event, trigger, context,
  158                                     object_type, policy, **kwargs):
  159         """Callback to handle RBAC_POLICY, BEFORE_UPDATE callback.
  160 
  161         :raises: RbacPolicyInUse -- in case the update is forbidden.
  162         """
  163         prev_tenant = policy['target_tenant']
  164         new_tenant = kwargs['policy_update']['target_tenant']
  165         if prev_tenant == new_tenant:
  166             return
  167         if new_tenant != '*':
  168             return cls.validate_rbac_policy_delete(
  169                 resource, event, trigger, context, object_type, policy)
  170 
  171     @classmethod
  172     def validate_rbac_policy_change(cls, resource, event, trigger, context,
  173                                     object_type, policy, **kwargs):
  174         """Callback to validate RBAC_POLICY changes.
  175 
  176         This is the dispatching function for create, update and delete
  177         callbacks. On creation and update, verify that the creator is an admin
  178         or owns the resource being shared.
  179         """
  180         # TODO(hdaniel): As this code was shamelessly stolen from
  181         # NeutronDbPluginV2.validate_network_rbac_policy_change(), those pieces
  182         # should be synced and contain the same bugs, until Network RBAC logic
  183         # (hopefully) melded with this one.
  184         if object_type != cls.rbac_db_cls.db_model.object_type:
  185             return
  186         db_obj = obj_db_api.get_object(
  187             cls, context.elevated(), id=policy['object_id'])
  188         if event in (events.BEFORE_CREATE, events.BEFORE_UPDATE):
  189             if (not context.is_admin and
  190                     db_obj['tenant_id'] != context.tenant_id):
  191                 msg = _("Only admins can manipulate policies on objects "
  192                         "they do not own")
  193                 raise exceptions.InvalidInput(error_message=msg)
  194         callback_map = {events.BEFORE_UPDATE: cls.validate_rbac_policy_update,
  195                         events.BEFORE_DELETE: cls.validate_rbac_policy_delete}
  196         if event in callback_map:
  197             return callback_map[event](resource, event, trigger, context,
  198                                        object_type, policy, **kwargs)
  199 
  200     def attach_rbac(self, obj_id, project_id, target_tenant='*'):
  201         obj_type = self.rbac_db_cls.db_model.object_type
  202         rbac_policy = {'rbac_policy': {'object_id': obj_id,
  203                                        'target_tenant': target_tenant,
  204                                        'project_id': project_id,
  205                                        'object_type': obj_type,
  206                                        'action': models.ACCESS_SHARED}}
  207         return self.create_rbac_policy(self.obj_context, rbac_policy)
  208 
  209     def update_shared(self, is_shared_new, obj_id):
  210         admin_context = self.obj_context.elevated()
  211         shared_prev = obj_db_api.get_object(self.rbac_db_cls, admin_context,
  212                                             object_id=obj_id,
  213                                             target_tenant='*',
  214                                             action=models.ACCESS_SHARED)
  215         is_shared_prev = bool(shared_prev)
  216         if is_shared_prev == is_shared_new:
  217             return
  218 
  219         # 'shared' goes False -> True
  220         if not is_shared_prev and is_shared_new:
  221             self.attach_rbac(obj_id, self.obj_context.tenant_id)
  222             return
  223 
  224         # 'shared' goes True -> False is actually an attempt to delete
  225         # rbac rule for sharing obj_id with target_tenant = '*'
  226         self._validate_rbac_policy_delete(self.obj_context, obj_id, '*')
  227         return self.obj_context.session.delete(shared_prev)
  228 
  229 
  230 def _update_post(self, obj_changes):
  231     if "shared" in obj_changes:
  232         self.update_shared(self.shared, self.id)
  233 
  234 
  235 def _update_hook(self, update_orig):
  236     with self.db_context_writer(self.obj_context):
  237         # NOTE(slaweq): copy of object changes is required to pass it later to
  238         # _update_post method because update() will reset all those changes
  239         obj_changes = self.obj_get_changes()
  240         update_orig(self)
  241         _update_post(self, obj_changes)
  242 
  243 
  244 def _create_post(self):
  245     if self.shared:
  246         self.attach_rbac(self.id, self.project_id)
  247 
  248 
  249 def _create_hook(self, orig_create):
  250     with self.db_context_writer(self.obj_context):
  251         orig_create(self)
  252         _create_post(self)
  253 
  254 
  255 def _to_dict_hook(self, to_dict_orig):
  256     dct = to_dict_orig(self)
  257     if self.obj_context:
  258         dct['shared'] = self.is_shared_with_tenant(self.obj_context,
  259                                                    self.id,
  260                                                    self.obj_context.tenant_id)
  261     else:
  262         # most OVO objects on an agent will not have a context set on the
  263         # object because they will be generated from obj_from_primitive.
  264         dct['shared'] = False
  265     return dct
  266 
  267 
  268 class RbacNeutronMetaclass(type):
  269     """Adds support for RBAC in NeutronDbObjects.
  270 
  271     Injects code for CRUD operations and modifies existing ops to do so.
  272     """
  273 
  274     @classmethod
  275     def _get_attribute(cls, attribute_name, bases):
  276         for b in bases:
  277             attribute = getattr(b, attribute_name, None)
  278             if attribute:
  279                 return attribute
  280 
  281     @classmethod
  282     def get_attribute(cls, attribute_name, bases, dct):
  283         return (dct.get(attribute_name, None) or
  284                 cls._get_attribute(attribute_name, bases))
  285 
  286     @classmethod
  287     def update_synthetic_fields(cls, bases, dct):
  288         if not dct.get('synthetic_fields', None):
  289             synthetic_attr = cls.get_attribute('synthetic_fields', bases, dct)
  290             dct['synthetic_fields'] = synthetic_attr or []
  291         if 'shared' in dct['synthetic_fields']:
  292             raise exceptions.ObjectActionError(
  293                 action=_('shared attribute switching to synthetic'),
  294                 reason=_('already a synthetic attribute'))
  295         dct['synthetic_fields'].append('shared')
  296 
  297     @staticmethod
  298     def subscribe_to_rbac_events(class_instance):
  299         for e in (events.BEFORE_CREATE, events.BEFORE_UPDATE,
  300                   events.BEFORE_DELETE):
  301             registry.subscribe(class_instance.validate_rbac_policy_change,
  302                                resources.RBAC_POLICY, e)
  303 
  304     @staticmethod
  305     def validate_existing_attrs(cls_name, dct):
  306         if 'shared' not in dct['fields']:
  307             raise KeyError(_('No shared key in %s fields') % cls_name)
  308         if 'rbac_db_cls' not in dct:
  309             raise AttributeError(_('rbac_db_cls not found in %s') % cls_name)
  310 
  311     @staticmethod
  312     def get_replaced_method(orig_method, new_method):
  313         def func(self):
  314             return new_method(self, orig_method)
  315         return func
  316 
  317     @classmethod
  318     def replace_class_methods_with_hooks(cls, bases, dct):
  319         methods_replacement_map = {'create': _create_hook,
  320                                    'update': _update_hook,
  321                                    'to_dict': _to_dict_hook}
  322         for orig_method_name, new_method in methods_replacement_map.items():
  323             orig_method = cls.get_attribute(orig_method_name, bases, dct)
  324             hook_method = cls.get_replaced_method(orig_method,
  325                                                   new_method)
  326             dct[orig_method_name] = hook_method
  327 
  328     def __new__(cls, name, bases, dct):
  329         cls.validate_existing_attrs(name, dct)
  330         cls.update_synthetic_fields(bases, dct)
  331         cls.replace_class_methods_with_hooks(bases, dct)
  332         klass = type(name, (RbacNeutronDbObjectMixin,) + bases, dct)
  333         klass.add_extra_filter_name('shared')
  334         cls.subscribe_to_rbac_events(klass)
  335 
  336         return klass
  337 
  338 
  339 NeutronRbacObject = with_metaclass(RbacNeutronMetaclass, base.NeutronDbObject)