def testIgmpMixed(self): self.naming.GetNetAddr.return_value = TEST_IPS acl = gce.GCE( policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_IGMP, self.naming), EXP_INFO) self.assertIn('2', str(acl)) self.assertIn('10.2.3.4/32', str(acl)) self.assertNotIn('2001:4860:8000::5/128', str(acl)) self.assertNotIn(gcp.GetIpv6TermName('good-term-pingv6'), str(acl))
def testMixedIsSeparateRules(self): self.naming.GetNetAddr.return_value = TEST_IPS self.naming.GetServiceByProto.side_effect = [['53'], ['53']] acl = gce.GCE( policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM + DEFAULT_DENY, self.naming), EXP_INFO) self.assertIn('INGRESS', str(acl)) self.assertNotIn('EGRESS', str(acl)) self.assertIn('2001:4860:8000::5/128', str(acl)) self.assertIn('10.2.3.4/32', str(acl)) self.assertIn('good-term-1', str(acl)) self.assertIn(gcp.GetIpv6TermName('good-term-1'), str(acl))
def testMixedWithEgressSourceTag(self): self.naming.GetNetAddr.return_value = TEST_IPS self.naming.GetServiceByProto.side_effect = [['53'], ['53']] acl = gce.GCE( policy.ParsePolicy( GOOD_HEADER_EGRESS_MIXED + GOOD_TERM_EGRESS_SOURCETAG, self.naming), EXP_INFO) self.assertNotIn('INGRESS', str(acl)) self.assertIn('EGRESS', str(acl)) self.assertIn('10.2.3.4/32', str(acl)) self.assertIn('2001:4860:8000::5/128', str(acl)) self.assertIn('dns-servers', str(acl)) self.assertIn(gcp.GetIpv6TermName('good-term-1-e'), str(acl))
def testMixedWithSourceTagAndV4Addresses(self): self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY self.naming.GetServiceByProto.side_effect = [['53'], ['53']] acl = gce.GCE( policy.ParsePolicy( GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG + DEFAULT_DENY, self.naming), EXP_INFO) self.assertIn('INGRESS', str(acl)) self.assertNotIn('EGRESS', str(acl)) self.assertNotIn('2001:4860:8000::5/128', str(acl)) self.assertIn('10.2.3.4/32', str(acl)) self.assertIn('internal-servers', str(acl)) self.assertNotIn(gcp.GetIpv6TermName('good-term-1'), str(acl))
def testMixedDefaultDenyIngressCreation(self): self.naming.GetNetAddr.return_value = TEST_IPS self.naming.GetServiceByProto.side_effect = [['53'], ['53']] acl = gce.GCE( policy.ParsePolicy( GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_SOURCETAG + DEFAULT_DENY, self.naming), EXP_INFO) self.assertIn('INGRESS', str(acl)) self.assertNotIn('EGRESS', str(acl)) self.assertIn('"priority": 65534', str(acl)) self.assertIn('default-deny', str(acl)) self.assertIn(gcp.GetIpv6TermName('default-deny'), str(acl)) self.assertIn('::/0', str(acl)) self.assertIn('0.0.0.0/0', str(acl))
def ConvertToDict(self, priority_index): """Converts term to dict representation of SecurityPolicy.Rule JSON format. Takes all of the attributes associated with a term (match, action, etc) and converts them into a dictionary which most closely represents the SecurityPolicy.Rule JSON format. Args: priority_index: An integer priority value assigned to the term. Returns: A dict term. """ if self.skip: return {} rules = [] # Identify if this is inet6 processing for a term under a mixed policy. mixed_policy_inet6_term = False if self.policy_inet_version == 'mixed' and self.address_family == 'inet6': mixed_policy_inet6_term = True term_dict = { 'action': self.ACTION_MAP.get(self.term.action[0], self.term.action[0]), 'direction': self.term.direction, 'priority': priority_index } # Get the correct syntax for API versions. src_ip_range = ApiVersionSyntaxMap.SYNTAX_MAP[ self.api_version]['src_ip_range'] dest_ip_range = ApiVersionSyntaxMap.SYNTAX_MAP[ self.api_version]['dest_ip_range'] layer_4_config = ApiVersionSyntaxMap.SYNTAX_MAP[ self.api_version]['layer_4_config'] target_resources = [] for proj, vpc in self.term.target_resources: target_resources.append( self._TARGET_RESOURCE_FORMAT.format(proj, vpc)) if target_resources: # Only set when non-empty. term_dict['targetResources'] = target_resources term_dict['enableLogging'] = self._GetLoggingSetting() # This combo provides ability to identify the rule. term_name = self.term.name if mixed_policy_inet6_term: term_name = gcp.GetIpv6TermName(term_name) raw_description = term_name + ': ' + ' '.join(self.term.comment) term_dict['description'] = gcp.TruncateString( raw_description, self._MAX_TERM_COMMENT_LENGTH) filtered_protocols = [] for proto in self.term.protocol: # ICMP filtering by inet_version # Since each term has inet_version, 'mixed' is correctly processed here. if proto == 'icmp' and self.address_family == 'inet6': logging.warning( 'WARNING: Term %s is being rendered for inet6, ICMP ' 'protocol will not be rendered.', self.term.name) continue if proto == 'icmpv6' and self.address_family == 'inet': logging.warning( 'WARNING: Term %s is being rendered for inet, ICMPv6 ' 'protocol will not be rendered.', self.term.name) continue if proto == 'igmp' and self.address_family == 'inet6': logging.warning( 'WARNING: Term %s is being rendered for inet6, IGMP ' 'protocol will not be rendered.', self.term.name) continue filtered_protocols.append(proto) # If there is no protocol left after ICMP/IGMP filtering, drop this term. # But only do this for terms that originally had protocols. # Otherwise you end up dropping the default-deny. if self.term.protocol and not filtered_protocols: return {} protocols_and_ports = [] if not self.term.protocol: # Empty protocol list means any protocol, but any protocol in HF is # represented as "all" protocols_and_ports = [{'ipProtocol': 'all'}] else: for proto in filtered_protocols: # If the protocol name is not supported, use the protocol number. if proto not in self._ALLOW_PROTO_NAME: proto = str(self.PROTO_MAP[proto]) logging.info( 'INFO: Term %s is being rendered using protocol number', self.term.name) proto_ports = {'ipProtocol': proto} if self.term.destination_port: ports = self._GetPorts() if ports: # Only set when non-empty. proto_ports['ports'] = ports protocols_and_ports.append(proto_ports) if self.api_version == 'ga': term_dict['match'] = {layer_4_config: protocols_and_ports} else: term_dict['match'] = { 'config': { layer_4_config: protocols_and_ports } } # match needs a field called versionedExpr with value FIREWALL # See documentation: # https://cloud.google.com/compute/docs/reference/rest/beta/organizationSecurityPolicies/addRule term_dict['match']['versionedExpr'] = 'FIREWALL' ip_version = self.AF_MAP[self.address_family] if ip_version == 4: any_ip = [nacaddr.IP('0.0.0.0/0')] else: any_ip = [nacaddr.IPv6('::/0')] if self.term.direction == 'EGRESS': daddrs = self.term.GetAddressOfVersion('destination_address', ip_version) # If the address got filtered out and is empty due to address family, we # don't render the term. At this point of term processing, the direction # has already been validated, so we can just log and return empty rule. if self.term.destination_address and not daddrs: logging.warning( 'WARNING: Term %s is not being rendered for %s, ' 'because there are no addresses of that family.', self.term.name, self.address_family) return [] # This should only happen if there were no addresses set originally. if not daddrs: daddrs = any_ip destination_address_chunks = [ daddrs[x:x + self._TERM_ADDRESS_LIMIT] for x in range(0, len(daddrs), self._TERM_ADDRESS_LIMIT) ] for daddr_chunk in destination_address_chunks: rule = copy.deepcopy(term_dict) if self.api_version == 'ga': rule['match'][dest_ip_range] = [ daddr.with_prefixlen for daddr in daddr_chunk ] else: rule['match']['config'][dest_ip_range] = [ daddr.with_prefixlen for daddr in daddr_chunk ] rule['priority'] = priority_index rules.append(rule) priority_index += 1 else: saddrs = self.term.GetAddressOfVersion('source_address', ip_version) # If the address got filtered out and is empty due to address family, we # don't render the term. At this point of term processing, the direction # has already been validated, so we can just log and return empty rule. if self.term.source_address and not saddrs: logging.warning( 'WARNING: Term %s is not being rendered for %s, ' 'because there are no addresses of that family.', self.term.name, self.address_family) return [] # This should only happen if there were no addresses set originally. if not saddrs: saddrs = any_ip source_address_chunks = [ saddrs[x:x + self._TERM_ADDRESS_LIMIT] for x in range(0, len(saddrs), self._TERM_ADDRESS_LIMIT) ] for saddr_chunk in source_address_chunks: rule = copy.deepcopy(term_dict) if self.api_version == 'ga': rule['match'][src_ip_range] = [ saddr.with_prefixlen for saddr in saddr_chunk ] else: rule['match']['config'][src_ip_range] = [ saddr.with_prefixlen for saddr in saddr_chunk ] rule['priority'] = priority_index rules.append(rule) priority_index += 1 return rules
def ConvertToDict(self): """Convert term to a dictionary. This is used to get a dictionary describing this term which can be output easily as a JSON blob. Returns: A dictionary that contains all fields necessary to create or update a GCE firewall. Raises: GceFirewallError: The term name is too long. """ if self.term.owner: self.term.comment.append('Owner: %s' % self.term.owner) term_dict = { 'description': ' '.join(self.term.comment), 'name': self.term.name, 'direction': self.term.direction } if self.term.network: term_dict['network'] = self.term.network term_dict['name'] = '%s-%s' % (self.term.network.split('/')[-1], term_dict['name']) # Identify if this is inet6 processing for a term under a mixed policy. mixed_policy_inet6_term = False if self.policy_inet_version == 'mixed' and self.inet_version == 'inet6': mixed_policy_inet6_term = True # Update term name to have the IPv6 suffix for the inet6 rule. if mixed_policy_inet6_term: term_dict['name'] = gcp.GetIpv6TermName(term_dict['name']) # Checking counts of tags, and ports to see if they exceeded limits. if len(self.term.source_tag) > self._TERM_SOURCE_TAGS_LIMIT: raise GceFirewallError( 'GCE firewall rule exceeded number of source tags per rule: %s' % self.term.name) if len(self.term.destination_tag) > self._TERM_TARGET_TAGS_LIMIT: raise GceFirewallError( 'GCE firewall rule exceeded number of target tags per rule: %s' % self.term.name) if self.term.source_tag: if self.term.direction == 'INGRESS': term_dict['sourceTags'] = self.term.source_tag elif self.term.direction == 'EGRESS': term_dict['targetTags'] = self.term.source_tag if self.term.destination_tag and self.term.direction == 'INGRESS': term_dict['targetTags'] = self.term.destination_tag if self.term.priority: term_dict['priority'] = self.term.priority # Update term priority for the inet6 rule. if mixed_policy_inet6_term: term_dict['priority'] = GetNextPriority(term_dict['priority']) rules = [] # If 'mixed' ends up in indvidual term inet_version, something has gone # horribly wrong. The only valid values are inet/inet6. term_af = self.AF_MAP.get(self.inet_version) if self.inet_version == 'mixed': raise GceFirewallError( 'GCE firewall rule has incorrect inet_version for rule: %s' % self.term.name) # Exit early for inet6 processing of mixed rules that have only tags, # and no IP addresses, since this is handled in the inet processing. if mixed_policy_inet6_term: if not self.term.source_address and not self.term.destination_address: if 'targetTags' in term_dict or 'sourceTags' in term_dict: return [] saddrs = sorted(self.term.GetAddressOfVersion('source_address', term_af), key=ipaddress.get_mixed_type_key) daddrs = sorted(self.term.GetAddressOfVersion('destination_address', term_af), key=ipaddress.get_mixed_type_key) # If the address got filtered out and is empty due to address family, we # don't render the term. At this point of term processing, the direction # has already been validated, so we can just log and return empty rule. if self.term.source_address and not saddrs: logging.warning( 'WARNING: Term %s is not being rendered for %s, ' 'because there are no addresses of that family.', self.term.name, self.inet_version) return [] if self.term.destination_address and not daddrs: logging.warning( 'WARNING: Term %s is not being rendered for %s, ' 'because there are no addresses of that family.', self.term.name, self.inet_version) return [] filtered_protocols = [] if not self.term.protocol: # Any protocol is represented as "all" filtered_protocols = ['all'] logging.info( 'INFO: Term %s has no protocol specified,' 'which is interpreted as "all" protocols.', self.term.name) proto_dict = copy.deepcopy(term_dict) if self.term.logging: proto_dict['logConfig'] = {'enable': True} for proto in self.term.protocol: # ICMP filtering by inet_version # Since each term has inet_version, 'mixed' is correctly processed here. # Convert protocol to number for uniformity of comparison. # PROTO_MAP always returns protocol number. if proto in self._ALLOW_PROTO_NAME: proto_num = self.PROTO_MAP[proto] else: proto_num = proto if proto_num == self.PROTO_MAP[ 'icmp'] and self.inet_version == 'inet6': logging.warning( 'WARNING: Term %s is being rendered for inet6, ICMP ' 'protocol will not be rendered.', self.term.name) continue if proto_num == self.PROTO_MAP[ 'icmpv6'] and self.inet_version == 'inet': logging.warning( 'WARNING: Term %s is being rendered for inet, ICMPv6 ' 'protocol will not be rendered.', self.term.name) continue if proto_num == self.PROTO_MAP[ 'igmp'] and self.inet_version == 'inet6': logging.warning( 'WARNING: Term %s is being rendered for inet6, IGMP ' 'protocol will not be rendered.', self.term.name) continue filtered_protocols.append(proto) # If there is no protocol left after ICMP/IGMP filtering, drop this term. if not filtered_protocols: return [] for proto in filtered_protocols: # If the protocol name is not supported, protocol number is used. # This is done by default in policy.py. if proto not in self._ALLOW_PROTO_NAME: logging.info( 'INFO: Term %s is being rendered using protocol number', self.term.name) dest = {'IPProtocol': proto} if self.term.destination_port: ports = [] for start, end in self.term.destination_port: if start == end: ports.append(str(start)) else: ports.append('%d-%d' % (start, end)) if len(ports) > self._TERM_PORTS_LIMIT: raise GceFirewallError( 'GCE firewall rule exceeded number of ports per rule: %s' % self.term.name) dest['ports'] = ports action = self.ACTION_MAP[self.term.action[0]] dict_val = [] if action in proto_dict: dict_val = proto_dict[action] if not isinstance(dict_val, list): dict_val = [dict_val] dict_val.append(dest) proto_dict[action] = dict_val # There's a limit of 256 addresses each term can contain. # If we're above that limit, we're breaking it down in more terms. if saddrs: source_addr_chunks = [ saddrs[x:x + self._TERM_ADDRESS_LIMIT] for x in range(0, len(saddrs), self._TERM_ADDRESS_LIMIT) ] for i, chunk in enumerate(source_addr_chunks): rule = copy.deepcopy(proto_dict) if len(source_addr_chunks) > 1: rule['name'] = '%s-%d' % (rule['name'], i + 1) rule['sourceRanges'] = [str(saddr) for saddr in chunk] rules.append(rule) elif daddrs: dest_addr_chunks = [ daddrs[x:x + self._TERM_ADDRESS_LIMIT] for x in range(0, len(daddrs), self._TERM_ADDRESS_LIMIT) ] for i, chunk in enumerate(dest_addr_chunks): rule = copy.deepcopy(proto_dict) if len(dest_addr_chunks) > 1: rule['name'] = '%s-%d' % (rule['name'], i + 1) rule['destinationRanges'] = [str(daddr) for daddr in chunk] rules.append(rule) else: rules.append(proto_dict) # Sanity checking term name lengths. long_rules = [rule['name'] for rule in rules if len(rule['name']) > 63] if long_rules: raise GceFirewallError( 'GCE firewall name ended up being too long: %s' % long_rules) return rules
def testGetIpv6TermName(self, term_name, expected): self.assertEqual(expected, gcp.GetIpv6TermName(term_name))