Skip to content

Commit a7d6b8c

Browse files
jrussettebroberson
andauthored
Add support for comma-delimited destinations in ASGs (#3644)
[#186770494](https://www.pivotaltracker.com/story/show/186770494) Co-authored-by: Brandon Roberson <[email protected]>
1 parent ac514a2 commit a7d6b8c

File tree

9 files changed

+294
-69
lines changed

9 files changed

+294
-69
lines changed

app/messages/validators/security_group_rule_validator.rb

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ def validate(record)
2727

2828
add_rule_error("protocol must be 'tcp', 'udp', 'icmp', or 'all'", record, index) unless valid_protocol(rule[:protocol])
2929

30-
validate_destination(rule[:destination], record, index) if valid_destination_type(rule[:destination], record, index)
30+
if valid_destination_type(rule[:destination], record, index)
31+
rule[:destination].split(',').each do |d|
32+
validate_destination(d, record, index)
33+
end
34+
end
35+
3136
validate_description(rule, record, index)
3237
validate_log(rule, record, index)
3338

@@ -80,85 +85,66 @@ def validate_icmp_protocol(rule, record, index)
8085
end
8186

8287
def valid_icmp_format(field)
83-
field.is_a?(Integer) && field >= -1 && field <= 255
88+
CloudController::ICMPRuleValidator.validate_icmp_control_message(field)
8489
end
8590

8691
def valid_ports(ports)
8792
return false unless ports.is_a?(String)
88-
return false if /[^\d\s\-,]/.match?(ports)
89-
90-
port_range = /\A\s*(\d+)\s*-\s*(\d+)\s*\z/.match(ports)
91-
if port_range
92-
left = port_range.captures[0].to_i
93-
right = port_range.captures[1].to_i
94-
95-
return false if left >= right
96-
return false unless port_in_range(left) && port_in_range(right)
97-
98-
return true
99-
end
100-
101-
port_list = ports.split(',')
102-
unless port_list.empty?
103-
return false unless port_list.all? { |p| /\A\s*\d+\s*\z/.match(p) }
104-
return false unless port_list.all? { |p| port_in_range(p.to_i) }
10593

106-
return true
107-
end
108-
109-
false
110-
end
111-
112-
def port_in_range(port)
113-
port > 0 && port < 65_536
94+
CloudController::TransportRuleValidator.validate_port(ports)
11495
end
11596

11697
def valid_destination_type(destination, record, index)
98+
error_message = 'destination must be a valid CIDR, IP address, or IP address range'
99+
if CloudController::RuleValidator.comma_delimited_destinations_enabled?
100+
error_message = 'nil destination; destination must be a comma-delimited list of valid CIDRs, IP addresses, or IP address ranges'
101+
end
102+
117103
if destination.nil?
118-
add_rule_error('destination must be a valid CIDR, IP address, or IP address range', record, index)
104+
add_rule_error(error_message, record, index)
119105
return false
120106
end
107+
121108
unless destination.is_a?(String)
122109
add_rule_error('destination must be a string', record, index)
123110
return false
124111
end
112+
125113
if /\s/ =~ destination
126114
add_rule_error('destination must not contain whitespace', record, index)
127115
return false
128116
end
129117

118+
if !CloudController::RuleValidator.comma_delimited_destinations_enabled? && !destination.index(',').nil?
119+
add_rule_error(error_message, record, index)
120+
return false
121+
end
122+
130123
true
131124
end
132125

133126
def validate_destination(destination, record, index)
134-
address_list = destination.split('-')
135127
error_message = 'destination must be a valid CIDR, IP address, or IP address range'
128+
error_message = 'destination must contain valid CIDR(s), IP address(es), or IP address range(s)' if CloudController::RuleValidator.comma_delimited_destinations_enabled?
129+
130+
address_list = destination.split('-')
136131

137132
if address_list.length == 1
138-
add_rule_error(error_message, record, index) unless parse_ip(address_list.first)
133+
add_rule_error(error_message, record, index) unless CloudController::RuleValidator.parse_ip(address_list.first)
139134

140135
elsif address_list.length == 2
141-
ipv4s = parse_ip(address_list)
142-
return add_rule_error(error_message, record, index) unless ipv4s
136+
ipv4s = CloudController::RuleValidator.parse_ip(address_list)
137+
return add_rule_error('destination IP address range is invalid', record, index) unless ipv4s
143138

144139
sorted_ipv4s = NetAddr.sort_IPv4(ipv4s)
145-
add_rule_error(error_message, record, index) unless ipv4s.first == sorted_ipv4s.first
140+
reversed_range_error = 'beginning of IP address range is numerically greater than the end of its range (range endpoints are inverted)'
141+
add_rule_error(reversed_range_error, record, index) unless ipv4s.first == sorted_ipv4s.first
146142

147143
else
148144
add_rule_error(error_message, record, index)
149145
end
150146
end
151147

152-
def parse_ip(val)
153-
if val.is_a?(Array)
154-
val.map { |ip| NetAddr::IPv4.parse(ip) }
155-
else
156-
NetAddr::IPv4Net.parse(val)
157-
end
158-
rescue NetAddr::ValidationError
159-
nil
160-
end
161-
162148
def add_rule_error(message, record, index)
163149
record.errors.add("Rules[#{index}]:", message)
164150
end

config/cloud_controller.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,9 @@ default_staging_security_groups:
261261
default_running_security_groups:
262262
- dummy4
263263

264+
security_groups:
265+
enable_comma_delimited_destinations: false
266+
264267
allowed_cors_domains:
265268
- http://*.appspot.com
266269
- http://*.inblue.net

docs/v3/source/includes/api_resources/_security_groups.erb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
"type": 8,
2121
"code": 0,
2222
"description": "Allow ping requests to private services"
23+
},
24+
{
25+
"protocol": "tcp",
26+
"destination": "1.1.1.1,2.2.2.2/24,10.0.0.0-10.0.0.255",
27+
"ports": "80,443,8080",
28+
"description": "Only valid if cc.security_groups.enable_comma_delimited_destinations is true"
2329
}
2430
],
2531
"relationships": {

docs/v3/source/includes/resources/security_groups/_object.md.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Name | Type | Description
2626
| Name | Type | Description | Required | Default
2727
| ---- | ---- | ----------- | -------- | -------
2828
| **protocol** | _string_ | Protocol type Valid values are `tcp`, `udp`, `icmp`, or `all` | yes | N/A |
29-
| **destination** | _string_ | Destinations that the rule applies to Valid CIDR, IP address, or IP address range | yes | N/A |
29+
| **destination** | _string_ | The destination where the rule applies. Must be a singular Valid CIDR, IP address, or IP address range unless `cc.security_groups.enable_comma_delimited_destinations` is enabled. Then, the destination can be a comma-delimited string of CIDRs, IP addresses, or IP address ranges. | yes | N/A |
3030
| **ports** | _string_ | Ports that the rule applies to; can be a single port (`9000`), a comma-separated list (`9000,9001`), or a range (`9000-9200`) | no | `null` |
3131
| **type** | _integer_ |[Type](https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types) required for ICMP protocol; valid values are between -1 and 255 (inclusive), where -1 allows all | no | `null` |
3232
| **code** | _integer_ |[Code](https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes) required for ICMP protocol; valid values are between -1 and 255 (inclusive), where -1 allows all | no | `null` |

lib/cloud_controller/config_schemas/base/api_schema.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ class ApiSchema < VCAP::Config
145145
default_staging_security_groups: [String],
146146
default_running_security_groups: [String],
147147

148+
security_groups: {
149+
enable_comma_delimited_destinations: bool
150+
},
151+
148152
resource_pool: {
149153
maximum_size: Integer,
150154
minimum_size: Integer,

lib/cloud_controller/rule_validator.rb

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def self.validate(rule)
1010
return errs unless errs.empty?
1111

1212
destination = rule['destination']
13-
errs << 'contains invalid destination' unless validate_destination(destination)
13+
errs << 'contains invalid destination' unless validate_destination_type(destination) && validate_destination(destination)
1414

1515
errs << 'contains invalid log value' if rule.key?('log') && !validate_boolean(rule['log'])
1616

@@ -24,31 +24,59 @@ def self.validate_fields(rule)
2424
(rule.keys - (required_fields + optional_fields)).map { |key| "contains the invalid field '#{key}'" }
2525
end
2626

27+
def self.validate_destination_type(destination)
28+
return false if destination.empty?
29+
30+
return false unless destination.is_a?(String)
31+
32+
return false if /\s/ =~ destination
33+
34+
true
35+
end
36+
2737
def self.validate_destination(destination)
28-
return false if destination.empty? || /\s/ =~ destination
38+
unless destination.index(',').nil?
39+
return false unless comma_delimited_destinations_enabled?
2940

30-
address_list = destination.split('-')
41+
destinations = destination.partition(',')
42+
first_destination = destinations.first
43+
remainder = destinations.last
44+
return validate_destination(first_destination) && validate_destination(remainder)
45+
end
3146

32-
return false if address_list.length > 2
47+
address_list = destination.split('-')
3348

3449
if address_list.length == 1
35-
NetAddr::IPv4Net.parse(address_list.first)
36-
return true
37-
end
50+
return true if parse_ip(address_list.first)
3851

39-
ipv4s = address_list.map do |address|
40-
NetAddr::IPv4.parse(address)
52+
elsif address_list.length == 2
53+
ipv4s = parse_ip(address_list)
54+
return false if ipv4s.nil?
55+
56+
sorted_ipv4s = NetAddr.sort_IPv4(ipv4s)
57+
return true if ipv4s.first == sorted_ipv4s.first
4158
end
42-
sorted_ipv4s = NetAddr.sort_IPv4(ipv4s)
43-
return true if ipv4s.first == sorted_ipv4s.first
4459

45-
false
46-
rescue NetAddr::ValidationError
4760
false
4861
end
4962

5063
def self.validate_boolean(bool)
5164
!!bool == bool
5265
end
66+
67+
def self.parse_ip(val)
68+
if val.is_a?(Array)
69+
val.map { |ip| NetAddr::IPv4.parse(ip) }
70+
else
71+
NetAddr::IPv4Net.parse(val)
72+
end
73+
rescue NetAddr::ValidationError
74+
nil
75+
end
76+
77+
def self.comma_delimited_destinations_enabled?
78+
config = VCAP::CloudController::Config.config
79+
config.get(:security_groups, :enable_comma_delimited_destinations)
80+
end
5381
end
5482
end

spec/unit/messages/security_group_create_message_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,34 @@ module VCAP::CloudController
145145
end
146146
end
147147

148+
context 'when comma-delimited destinations are enabled' do
149+
before do
150+
TestConfig.config[:security_groups][:enable_comma_delimited_destinations] = true
151+
end
152+
153+
context 'when rules are valid' do
154+
let(:rules) do
155+
[
156+
{
157+
protocol: 'tcp',
158+
destination: '10.10.10.0/24,1.0.0.0-1.0.0.200',
159+
ports: '443,80,8080'
160+
},
161+
{
162+
protocol: 'icmp',
163+
destination: '10.10.10.0/24,1.1.1.1',
164+
type: 8,
165+
code: 0
166+
}
167+
]
168+
end
169+
170+
it 'is valid' do
171+
expect(subject).to be_valid
172+
end
173+
end
174+
end
175+
148176
context 'when rules are invalid' do
149177
let(:rules) do
150178
[

0 commit comments

Comments
 (0)