Skip to content

Commit cb48ceb

Browse files
committed
Implementation of blueprint ip-validation
First draft. Added a forms.Field wrapper for IPAddress. Implemented IPv4 and IPv6 checks, subnet mask range, optional mask range limitation. As far as I see now, there is only 1 place in Dashboard to accept IP fields as input - the Security rules. I've tried to input IPv6 rule and it was accepted. The previous version of the code doesn't accept IPv6, only IPv4. I am not sure if IPv6 should be accepted here. It however works. Patch set 3: Now using netaddr library(used also by nova), which provides support for validation of IP addresses. Using this library, now the IPField can support more ways to enter an IP - like short versions: 10/8 - for all 10.xxx.xxx.xxx 192.168/16 - for all 192.168.xxx.xxx Regarding IPy library - it performs some strict subnet validation, which will not accept cidr like this: 192.168.1.1/20 because the only mask that matches this IP is 32. IPy doesn't allow broader masks. But my assumption is that the operators should take the responsibility for the data they enter. At least this CIDR is valid after all. Change-Id: Ie497fe65fde3af25a18109a182ab78255ad7ec60
1 parent 856983f commit cb48ceb

File tree

5 files changed

+227
-21
lines changed

5 files changed

+227
-21
lines changed

horizon/dashboards/nova/access_and_security/security_groups/forms.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
from horizon import api
3131
from horizon import exceptions
3232
from horizon import forms
33-
from horizon.utils.validators import validate_ipv4_cidr
3433
from horizon.utils.validators import validate_port_range
34+
from horizon.utils import fields
3535

3636

3737
LOG = logging.getLogger(__name__)
@@ -82,12 +82,13 @@ class AddRule(forms.SelfHandlingForm):
8282
validators=[validate_port_range])
8383

8484
source_group = forms.ChoiceField(label=_('Source Group'), required=False)
85-
cidr = forms.CharField(label=_("CIDR"),
85+
cidr = fields.IPField(label=_("CIDR"),
8686
required=False,
8787
initial="0.0.0.0/0",
8888
help_text=_("Classless Inter-Domain Routing "
8989
"(e.g. 192.168.0.0/24)"),
90-
validators=[validate_ipv4_cidr])
90+
version=fields.IPv4 | fields.IPv6,
91+
mask=True)
9192

9293
security_group_id = forms.IntegerField(widget=forms.HiddenInput())
9394

horizon/tests/utils_tests.py

+138-9
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717

1818
from horizon import test
19-
from horizon.utils import validators
19+
from django.core.exceptions import ValidationError
20+
from horizon.utils import fields
2021

2122

