Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
Cloud:OpenStack:Grizzly
openstack-quantum
bug-869570_CVE-2014-0056-stable-grizzly.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File bug-869570_CVE-2014-0056-stable-grizzly.patch of Package openstack-quantum
commit 13a5cd68bc20dfceec4921033784ca428d05d08c Author: Aaron Rosen <aaronorosen@gmail.com> Date: Tue Mar 18 18:07:33 2014 -0700 Prevent cross plugging router ports from other tenants Previously, a tenant could plug an interface into another tenant's router if he knew their router_id by creating a port with the correct device_id and device_owner. This patch prevents this from occuring by preventing non-admin users from creating ports with device_owner network:router_interface with a device_id that matches another tenants router. In addition, it prevents one from updating a ports device_owner and device_id so that the device_id won't match another tenants router with device_owner being network:router_interface. NOTE: with this change it does open up the possiblity for a tenant to discover router_id's of another tenant's by guessing them and updating a port till a conflict occurs. That said, randomly guessing the router id would be hard and in theory should not matter if exposed. We also need to allow a tenant to update the device_id on network:router_interface ports as this would be used for by anyone using a vm as a service router. This issue will be fixed in another patch upstream as a db migration is required but since this needs to be backported to all stable branches this is not possible. NOTE: The only plugins affect by this are the ones that use the l3-agent. NOTE: **One should perform and audit of the ports that are already attached to routers after applying this patch and remove ports that a tenant may have cross plugged.** Closes-bug: #1243327 Conflicts: quantum/common/exceptions.py quantum/db/db_base_plugin_v2.py quantum/tests/unit/test_l3_plugin.py diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index 71d1d9d..3e68935 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -236,25 +236,30 @@ class InvalidQuotaValue(Conflict): message = _("Change would make usage less than 0 for the following " "resources: %(unders)s") class InvalidSharedSetting(Conflict): message = _("Unable to reconfigure sharing settings for network " "%(network)s. Multiple tenants are using it") class InvalidExtensionEnv(BadRequest): message = _("Invalid extension environment: %(reason)s") class TooManyExternalNetworks(QuantumException): message = _("More than one external network exists") class InvalidConfigurationOption(QuantumException): message = _("An invalid value was provided for %(opt_name)s: " "%(opt_value)s") class GatewayConflictWithAllocationPools(InUse): message = _("Gateway ip %(ip_address)s conflicts with " "allocation pool %(pool)s") + + +class DeviceIDNotOwnedByTenant(Conflict): + message = _("The following device_id %(device_id)s is not owned by your " + "tenant or matches another tenants router.") diff --git a/quantum/db/db_base_plugin_v2.py b/quantum/db/db_base_plugin_v2.py index aacc62d..ff9b8c7 100644 --- a/quantum/db/db_base_plugin_v2.py +++ b/quantum/db/db_base_plugin_v2.py @@ -4,53 +4,55 @@ # 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. import datetime import random import netaddr from oslo.config import cfg from sqlalchemy import orm from sqlalchemy.orm import exc from quantum.api.v2 import attributes from quantum.common import constants from quantum.common import exceptions as q_exc +from quantum import context as ctx from quantum.db import api as db from quantum.db import models_v2 from quantum.db import sqlalchemyutils +from quantum.extensions import l3 from quantum.openstack.common import log as logging from quantum.openstack.common import timeutils from quantum.openstack.common import uuidutils from quantum import quantum_plugin_base_v2 LOG = logging.getLogger(__name__) AGENT_OWNER_PREFIX = 'network:' # Ports with the following 'device_owner' values will not prevent # network deletion. If delete_network() finds that all ports on a # network have these owners, it will explicitly delete each port # and allow network deletion to continue. Similarly, if delete_subnet() # finds out that all existing IP Allocations are associated with ports # with these owners, it will allow subnet deletion to proceed with the # IP allocations being cleaned up by cascade. AUTO_DELETE_PORT_OWNERS = ['network:dhcp'] class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): """ A class that implements the v2 Quantum plugin interface using SQLAlchemy models. Whenever a non-read call happens the plugin will call an event handler class method (e.g., network_created()). The result is that this class can be @@ -1246,50 +1248,53 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): page_reverse=False): marker_obj = self._get_marker_obj(context, 'subnet', limit, marker) return self._get_collection(context, models_v2.Subnet, self._make_subnet_dict, filters=filters, fields=fields, sorts=sorts, limit=limit, marker_obj=marker_obj, page_reverse=page_reverse) def get_subnets_count(self, context, filters=None): return self._get_collection_count(context, models_v2.Subnet, filters=filters) def create_port_bulk(self, context, ports): return self._create_bulk('port', context, ports) def create_port(self, context, port): p = port['port'] port_id = p.get('id') or uuidutils.generate_uuid() network_id = p['network_id'] mac_address = p['mac_address'] # NOTE(jkoelker) Get the tenant_id outside of the session to avoid # unneeded db action if the operation raises tenant_id = self._get_tenant_id_for_create(context, p) + if p.get('device_owner') == constants.DEVICE_OWNER_ROUTER_INTF: + self._enforce_device_owner_not_router_intf_or_device_id(context, p, + tenant_id) with context.session.begin(subtransactions=True): self._recycle_expired_ip_allocations(context, network_id) network = self._get_network(context, network_id) # Ensure that a MAC address is defined and it is unique on the # network if mac_address is attributes.ATTR_NOT_SPECIFIED: mac_address = QuantumDbPluginV2._generate_mac(context, network_id) else: # Ensure that the mac on the network is unique if not QuantumDbPluginV2._check_unique_mac(context, network_id, mac_address): raise q_exc.MacAddressInUse(net_id=network_id, mac=mac_address) # Returns the IP's for the port ips = self._allocate_ips_for_port(context, network, port) if 'status' not in p: status = constants.PORT_STATUS_ACTIVE else: status = p['status'] @@ -1307,50 +1312,67 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): # Update the allocated IP's if ips: for ip in ips: ip_address = ip['ip_address'] subnet_id = ip['subnet_id'] LOG.debug(_("Allocated IP %(ip_address)s " "(%(network_id)s/%(subnet_id)s/%(port_id)s)"), locals()) allocated = models_v2.IPAllocation( network_id=network_id, port_id=port_id, ip_address=ip_address, subnet_id=subnet_id, expiration=self._default_allocation_expiration() ) context.session.add(allocated) return self._make_port_dict(port) def update_port(self, context, id, port): p = port['port'] with context.session.begin(subtransactions=True): port = self._get_port(context, id) + if 'device_owner' in p: + current_device_owner = p['device_owner'] + changed_device_owner = True + else: + current_device_owner = port['device_owner'] + changed_device_owner = False + if p.get('device_id') != port['device_id']: + changed_device_id = True + + # if the current device_owner is ROUTER_INF and the device_id or + # device_owner changed check device_id is not another tenants + # router + if ((current_device_owner == constants.DEVICE_OWNER_ROUTER_INTF) + and (changed_device_id or changed_device_owner)): + self._enforce_device_owner_not_router_intf_or_device_id( + context, p, port['tenant_id'], port) + # Check if the IPs need to be updated if 'fixed_ips' in p: self._recycle_expired_ip_allocations(context, port['network_id']) original = self._make_port_dict(port) ips = self._update_ips_for_port(context, port["network_id"], id, original["fixed_ips"], p['fixed_ips']) # 'fixed_ip's not part of DB so it is deleted del p['fixed_ips'] # Update ips if necessary for ip in ips: allocated = models_v2.IPAllocation( network_id=port['network_id'], port_id=port.id, ip_address=ip['ip_address'], subnet_id=ip['subnet_id'], expiration=self._default_allocation_expiration()) context.session.add(allocated) port.update(p) return self._make_port_dict(port) @@ -1415,25 +1437,52 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): if subnet_ids: query = query.filter(IPAllocation.subnet_id.in_(subnet_ids)) query = self._apply_filters_to_query(query, Port, filters) if limit and page_reverse and sorts: sorts = [(s[0], not s[1]) for s in sorts] query = sqlalchemyutils.paginate_query(query, Port, limit, sorts, marker_obj) return query def get_ports(self, context, filters=None, fields=None, sorts=None, limit=None, marker=None, page_reverse=False): marker_obj = self._get_marker_obj(context, 'port', limit, marker) query = self._get_ports_query(context, filters=filters, sorts=sorts, limit=limit, marker_obj=marker_obj, page_reverse=page_reverse) items = [self._make_port_dict(c, fields) for c in query.all()] if limit and page_reverse: items.reverse() return items def get_ports_count(self, context, filters=None): return self._get_ports_query(context, filters).count() + + def _enforce_device_owner_not_router_intf_or_device_id(self, context, + port_request, + tenant_id, + db_port=None): + if not context.is_admin: + # find the device_id. If the call was update_port and the + # device_id was not passed in we use the device_id from the + # db. + device_id = port_request.get('device_id') + if not device_id and db_port: + device_id = db_port.get('device_id') + # check to make sure device_id does not match another tenants + # router. + if device_id: + if hasattr(self, 'get_router'): + try: + ctx_admin = ctx.get_admin_context() + router = self.get_router(ctx_admin, device_id) + except l3.RouterNotFound: + return + else: + # raise as extension doesn't support L3 anyways. + raise q_exc.DeviceIDNotOwnedByTenant( + device_id=device_id) + if tenant_id != router['tenant_id']: + raise q_exc.DeviceIDNotOwnedByTenant(device_id=device_id) diff --git a/quantum/tests/unit/test_l3_plugin.py b/quantum/tests/unit/test_l3_plugin.py index e490afd..f5f7d6c 100644 --- a/quantum/tests/unit/test_l3_plugin.py +++ b/quantum/tests/unit/test_l3_plugin.py @@ -340,59 +340,65 @@ class L3NatTestCaseMixin(object): return router_req.get_response(self.ext_api) def _make_router(self, fmt, tenant_id, name=None, admin_state_up=None, set_context=False): res = self._create_router(fmt, tenant_id, name, admin_state_up, set_context) return self.deserialize(fmt, res) def _add_external_gateway_to_router(self, router_id, network_id, expected_code=exc.HTTPOk.code, quantum_context=None): return self._update('routers', router_id, {'router': {'external_gateway_info': {'network_id': network_id}}}, expected_code=expected_code, quantum_context=quantum_context) def _remove_external_gateway_from_router(self, router_id, network_id, expected_code=exc.HTTPOk.code): return self._update('routers', router_id, {'router': {'external_gateway_info': {}}}, expected_code=expected_code) def _router_interface_action(self, action, router_id, subnet_id, port_id, - expected_code=exc.HTTPOk.code): + expected_code=exc.HTTPOk.code, + expected_body=None, + tenant_id=None): interface_data = {} if subnet_id: interface_data.update({'subnet_id': subnet_id}) if port_id and (action != 'add' or not subnet_id): interface_data.update({'port_id': port_id}) req = self.new_action_request('routers', interface_data, router_id, "%s_router_interface" % action) + # if tenant_id was specified, create a tenant context for this request + if tenant_id: + req.environ['quantum.context'] = context.Context( + '', tenant_id) res = req.get_response(self.ext_api) self.assertEqual(res.status_int, expected_code) return self.deserialize(self.fmt, res) @contextlib.contextmanager def router(self, name='router1', admin_state_up=True, fmt=None, tenant_id=_uuid(), set_context=False): router = self._make_router(fmt or self.fmt, tenant_id, name, admin_state_up, set_context) try: yield router finally: self._delete('routers', router['router']['id']) def _set_net_external(self, net_id): self._update('networks', net_id, {'network': {l3.EXTERNAL: True}}) def _create_floatingip(self, fmt, network_id, port_id=None, fixed_ip=None, set_context=False): data = {'floatingip': {'floating_network_id': network_id, 'tenant_id': self._tenant_id}} if port_id: data['floatingip']['port_id'] = port_id if fixed_ip: @@ -936,50 +942,106 @@ class L3NatDBTestCase(L3NatTestCaseBase): s['subnet']['network_id']) body = self._show('routers', r['router']['id']) gw_info = body['router']['external_gateway_info'] self.assertEqual(gw_info, None) def test_router_add_gateway_tenant_ctx(self): with self.router(tenant_id='noadmin', set_context=True) as r: with self.subnet() as s: self._set_net_external(s['subnet']['network_id']) ctx = context.Context('', 'noadmin') self._add_external_gateway_to_router( r['router']['id'], s['subnet']['network_id'], quantum_context=ctx) body = self._show('routers', r['router']['id']) net_id = body['router']['external_gateway_info']['network_id'] self.assertEqual(net_id, s['subnet']['network_id']) self._remove_external_gateway_from_router( r['router']['id'], s['subnet']['network_id']) body = self._show('routers', r['router']['id']) gw_info = body['router']['external_gateway_info'] self.assertEqual(gw_info, None) + def test_create_router_port_with_device_id_of_other_teants_router(self): + with self.router() as admin_router: + with self.network(tenant_id='tenant_a', + set_context=True) as n: + with self.subnet(network=n): + self._create_port( + self.fmt, n['network']['id'], + tenant_id='tenant_a', + device_id=admin_router['router']['id'], + device_owner='network:router_interface', + set_context=True, + expected_res_status=exc.HTTPConflict.code) + + def test_create_non_router_port_device_id_of_other_teants_router_update( + self): + # This tests that HTTPConflict is raised if we create a non-router + # port that matches the device_id of another tenants router and then + # we change the device_owner to be network:router_interface. + with self.router() as admin_router: + with self.network(tenant_id='tenant_a', + set_context=True) as n: + with self.subnet(network=n): + port_res = self._create_port( + self.fmt, n['network']['id'], + tenant_id='tenant_a', + device_id=admin_router['router']['id'], + set_context=True) + port = self.deserialize(self.fmt, port_res) + quantum_context = context.Context('', 'tenant_a') + data = {'port': {'device_owner': + 'network:router_interface'}} + self._update('ports', port['port']['id'], data, + quantum_context=quantum_context, + expected_code=exc.HTTPConflict.code) + self._delete('ports', port['port']['id']) + + def test_update_port_device_id_to_different_tenants_router(self): + with self.router() as admin_router: + with self.router(tenant_id='tenant_a', + set_context=True) as tenant_router: + with self.network(tenant_id='tenant_a', + set_context=True) as n: + with self.subnet(network=n) as s: + port = self._router_interface_action( + 'add', tenant_router['router']['id'], + s['subnet']['id'], None, tenant_id='tenant_a') + quantum_context = context.Context('', 'tenant_a') + data = {'port': + {'device_id': admin_router['router']['id']}} + self._update('ports', port['port_id'], data, + quantum_context=quantum_context, + expected_code=exc.HTTPConflict.code) + self._router_interface_action( + 'remove', tenant_router['router']['id'], + s['subnet']['id'], None, tenant_id='tenant_a') + def test_router_add_gateway_invalid_network_returns_404(self): with self.router() as r: self._add_external_gateway_to_router( r['router']['id'], "foobar", expected_code=exc.HTTPNotFound.code) def test_router_add_gateway_net_not_external_returns_400(self): with self.router() as r: with self.subnet() as s: # intentionally do not set net as external self._add_external_gateway_to_router( r['router']['id'], s['subnet']['network_id'], expected_code=exc.HTTPBadRequest.code) def test_router_add_gateway_no_subnet_returns_400(self): with self.router() as r: with self.network() as n: self._set_net_external(n['network']['id']) self._add_external_gateway_to_router( r['router']['id'], n['network']['id'], expected_code=exc.HTTPBadRequest.code) def test_router_remove_interface_inuse_returns_409(self): with self.router() as r:
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor