Merge "Restrict user private network cidr input"
This commit is contained in:
@@ -1615,6 +1615,19 @@ Ignore all listed Nova extensions, and behave as if they were unsupported.
|
|||||||
Can be used to selectively disable certain costly extensions for performance
|
Can be used to selectively disable certain costly extensions for performance
|
||||||
reasons.
|
reasons.
|
||||||
|
|
||||||
|
``ALLOWED_PRIVATE_SUBNET_CIDR``
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 10.0.0(Newton)
|
||||||
|
|
||||||
|
Default: ``{'ipv4': [], 'ipv6': []}``
|
||||||
|
|
||||||
|
Dict used to restrict user private subnet cidr range.
|
||||||
|
An empty list means that user input will not be restricted
|
||||||
|
for a corresponding IP version. By default, there is
|
||||||
|
no restriction for both IPv4 and IPv6.
|
||||||
|
|
||||||
|
Example: ``{'ipv4': ['192.168.0.0/16', '10.0.0.0/8'], 'ipv6': ['fc00::/7',]}``
|
||||||
|
|
||||||
``ADMIN_FILTER_DATA_FIRST``
|
``ADMIN_FILTER_DATA_FIRST``
|
||||||
---------------------------
|
---------------------------
|
||||||
|
@@ -22,12 +22,25 @@ from horizon import exceptions
|
|||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
from openstack_dashboard.dashboards.project.networks.subnets \
|
from openstack_dashboard.dashboards.project.networks.subnets \
|
||||||
import workflows as project_workflows
|
import workflows as project_workflows
|
||||||
|
from openstack_dashboard.dashboards.project.networks import workflows \
|
||||||
|
as net_workflows
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateSubnetInfoAction(project_workflows.CreateSubnetInfoAction):
|
||||||
|
check_subnet_range = False
|
||||||
|
|
||||||
|
|
||||||
|
class CreateSubnetInfo(project_workflows.CreateSubnetInfo):
|
||||||
|
action_class = CreateSubnetInfoAction
|
||||||
|
|
||||||
|
|
||||||
class CreateSubnet(project_workflows.CreateSubnet):
|
class CreateSubnet(project_workflows.CreateSubnet):
|
||||||
|
default_steps = (CreateSubnetInfo,
|
||||||
|
net_workflows.CreateSubnetDetail)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse("horizon:admin:networks:detail",
|
return reverse("horizon:admin:networks:detail",
|
||||||
args=(self.context.get('network_id'),))
|
args=(self.context.get('network_id'),))
|
||||||
@@ -53,6 +66,17 @@ class CreateSubnet(project_workflows.CreateSubnet):
|
|||||||
return True if subnet else False
|
return True if subnet else False
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateSubnetInfoAction(project_workflows.UpdateSubnetInfoAction):
|
||||||
|
check_subnet_range = False
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateSubnetInfo(project_workflows.UpdateSubnetInfo):
|
||||||
|
action_class = UpdateSubnetInfoAction
|
||||||
|
|
||||||
|
|
||||||
class UpdateSubnet(project_workflows.UpdateSubnet):
|
class UpdateSubnet(project_workflows.UpdateSubnet):
|
||||||
success_url = "horizon:admin:networks:detail"
|
success_url = "horizon:admin:networks:detail"
|
||||||
failure_url = "horizon:admin:networks:detail"
|
failure_url = "horizon:admin:networks:detail"
|
||||||
|
|
||||||
|
default_steps = (UpdateSubnetInfo,
|
||||||
|
project_workflows.UpdateSubnetDetail)
|
||||||
|
@@ -765,6 +765,189 @@ class NetworkTests(test.TestCase, NetworkStubMixin):
|
|||||||
self.test_network_create_post_with_subnet_cidr_without_mask(
|
self.test_network_create_post_with_subnet_cidr_without_mask(
|
||||||
test_with_subnetpool=True)
|
test_with_subnetpool=True)
|
||||||
|
|
||||||
|
@test.update_settings(
|
||||||
|
ALLOWED_PRIVATE_SUBNET_CIDR={'ipv4': ['192.168.0.0/16']})
|
||||||
|
@test.create_stubs({api.neutron: ('is_extension_supported',
|
||||||
|
'profile_list',
|
||||||
|
'subnetpool_list')})
|
||||||
|
def test_network_create_post_with_subnet_cidr_invalid_v4_range(
|
||||||
|
self,
|
||||||
|
test_with_profile=False,
|
||||||
|
test_with_subnetpool=False
|
||||||
|
):
|
||||||
|
network = self.networks.first()
|
||||||
|
subnet = self.subnets.first()
|
||||||
|
if test_with_profile:
|
||||||
|
net_profiles = self.net_profiles.list()
|
||||||
|
net_profile_id = self.net_profiles.first().id
|
||||||
|
api.neutron.profile_list(IsA(http.HttpRequest),
|
||||||
|
'network').AndReturn(net_profiles)
|
||||||
|
|
||||||
|
api.neutron.is_extension_supported(IsA(http.HttpRequest),
|
||||||
|
'subnet_allocation').\
|
||||||
|
AndReturn(True)
|
||||||
|
api.neutron.subnetpool_list(IsA(http.HttpRequest)).\
|
||||||
|
AndReturn(self.subnetpools.list())
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
form_data = {'net_name': network.name,
|
||||||
|
'shared': False,
|
||||||
|
'admin_state': network.admin_state_up,
|
||||||
|
'with_subnet': True}
|
||||||
|
if test_with_profile:
|
||||||
|
form_data['net_profile_id'] = net_profile_id
|
||||||
|
if test_with_subnetpool:
|
||||||
|
subnetpool = self.subnetpools.first()
|
||||||
|
form_data['subnetpool'] = subnetpool.id
|
||||||
|
form_data['prefixlen'] = subnetpool.default_prefixlen
|
||||||
|
|
||||||
|
form_data.update(form_data_subnet(subnet, cidr='30.30.30.0/24',
|
||||||
|
allocation_pools=[]))
|
||||||
|
url = reverse('horizon:project:networks:create')
|
||||||
|
res = self.client.post(url, form_data)
|
||||||
|
|
||||||
|
expected_msg = ("CIDRs allowed for user private ipv4 networks "
|
||||||
|
"are 192.168.0.0/16.")
|
||||||
|
self.assertContains(res, expected_msg)
|
||||||
|
|
||||||
|
@test.update_settings(
|
||||||
|
ALLOWED_PRIVATE_SUBNET_CIDR={'ipv4': ['192.168.0.0/16']})
|
||||||
|
@test.update_settings(
|
||||||
|
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
|
||||||
|
def test_network_create_post_with_subnet_cidr_invalid_v4_range_w_profile(
|
||||||
|
self):
|
||||||
|
self.test_network_create_post_with_subnet_cidr_invalid_v4_range(
|
||||||
|
test_with_profile=True)
|
||||||
|
|
||||||
|
@test.update_settings(
|
||||||
|
ALLOWED_PRIVATE_SUBNET_CIDR={'ipv4': ['192.168.0.0/16']})
|
||||||
|
def test_network_create_post_with_subnet_cidr_invalid_v4_range_w_snpool(
|
||||||
|
self):
|
||||||
|
self.test_network_create_post_with_subnet_cidr_invalid_v4_range(
|
||||||
|
test_with_subnetpool=True)
|
||||||
|
|
||||||
|
@test.update_settings(ALLOWED_PRIVATE_SUBNET_CIDR={'ipv6': ['fc00::/9']})
|
||||||
|
@test.create_stubs({api.neutron: ('is_extension_supported',
|
||||||
|
'profile_list',
|
||||||
|
'subnetpool_list')})
|
||||||
|
def test_network_create_post_with_subnet_cidr_invalid_v6_range(
|
||||||
|
self,
|
||||||
|
test_with_profile=False,
|
||||||
|
test_with_subnetpool=False
|
||||||
|
):
|
||||||
|
network = self.networks.first()
|
||||||
|
subnet_v6 = self.subnets.list()[3]
|
||||||
|
|
||||||
|
if test_with_profile:
|
||||||
|
net_profiles = self.net_profiles.list()
|
||||||
|
net_profile_id = self.net_profiles.first().id
|
||||||
|
api.neutron.profile_list(IsA(http.HttpRequest),
|
||||||
|
'network').AndReturn(net_profiles)
|
||||||
|
|
||||||
|
api.neutron.is_extension_supported(IsA(http.HttpRequest),
|
||||||
|
'subnet_allocation').\
|
||||||
|
AndReturn(True)
|
||||||
|
api.neutron.subnetpool_list(IsA(http.HttpRequest)).\
|
||||||
|
AndReturn(self.subnetpools.list())
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
form_data = {'net_name': network.name,
|
||||||
|
'shared': False,
|
||||||
|
'admin_state': network.admin_state_up,
|
||||||
|
'with_subnet': True}
|
||||||
|
if test_with_profile:
|
||||||
|
form_data['net_profile_id'] = net_profile_id
|
||||||
|
if test_with_subnetpool:
|
||||||
|
subnetpool = self.subnetpools.first()
|
||||||
|
form_data['subnetpool'] = subnetpool.id
|
||||||
|
form_data['prefixlen'] = subnetpool.default_prefixlen
|
||||||
|
|
||||||
|
form_data.update(form_data_subnet(subnet_v6, cidr='fc00::/7',
|
||||||
|
allocation_pools=[]))
|
||||||
|
url = reverse('horizon:project:networks:create')
|
||||||
|
res = self.client.post(url, form_data)
|
||||||
|
|
||||||
|
expected_msg = ("CIDRs allowed for user private ipv6 networks "
|
||||||
|
"are fc00::/9.")
|
||||||
|
self.assertContains(res, expected_msg)
|
||||||
|
|
||||||
|
@test.update_settings(ALLOWED_PRIVATE_SUBNET_CIDR={'ipv6': ['fc00::/9']})
|
||||||
|
@test.update_settings(
|
||||||
|
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
|
||||||
|
def test_network_create_post_with_subnet_cidr_invalid_v6_range_w_profile(
|
||||||
|
self):
|
||||||
|
self.test_network_create_post_with_subnet_cidr_invalid_v6_range(
|
||||||
|
test_with_profile=True)
|
||||||
|
|
||||||
|
@test.update_settings(ALLOWED_PRIVATE_SUBNET_CIDR={'ipv6': ['fc00::/9']})
|
||||||
|
def test_network_create_post_with_subnet_cidr_invalid_v6_range_w_snpool(
|
||||||
|
self):
|
||||||
|
self.test_network_create_post_with_subnet_cidr_invalid_v4_range(
|
||||||
|
test_with_subnetpool=True)
|
||||||
|
|
||||||
|
@test.create_stubs({api.neutron: ('network_create',
|
||||||
|
'subnet_create',
|
||||||
|
'profile_list',
|
||||||
|
'is_extension_supported',
|
||||||
|
'subnetpool_list')})
|
||||||
|
def test_network_create_post_with_subnet_cidr_not_restrict(
|
||||||
|
self,
|
||||||
|
test_with_profile=False
|
||||||
|
):
|
||||||
|
network = self.networks.first()
|
||||||
|
subnet = self.subnets.first()
|
||||||
|
cidr = '30.30.30.0/24'
|
||||||
|
gateway_ip = '30.30.30.1'
|
||||||
|
params = {'name': network.name,
|
||||||
|
'admin_state_up': network.admin_state_up,
|
||||||
|
'shared': False}
|
||||||
|
subnet_params = {'network_id': network.id,
|
||||||
|
'name': subnet.name,
|
||||||
|
'cidr': cidr,
|
||||||
|
'ip_version': subnet.ip_version,
|
||||||
|
'gateway_ip': gateway_ip,
|
||||||
|
'enable_dhcp': subnet.enable_dhcp}
|
||||||
|
|
||||||
|
if test_with_profile:
|
||||||
|
net_profiles = self.net_profiles.list()
|
||||||
|
net_profile_id = self.net_profiles.first().id
|
||||||
|
api.neutron.profile_list(IsA(http.HttpRequest),
|
||||||
|
'network').AndReturn(net_profiles)
|
||||||
|
params['net_profile_id'] = net_profile_id
|
||||||
|
api.neutron.is_extension_supported(IsA(http.HttpRequest),
|
||||||
|
'subnet_allocation').\
|
||||||
|
AndReturn(True)
|
||||||
|
api.neutron.subnetpool_list(IsA(http.HttpRequest)).\
|
||||||
|
AndReturn(self.subnetpools.list())
|
||||||
|
api.neutron.network_create(IsA(http.HttpRequest),
|
||||||
|
**params).AndReturn(network)
|
||||||
|
api.neutron.subnet_create(IsA(http.HttpRequest),
|
||||||
|
**subnet_params).AndReturn(subnet)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
form_data = {'net_name': network.name,
|
||||||
|
'admin_state': network.admin_state_up,
|
||||||
|
'shared': False,
|
||||||
|
'with_subnet': True}
|
||||||
|
|
||||||
|
if test_with_profile:
|
||||||
|
form_data['net_profile_id'] = net_profile_id
|
||||||
|
|
||||||
|
form_data.update(form_data_subnet(subnet, cidr=cidr,
|
||||||
|
gateway_ip=gateway_ip,
|
||||||
|
allocation_pools=[]))
|
||||||
|
url = reverse('horizon:project:networks:create')
|
||||||
|
res = self.client.post(url, form_data)
|
||||||
|
|
||||||
|
self.assertNoFormErrors(res)
|
||||||
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
|
@test.update_settings(
|
||||||
|
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
|
||||||
|
def test_network_create_post_with_subnet_cidr_not_restrict_w_profile(self):
|
||||||
|
self.test_network_create_post_with_subnet_cidr_not_restrict(
|
||||||
|
test_with_profile=True)
|
||||||
|
|
||||||
@test.create_stubs({api.neutron: ('profile_list',
|
@test.create_stubs({api.neutron: ('profile_list',
|
||||||
'is_extension_supported',
|
'is_extension_supported',
|
||||||
'subnetpool_list',)})
|
'subnetpool_list',)})
|
||||||
|
@@ -199,6 +199,8 @@ class CreateSubnetInfoAction(workflows.Action):
|
|||||||
initial=False,
|
initial=False,
|
||||||
required=False)
|
required=False)
|
||||||
|
|
||||||
|
check_subnet_range = True
|
||||||
|
|
||||||
class Meta(object):
|
class Meta(object):
|
||||||
name = _("Subnet")
|
name = _("Subnet")
|
||||||
help_text = _('Creates a subnet associated with the network.'
|
help_text = _('Creates a subnet associated with the network.'
|
||||||
@@ -268,6 +270,28 @@ class CreateSubnetInfoAction(workflows.Action):
|
|||||||
self.fields['subnetpool'].widget = forms.HiddenInput()
|
self.fields['subnetpool'].widget = forms.HiddenInput()
|
||||||
self.fields['prefixlen'].widget = forms.HiddenInput()
|
self.fields['prefixlen'].widget = forms.HiddenInput()
|
||||||
|
|
||||||
|
def _check_subnet_range(self, subnet, allow_cidr):
|
||||||
|
allowed_net = netaddr.IPNetwork(allow_cidr)
|
||||||
|
return subnet in allowed_net
|
||||||
|
|
||||||
|
def _check_cidr_allowed(self, ip_version, subnet):
|
||||||
|
if not self.check_subnet_range:
|
||||||
|
return
|
||||||
|
|
||||||
|
allowed_cidr = getattr(settings, "ALLOWED_PRIVATE_SUBNET_CIDR", {})
|
||||||
|
version_str = 'ipv%s' % ip_version
|
||||||
|
allowed_ranges = allowed_cidr.get(version_str, [])
|
||||||
|
if allowed_ranges:
|
||||||
|
under_range = any(self._check_subnet_range(subnet, allowed_range)
|
||||||
|
for allowed_range in allowed_ranges)
|
||||||
|
if not under_range:
|
||||||
|
range_str = ', '.join(allowed_ranges)
|
||||||
|
msg = (_("CIDRs allowed for user private %(ip_ver)s "
|
||||||
|
"networks are %(allowed)s.") %
|
||||||
|
{'ip_ver': '%s' % version_str,
|
||||||
|
'allowed': range_str})
|
||||||
|
raise forms.ValidationError(msg)
|
||||||
|
|
||||||
def _check_subnet_data(self, cleaned_data, is_create=True):
|
def _check_subnet_data(self, cleaned_data, is_create=True):
|
||||||
cidr = cleaned_data.get('cidr')
|
cidr = cleaned_data.get('cidr')
|
||||||
ip_version = int(cleaned_data.get('ip_version'))
|
ip_version = int(cleaned_data.get('ip_version'))
|
||||||
@@ -295,6 +319,8 @@ class CreateSubnetInfoAction(workflows.Action):
|
|||||||
msg = _("The subnet in the Network Address is "
|
msg = _("The subnet in the Network Address is "
|
||||||
"too small (/%s).") % subnet.prefixlen
|
"too small (/%s).") % subnet.prefixlen
|
||||||
self._errors['cidr'] = self.error_class([msg])
|
self._errors['cidr'] = self.error_class([msg])
|
||||||
|
self._check_cidr_allowed(ip_version, subnet)
|
||||||
|
|
||||||
if not no_gateway and gateway_ip:
|
if not no_gateway and gateway_ip:
|
||||||
if netaddr.IPAddress(gateway_ip).version is not ip_version:
|
if netaddr.IPAddress(gateway_ip).version is not ip_version:
|
||||||
msg = _('Gateway IP and IP version are inconsistent.')
|
msg = _('Gateway IP and IP version are inconsistent.')
|
||||||
|
@@ -783,4 +783,16 @@ REST_API_REQUIRED_SETTINGS = ['OPENSTACK_HYPERVISOR_FEATURES',
|
|||||||
# To allow operators to require admin users provide a search criteria first
|
# To allow operators to require admin users provide a search criteria first
|
||||||
# before loading any data into the admin views, set the following attribute to
|
# before loading any data into the admin views, set the following attribute to
|
||||||
# True
|
# True
|
||||||
#ADMIN_FILTER_DATA_FIRST=False
|
#ADMIN_FILTER_DATA_FIRST=False
|
||||||
|
|
||||||
|
# Dict used to restrict user private subnet cidr range.
|
||||||
|
# An empty list means that user input will not be restricted
|
||||||
|
# for a corresponding IP version. By default, there is
|
||||||
|
# no restriction for IPv4 or IPv6. To restrict
|
||||||
|
# user private subnet cidr range set ALLOWED_PRIVATE_SUBNET_CIDR
|
||||||
|
# to something like
|
||||||
|
#ALLOWED_PRIVATE_SUBNET_CIDR = {
|
||||||
|
# 'ipv4': ['10.0.0.0/8', '192.168.0.0/16'],
|
||||||
|
# 'ipv6': ['fc00::/7']
|
||||||
|
#}
|
||||||
|
ALLOWED_PRIVATE_SUBNET_CIDR = {'ipv4': [], 'ipv6': []}
|
||||||
|
@@ -265,3 +265,5 @@ REST_API_SETTING_2 = 'bar'
|
|||||||
REST_API_SECURITY = 'SECURITY'
|
REST_API_SECURITY = 'SECURITY'
|
||||||
REST_API_REQUIRED_SETTINGS = ['REST_API_SETTING_1']
|
REST_API_REQUIRED_SETTINGS = ['REST_API_SETTING_1']
|
||||||
REST_API_ADDITIONAL_SETTINGS = ['REST_API_SETTING_2']
|
REST_API_ADDITIONAL_SETTINGS = ['REST_API_SETTING_2']
|
||||||
|
|
||||||
|
ALLOWED_PRIVATE_SUBNET_CIDR = {'ipv4': [], 'ipv6': []}
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Allows to restrict CIDR range for user private network <https://blueprints.launchpad.net/horizon/+spec/restrict-private-network-input>
|
Reference in New Issue
Block a user