2223
class ValidatorsTests(test.TestCase):
@@ -27,16 +28,144 @@ def test_validate_ipv4_cidr(self):
2728
"10.144.11.107/4",
2829
"255.255.255.255/0",
2930
"0.1.2.3/16",
30-
"0.0.0.0/32")
31-
BAD_CIDRS = ("255.255.255.256",
32-
"256.255.255.255",
33-
"1.2.3.4.5",
34-
"0.0.0.0",
31+
"0.0.0.0/32",
32+
# short form
33+
"128.0/16",
34+
"128/4")
35+
BAD_CIDRS = ("255.255.255.256\\",
36+
"256.255.255.255$",
37+
"1.2.3.4.5/41",
38+
"0.0.0.0/99",
3539
"127.0.0.1/",
3640
"127.0.0.1/33",
3741
"127.0.0.1/-1",
38-
"127.0.0.1/100")
42+
"127.0.0.1/100",
43+
# some valid IPv6 addresses
44+
"fe80::204:61ff:254.157.241.86/4",
45+
"fe80::204:61ff:254.157.241.86/0",
46+
"2001:0DB8::CD30:0:0:0:0/60",
47+
"2001:0DB8::CD30:0/90")
48+
ip = fields.IPField(mask=True, version=fields.IPv4)
3949
for cidr in GOOD_CIDRS:
40-
self.assertTrue(validators.ipv4_cidr_re.match(cidr))
50+
self.assertIsNone(ip.validate(cidr))
4151
for cidr in BAD_CIDRS:
42-
self.assertFalse(validators.ipv4_cidr_re.match(cidr))
52+
self.assertRaises(ValidationError, ip.validate, cidr)
53+
54+
def test_validate_ipv6_cidr(self):
55+
GOOD_CIDRS = ("::ffff:0:0/56",
56+
"2001:0db8::1428:57ab/17",
57+
"FEC0::/10",
58+
"fe80::204:61ff:254.157.241.86/4",
59+
"fe80::204:61ff:254.157.241.86/0",
60+
"2001:0DB8::CD30:0:0:0:0/60",
61+
"2001:0DB8::CD30:0/90",
62+
"::1/128")
63+
BAD_CIDRS = ("1111:2222:3333:4444:::/",
64+
"::2222:3333:4444:5555:6666:7777:8888:\\",
65+
":1111:2222:3333:4444::6666:1.2.3.4/1000",
66+
"1111:2222::4444:5555:6666::8888@",
67+
"1111:2222::4444:5555:6666:8888/",
68+
"::ffff:0:0/129",
69+
"1.2.3.4:1111:2222::5555//22",
70+
"fe80::204:61ff:254.157.241.86/200",
71+
# some valid IPv4 addresses
72+
"10.144.11.107/4",
73+
"255.255.255.255/0",
74+
"0.1.2.3/16")
75+
ip = fields.IPField(mask=True, version=fields.IPv6)
76+
for cidr in GOOD_CIDRS:
77+
self.assertIsNone(ip.validate(cidr))
78+
for cidr in BAD_CIDRS:
79+
self.assertRaises(ValidationError, ip.validate, cidr)
80+
81+
def test_validate_mixed_cidr(self):
82+
GOOD_CIDRS = ("::ffff:0:0/56",
83+
"2001:0db8::1428:57ab/17",
84+
"FEC0::/10",
85+
"fe80::204:61ff:254.157.241.86/4",
86+
"fe80::204:61ff:254.157.241.86/0",
87+
"2001:0DB8::CD30:0:0:0:0/60",
88+
"0.0.0.0/16",
89+
"10.144.11.107/4",
90+
"255.255.255.255/0",
91+
"0.1.2.3/16",
92+
# short form
93+
"128.0/16",
94+
"10/4")
95+
BAD_CIDRS = ("1111:2222:3333:4444::://",
96+
"::2222:3333:4444:5555:6666:7777:8888:",
97+
":1111:2222:3333:4444::6666:1.2.3.4/1/1",
98+
"1111:2222::4444:5555:6666::8888\\2",
99+
"1111:2222::4444:5555:6666:8888/",
100+
"1111:2222::4444:5555:6666::8888/130",
101+
"127.0.0.1/",
102+
"127.0.0.1/33",
103+
"127.0.0.1/-1")
104+
ip = fields.IPField(mask=True, version=fields.IPv4 | fields.IPv6)
105+
for cidr in GOOD_CIDRS:
106+
self.assertIsNone(ip.validate(cidr))
107+
for cidr in BAD_CIDRS:
108+
self.assertRaises(ValidationError, ip.validate, cidr)
109+
110+
def test_validate_IPs(self):
111+
GOOD_IPS_V4 = ("0.0.0.0",
112+
"10.144.11.107",
113+
"169.144.11.107",
114+
"172.100.11.107",
115+
"255.255.255.255",
116+
"0.1.2.3")
117+
GOOD_IPS_V6 = ("",
118+
"::ffff:0:0",
119+
"2001:0db8::1428:57ab",
120+
"FEC0::",
121+
"fe80::204:61ff:254.157.241.86",
122+
"fe80::204:61ff:254.157.241.86",
123+
"2001:0DB8::CD30:0:0:0:0")
124+
BAD_IPS_V4 = ("1111:2222:3333:4444:::",
125+
"::2222:3333:4444:5555:6666:7777:8888:",
126+
":1111:2222:3333:4444::6666:1.2.3.4",
127+
"1111:2222::4444:5555:6666::8888",
128+
"1111:2222::4444:5555:6666:8888/",
129+
"1111:2222::4444:5555:6666::8888/130",
130+
"127.0.0.1/",
131+
"127.0.0.1/33",
132+
"127.0.0.1/-1")
133+
BAD_IPS_V6 = ("1111:2222:3333:4444:::",
134+
"::2222:3333:4444:5555:6666:7777:8888:",
135+
":1111:2222:3333:4444::6666:1.2.3.4",
136+
"1111:2222::4444:5555:6666::8888",
137+
"1111:2222::4444:5555:6666:8888/",
138+
"1111:2222::4444:5555:6666::8888/130")
139+
ipv4 = fields.IPField(required=True, version=fields.IPv4)
140+
ipv6 = fields.IPField(required=False, version=fields.IPv6)
141+
ipmixed = fields.IPField(required=False,
142+
version=fields.IPv4 | fields.IPv6)
143+
144+
for ip_addr in GOOD_IPS_V4:
145+
self.assertIsNone(ipv4.validate(ip_addr))
146+
self.assertIsNone(ipmixed.validate(ip_addr))
147+
148+
for ip_addr in GOOD_IPS_V6:
149+
self.assertIsNone(ipv6.validate(ip_addr))
150+
self.assertIsNone(ipmixed.validate(ip_addr))
151+
152+
for ip_addr in BAD_IPS_V4:
153+
self.assertRaises(ValidationError, ipv4.validate, ip_addr)
154+
self.assertRaises(ValidationError, ipmixed.validate, ip_addr)
155+
156+
for ip_addr in BAD_IPS_V6:
157+
self.assertRaises(ValidationError, ipv6.validate, ip_addr)
158+
self.assertRaises(ValidationError, ipmixed.validate, ip_addr)
159+
160+
self.assertRaises(ValidationError, ipv4.validate, "") # required=True
161+
162+
iprange = fields.IPField(required=False,
163+
mask=True,
164+
mask_range_from=10,
165+
version=fields.IPv4 | fields.IPv6)
166+
self.assertRaises(ValidationError, iprange.validate,
167+
"fe80::204:61ff:254.157.241.86/6")
168+
self.assertRaises(ValidationError, iprange.validate,
169+
"169.144.11.107/8")
170+
self.assertIsNone(iprange.validate("fe80::204:61ff:254.157.241.86/36"))
171+
self.assertIsNone(iprange.validate("169.144.11.107/18"))

horizon/utils/fields.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import re
2+
import netaddr
3+
from django.core.exceptions import ValidationError
4+
from django.forms import forms
5+
from django.utils.translation import ugettext as _
6+
7+
ip_allowed_symbols_re = re.compile(r'^[a-fA-F0-9:/\.]+$')
8+
IPv4 = 1
9+
IPv6 = 2
10+
11+
12+
class IPField(forms.Field):
13+
"""
14+
Form field for entering IP/range values, with validation.
15+
Supports IPv4/IPv6 in the format:
16+
.. xxx.xxx.xxx.xxx
17+
.. xxx.xxx.xxx.xxx/zz
18+
.. ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
19+
.. ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/zz
20+
and all compressed forms. Also the short forms
21+
are supported:
22+
xxx/yy
23+
xxx.xxx/yy
24+
25+
.. attribute:: version
26+
27+
Specifies which IP version to validate,
28+
valid values are 1 (fields.IPv4), 2 (fields.IPv6) or
29+
both - 3 (fields.IPv4 | fields.IPv6).
30+
Defaults to IPv4 (1)
31+
32+
.. attribute:: mask
33+
34+
Boolean flag to validate subnet masks along with IP address.
35+
E.g: 10.0.0.1/32
36+
37+
.. attribute:: mask_range_from
38+
Subnet range limitation, e.g. 16
39+
That means the input mask will be checked to be in the range
40+
16:max_value. Useful to limit the subnet ranges
41+
to A/B/C-class networks.
42+
"""
43+
invalid_format_message = _("Incorrect format for IP address")
44+
invalid_version_message = _("Invalid version for IP address")
45+
invalid_mask_message = _("Invalid subnet mask")
46+
max_v4_mask = 32
47+
max_v6_mask = 128
48+
49+
def __init__(self, *args, **kwargs):
50+
self.mask = kwargs.pop("mask", None)
51+
self.min_mask = kwargs.pop("mask_range_from", 0)
52+
self.version = kwargs.pop('version', IPv4)
53+
54+
super(IPField, self).__init__(*args, **kwargs)
55+
56+
def validate(self, value):
57+
super(IPField, self).validate(value)
58+
if not value and not self.required:
59+
return
60+
61+
try:
62+
if self.mask:
63+
self.ip = netaddr.IPNetwork(value)
64+
else:
65+
self.ip = netaddr.IPAddress(value)
66+
except:
67+
raise ValidationError(self.invalid_format_message)
68+
69+
if not any([self.version & IPv4 > 0 and self.ip.version == 4,
70+
self.version & IPv6 > 0 and self.ip.version == 6]):
71+
raise ValidationError(self.invalid_version_message)
72+
73+
if self.mask:
74+
if self.ip.version == 4 and \
75+
not self.min_mask <= self.ip.prefixlen <= self.max_v4_mask:
76+
raise ValidationError(self.invalid_mask_message)
77+
78+
if self.ip.version == 6 and \
79+
not self.min_mask <= self.ip.prefixlen <= self.max_v6_mask:
80+
raise ValidationError(self.invalid_mask_message)
81+
82+
def clean(self, value):
83+
super(IPField, self).clean(value)
84+
return str(getattr(self, "ip", ""))

horizon/utils/validators.py

-9
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,10 @@
1414
# License for the specific language governing permissions and limitations
1515
# under the License.
1616

17-
import re
18-
1917
from django.conf import settings
20-
from django.core import validators
2118
from django.core.exceptions import ValidationError
2219
from django.utils.translation import ugettext as _
2320

24-
ipv4_cidr_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)' # 0-255
25-
'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}' # 3x .0-255
26-
'/(3[0-2]|[1-2]?\d)$') # /0-32
27-
28-
29-
validate_ipv4_cidr = validators.RegexValidator(ipv4_cidr_re)
3021
horizon_config = getattr(settings, "HORIZON_CONFIG", {})
3122
password_config = horizon_config.get("password_validator", {})
3223

tools/test-requires

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pep8
88
pylint
99
distribute>=0.6.24
1010
selenium
11+
netaddr
1112

1213
# Docs Requirements
1314
sphinx

0 commit comments

Comments
 (0)