def test_eq(self): default_namespace = K8sNamespace('default') pod_a = Pod('A', default_namespace) ip1 = IpBlock("1.2.3.0/24") ip2 = IpBlock("1.2.3.0/32") ip3 = ip1 - ip2 set1 = PeerSet({pod_a, ip1}) set2 = PeerSet({pod_a, ip2, ip3}) self.assertTrue(set1 == set2)
def get_all_global_peers(self): """ Return all global peers known in the system :return PeerSet: The required set of peers """ res = PeerSet() for peer in self.peer_set: if peer.is_global_peer(): res.add(peer) return res
def test_subtract(self): self.assertTrue(True) default_namespace = K8sNamespace('default') pod_a = Pod('A', default_namespace) ip1 = IpBlock("1.2.3.0/24") ip2 = IpBlock("1.2.3.0/32") ip3 = ip1 - ip2 set1 = PeerSet({pod_a, ip1}) set2 = PeerSet({pod_a, ip2}) set3 = PeerSet({ip3}) self.assertTrue(set1-set2 == set3)
def get_pods_with_service_account_name(self, sa_name, namespace_str): """ Return all pods that are with a service account name in a given namespace :param sa_name: string the service account name :param namespace_str: string the namespace str :rtype PeerSet """ res = PeerSet() for peer in self.peer_set: if isinstance(peer, Pod) and peer.service_account_name == sa_name and peer.namespace.name == namespace_str: res.add(peer) return res
def get_namespace_pods(self, namespace): """ Return all pods that are in a given namespace :param K8sNamespace namespace: The target namespace :return PeerSet: All pods in the namespace """ if namespace is None: return self.get_all_peers_group() res = PeerSet() for peer in self.peer_set: if peer.namespace == namespace: res.add(peer) return res
def test_or(self): default_namespace = K8sNamespace('default') pod_a = Pod('A', default_namespace) pod_b = Pod('B', default_namespace) ip1 = IpBlock("1.2.3.0/24") ip2 = IpBlock("1.2.3.0/32") ip3 = ip1 - ip2 set1 = PeerSet({pod_a, pod_b, ip1}) set2 = PeerSet({pod_a, ip2}) set3 = PeerSet({pod_b, ip3}) self.assertTrue(set2 | set3 == set1) set2 |= set3 self.assertTrue(set2 == set1)
def get_namespace_pods_with_key(self, key, does_not_exist): """ Return all pods in namespaces that have a given key :param str key: The relevant key :param bool does_not_exist: whether to check for the inexistence of this key :return PeerSet: All pods in namespace with (or without) the given key """ res = PeerSet() for peer in self.peer_set: if peer.namespace is None: continue if (key in peer.namespace.labels) ^ does_not_exist: res.add(peer) return res
def test_and(self): default_namespace = K8sNamespace('default') pod_a = Pod('A', default_namespace) pod_b = Pod('B', default_namespace) ip1 = IpBlock("1.2.3.0/24") ip2 = IpBlock("1.2.3.0/32") pod_set_1 = {pod_a, pod_b, ip2} pod_set_2 = {pod_a, ip1} a = PeerSet(pod_set_1) b = PeerSet(pod_set_2) res1 = a & b self.assertTrue(res1 == PeerSet({pod_a, ip2})) a &= b self.assertTrue(a == res1)
def get_peers_with_key(self, namespace, key, does_not_exist): """ Return all peers (possibly in a given namespace) that have a specific key in their labels :param K8sNamespace namespace: If not none - only include peers in this namespace :param str key: The relevant key :param bool does_not_exist: Whether to only include peers that do not have this key :return PeerSet: All peers that (do not) have the key """ res = PeerSet() for peer in self.peer_set: if namespace is not None and peer.namespace != namespace: continue if (key in peer.labels or key in peer.extra_labels) ^ does_not_exist: res.add(peer) return res
def parse_label_selector(self, label_selector): """ Parse a LabelSelector element :param dict label_selector: The element to parse :return: A PeerSet containing all the pods captured by this selection :rtype: Peer.PeerSet """ if label_selector is None: return PeerSet() # A None value means the selector selects nothing if not label_selector: return self.peer_container.get_all_peers_group( ) # An empty value means the selector selects everything allowed_elements = {'matchLabels': [0, dict]} self.check_fields_validity(label_selector, 'authorization policy WorkloadSelector', allowed_elements) res = self.peer_container.get_all_peers_group() match_labels = label_selector.get('matchLabels') if match_labels: for key, val in match_labels.items(): res &= self.peer_container.get_peers_with_label(key, [val]) self.allowed_labels.add(':'.join(match_labels.keys())) if not res: self.warning( 'A podSelector selects no pods. Better use "podSelector: Null"', label_selector) return res
def __init__(self, ns_resources=None, peer_resources=None, config_name='global'): """ create a PeerContainer object :param list ns_resources: the list of namespace resources :param list peer_resources: the list of peer resources :param str config_name: the config name """ self.peer_set = PeerSet() self.namespaces = { } # mapping from namespace name to the actual K8sNamespace object self.representative_peers = {} if ns_resources: self._set_namespace_list(ns_resources) if peer_resources: self._set_peer_list(peer_resources, config_name)
def parse_source(self, source_dict): """ Parse a source peer inside a rule (an element of the 'from' array) :param dict source_dict: The object to parse :return: A PeerSet object containing the set of peers defined by the selectors/ipblocks :rtype: Peer.PeerSet """ from_allowed_elements = {'source': [1, dict]} self.check_fields_validity(source_dict, 'authorization policy rule: from', from_allowed_elements) source_peer = source_dict.get('source') if source_peer is None: self.syntax_error('Authorization policy from.source cannot be null. ') # TODO: support source with multiple attributes ("fields in the source are ANDed together") # TODO: add support for allowed elements currently unsupported (principals, requestPrincipals, remoteIpBlocks) allowed_elements = {'namespaces': [0, list], 'notNamespaces': [0, list], 'ipBlocks': [0, list], 'notIpBlocks': [0, list], 'principals': [0, list], 'notPrincipals': [0, list], 'requestPrincipals': 2, 'notRequestPrincipals': 2, 'remoteIpBlocks': 2, 'notRemoteIpBlocks': 2} # TODO: though specified 'list' value_type, check_fields_validity doesn't fail since value is None (empty)... self.check_fields_validity(source_peer, 'authorization policy rule: source', allowed_elements) for key_elem in allowed_elements: self.validate_existing_key_is_not_null(source_peer, key_elem) self.validate_dict_elem_has_non_empty_array_value(source_peer, 'from.source') has_ns = 'namespaces' in source_peer or 'notNamespaces' in source_peer has_ip = 'ipBlocks' in source_peer or 'notIpBlocks' in source_peer has_principals = 'principals' in source_peer or 'notPrincipals' in source_peer # TODO: how to support a source peer with both namespace and ip-block properties? # currently assuming ip-block is only outside the cluster if has_ip and (has_principals or has_ns): self.warning('currently not supporting source with both namespaces/principals and ip block') # TODO: should return empty peerSet if has requirements of both ns and ip-block ? return PeerSet() res = self.peer_container.get_all_peers_group(True) if has_principals: principals_list = source_peer.get('principals') not_principals_list = source_peer.get('notPrincipals') res &= self.parse_principals(principals_list, not_principals_list) if has_ns: ns_list = source_peer.get('namespaces') not_ns_list = source_peer.get('notNamespaces') res &= self.parse_namespaces(ns_list, not_ns_list) elif has_ip: ip_blocks = source_peer.get('ipBlocks') not_ip_blocks = source_peer.get('notIpBlocks') res &= self.parse_ip_block(ip_blocks, not_ip_blocks) return res
def parse_ingress_rule(self, rule): """ Parse a single ingress rule, producing a IstioPolicyRule. :param dict rule: The dict with the rule fields :return: A IstioPolicyRule with the proper PeerSet and ConnectionSet :rtype: IstioPolicyRule """ if rule is None: self.syntax_error('Authorization policy rule cannot be null. ') allowed_elements = { 'from': [0, list], 'to': [0, list], 'when': [0, list] } self.check_fields_validity(rule, 'authorization policy rule', allowed_elements) for key_elem in allowed_elements: self.validate_existing_key_is_not_null(rule, key_elem) # collect source peers into res_peers from_array = self.get_key_array_and_validate_not_empty(rule, 'from') if from_array is not None: res_peers = PeerSet() for source_dict in from_array: res_peers |= self.parse_source(source_dict) else: # no 'from' in the rule => all source peers allowed res_peers = self.peer_container.get_all_peers_group(True) to_array = self.get_key_array_and_validate_not_empty(rule, 'to') # currently parsing only ports # TODO: extend operations parsing to include other attributes if to_array is not None: connections = ConnectionSet() for operation_dict in to_array: connections |= self.parse_operation(operation_dict) else: # no 'to' in the rule => all connections allowed connections = ConnectionSet(True) # condition possible result value: source-ip (from) , source-namespace (from) [Peerset], destination.port (to) [ConnectionSet] # should update either res_pods or connections according to the condition condition_array = rule.get( 'when') # this array can be empty (unlike 'to' and 'from') # the combined condition ("AND" of all conditions) should be applied if condition_array is not None: for condition in condition_array: condition_res = self.parse_condition(condition) if isinstance(condition_res, PeerSet): res_peers &= condition_res elif isinstance(condition_res, ConnectionSet): connections &= condition_res if not res_peers: self.warning('Rule selects no pods', rule) return IstioPolicyRule(res_peers, connections)
def __init__(self, name, namespace): self.name = name self.namespace = namespace self.selected_peers = PeerSet() # The peers affected by this policy self.ingress_rules = [] self.egress_rules = [] self.affects_ingress = False # whether the policy affects the ingress of the selected peers self.affects_egress = False # whether the policy affects the egress of the selected peers self.findings = [ ] # accumulated findings which are relevant only to this policy (emptiness and redundancy)
def referenced_ip_blocks(self): """ :return: A set of all ipblocks referenced in one of the policy rules (one Peer object per one ip range) :rtype: Peer.PeerSet """ res = PeerSet() for rule in self.ingress_rules: for peer in rule.peer_set: if isinstance(peer, IpBlock): res |= peer.split() return res
def get_pods_with_service_name_containing_given_string(self, name_substring): """ Returns all pods that belong to services whose name contains the given substring :param str name_substring: the service name substring :return: PeerSet """ res = PeerSet() for key, val in self.services.items(): if name_substring in key: res |= val.target_pods return res
def __init__(self, policy, peer_container, ingress_file_name=''): """ :param dict policy: The ingress policy object as provided by the yaml parser :param PeerContainer peer_container: The ingress policy will be evaluated against this set of peers :param str ingress_file_name: The name of the ingress resource file """ GenericYamlParser.__init__(self, ingress_file_name) self.policy = policy self.peer_container = peer_container self.namespace = None self.default_backend_peers = PeerSet() self.default_backend_ports = PortSet()
def _set_services_and_populate_target_pods(self, service_list): """ Populates services from the given service list, and for every service computes and populates its target pods. :param list service_list: list of service in K8sService format :return: None """ for srv in service_list: # populate target ports if srv.selector: srv.target_pods = self.peer_set for key, val in srv.selector.items(): srv.target_pods &= self.get_peers_with_label(key, [val], GenericYamlParser.FilterActionType.In, srv.namespace) # remove target_pods that don't contain named ports referenced by target_ports for port in srv.ports.values(): if not isinstance(port.target_port, str): continue # check if all pods include this named port, and remove those that don't pods_to_remove = PeerSet() for pod in srv.target_pods: pod_named_port = pod.named_ports.get(port.target_port) if not pod_named_port: print(f'Warning: The named port {port.target_port} referenced in Service {srv.name}' f' is not defined in the pod {pod}. Ignoring the pod') pods_to_remove.add(pod) elif pod_named_port[1] != port.protocol: print(f'Warning: The protocol {port.protocol} in the named port {port.target_port} ' f'referenced in Service {srv.name} does not match the protocol {pod_named_port[1]} ' f'defined in the pod {pod}. Ignoring the pod') pods_to_remove.add(pod) srv.target_pods -= pods_to_remove if not srv.target_pods: print(f'Warning: The service {srv.name} does not reference any pod') self.services[srv.full_name()] = srv
def _recursive_parse_label_selector(self, label_selector, origin_map, namespace, namespace_selector): """ Recursive Parse a label selector expression appearing in selector/notSelector/namespaceSelector parts of the EntityRule :param str label_selector: The selector expression to parse :param dict origin_map: The EntityRule object (for reporting errors) :param K8sNamespace namespace: Restrict pods to the given namespace :param bool namespace_selector: True if this is a namespaceSelector :return: the set of peers selected by the selector expression :rtype: PeerSet """ include_globals = not namespace_selector or 'global()' in label_selector label_selector = self._strip_selector(origin_map, label_selector) # We are handling the operators according to the "order of operation" - '!', '&&', '||'. # i.e. we will first try to spit the label by '||', # if the label does not contain '||', we will split by '&&', # if the label does not contain &&, we will look for the prefix '!', # and if there is no '!', we will evaluate the expression. # handling '||' : split_expr = self._split_selector(label_selector, '||') if len(split_expr) != 1: res = PeerSet() for expr in split_expr: res |= self._recursive_parse_label_selector( expr, origin_map, namespace, namespace_selector) return res # there is no '||', handling '&&' : split_expr = self._split_selector(label_selector, '&&') if len(split_expr) != 1: res = self.peer_container.get_all_peers_group( include_globals=include_globals) for expr in split_expr: res &= self._recursive_parse_label_selector( expr, origin_map, namespace, namespace_selector) return res # there is no '&&', handling '!' : if label_selector[0] == '!': if namespace_selector: all_peers = self.peer_container.get_all_peers_group( include_globals=include_globals) else: all_peers = self.peer_container.get_namespace_pods(namespace) return all_peers - self._recursive_parse_label_selector( label_selector[1:], origin_map, namespace, namespace_selector) # there is no operator, parsing the expression: return self._parse_selector_expr(split_expr[0], origin_map, namespace, namespace_selector)
def parse_principals(self, principals_list, not_principals_list): """ Parse a principals element (within a source component of a rule) :param list[str] principals_list: list of principals patterns/strings :param list[str] not_principals_list: negative list of principals patterns/strings :return: A PeerSet containing the relevant pods :rtype: Peer.PeerSet """ res = PeerSet() if principals_list is not None else self.peer_container.get_all_peers_group() for principal in principals_list or []: res |= self._parse_principal_str(principal, principals_list) for principal in not_principals_list or []: res -= self._parse_principal_str(principal, not_principals_list) return res
def _parse_ns_str(self, ns): """ parse a namespace string from source component in rule :param str ns: the namespace string :return: PeerSet: All pods in the namespace ns """ ns_str_values = self._parse_istio_regex_from_enumerated_domain(ns, 'namespaces') res = PeerSet() if not ns_str_values: self.warning(f"no match for namespace: {ns}") for ns_str in ns_str_values: ns_obj = self.peer_container.get_namespace(ns_str) res |= self.peer_container.get_namespace_pods(ns_obj) return res
def _parse_principal_str(self, principal, principals_list): """ parse a principal string from source component in rule :param str principal: the principal str, currently assuming in format: "cluster.local/ns/<ns-str>/sa/<sa-str>" :param list principals_list: The principals object (for reporting warnings) :return: PeerSet: All pods with the given ns + sa_name as extracted from principal str """ principal_str_values = self._parse_istio_regex_from_enumerated_domain(principal, 'principals') res = PeerSet() if not principal_str_values: self.warning(f"no match for principal: {principal}", principals_list) for principal_str in principal_str_values: ns, sa_name = self._get_principal_str_components(principal_str) if ns and sa_name: res |= self.peer_container.get_pods_with_service_account_name(sa_name, ns) return res
def _get_rule_peers(self, entity_rule): """ Parse the peer-specifying parts of the source/destination parts of a rule :param dict entity_rule: The object to parse :return: The peers that are specified by nets/notNets/selector/notSelector/namespaceSelector :rtype: PeerSet """ nets = entity_rule.get('nets') if nets: rule_ips = IpBlock(nets[0]) for cidr in nets[1:]: rule_ips |= IpBlock(cidr) else: rule_ips = IpBlock.get_all_ips_block() not_nets = entity_rule.get('notNets', []) for cidr in not_nets: rule_ips -= IpBlock(cidr) ns_selector = self._get_value_as_str(entity_rule, 'namespaceSelector') pod_selector = self._get_value_as_str(entity_rule, 'selector') not_pod_selector = self._get_value_as_str(entity_rule, 'notSelector') if ns_selector: rule_peers = self._parse_label_selector(ns_selector, entity_rule, namespace_selector=True) elif pod_selector: rule_peers = self.peer_container.get_namespace_pods(self.namespace) elif nets or not_nets: rule_peers = PeerSet() rule_peers.add(rule_ips) else: rule_peers = self.peer_container.get_all_peers_group(True) ns_to_use = self.namespace if not ns_selector else None if pod_selector is not None: selected_pods = self._parse_label_selector(pod_selector, entity_rule, ns_to_use) if pod_selector.strip() != 'all()' and selected_pods == rule_peers: self.warning( 'selector has no effect - better delete or use "all()"', entity_rule) rule_peers &= selected_pods if not_pod_selector: rule_peers -= self._parse_label_selector(not_pod_selector, entity_rule, ns_to_use) if (nets or not_nets) and (ns_selector or pod_selector): rule_peers = PeerSet() self.warning( 'Mixing ip-based selection with label-based selection is likely a mistake', entity_rule) return rule_peers
def parse_namespaces(self, ns_list, not_ns_list): """ Parse a namespaces element (within a source component of a rule) :param list[str] ns_list: list of namespaces patterns/strings :param list[str] not_ns_list: negative list of namespaces patterns/strings :return: A PeerSet containing the relevant pods :rtype: Peer.PeerSet """ # If 'namespaces' not set, any namespace is allowed. ns_list = self.peer_container.get_all_namespaces_str_list() if ns_list is None else ns_list not_ns_list = [] if not_ns_list is None else not_ns_list res = PeerSet() for ns in ns_list: res |= self._parse_ns_str(ns) for ns in not_ns_list: res -= self._parse_ns_str(ns) return res
def _make_rules_from_conns(self, tcp_conns): """ Make IngressPolicyRules from the given connections :param TcpLikeProperties tcp_conns: the given connections :return: the list of IngressPolicyRules """ peers_to_conns = dict() res = [] # extract peers dimension from cubes for cube in tcp_conns: ports = None paths = None hosts = None peer_set = None for i, dim in enumerate(tcp_conns.active_dimensions): if dim == "dst_ports": ports = cube[i] elif dim == "paths": paths = cube[i] elif dim == "hosts": hosts = cube[i] elif dim == "peers": peer_set = PeerSet( set( tcp_conns.base_peer_set.get_peer_list_by_indices( cube[i]))) else: assert False if not peer_set: peer_set = self.peer_container.peer_set.copy() port_set = PortSet() port_set.port_set = ports port_set.named_ports = tcp_conns.named_ports port_set.excluded_named_ports = tcp_conns.excluded_named_ports new_conns = self._get_connection_set_from_properties( port_set, paths_dfa=paths, hosts_dfa=hosts) if peers_to_conns.get(peer_set): peers_to_conns[ peer_set] |= new_conns # optimize conns for the same peers else: peers_to_conns[peer_set] = new_conns for peer_set, conns in peers_to_conns.items(): res.append(IngressPolicyRule(peer_set, conns)) return res
def get_all_peers_group(self, add_external_ips=False, include_globals=True): """ Return all peers known in the system :param bool add_external_ips: Whether to also add the full range of ips :param bool include_globals: Whether to include global peers :return PeerSet: The required set of peers """ res = PeerSet() for peer in self.peer_set: if include_globals or not peer.is_global_peer(): res.add(peer) if add_external_ips: res.add(IpBlock.get_all_ips_block()) return res
def get_profile_pods(self, profile_name, first_profile_only): """ Return all the pods that have a specific profile assigned :param str profile_name: The name of the target profile :param bool first_profile_only: whether to only consider the first profile of each pod :return PeerSet: The set of pods with the given profile """ res = PeerSet() for peer in self.peer_set: if peer.has_profiles(): if first_profile_only: if peer.get_first_profile_name() == profile_name: res.add(peer) else: if profile_name in peer.profiles: res.add(peer) return res
def test_get_peer_set(self): ip1 = IpBlock("1.2.3.0/24") ip1_set = PeerSet({ip1}) self.assertTrue(ip1_set == ip1.get_peer_set()) self.assertTrue(PeerSet() == (ip1-ip1).get_peer_set())
def get_namespace_pods_with_label(self, key, values, action=FilterActionType.In): """ Return all pods in namespace with a given key-value label :param str key: The relevant key :param list[str] values: possible values for the key :param FilterActionType action: how to filter the values :return PeerSet: All pods in namespaces that have (or not) the given key-value label """ res = PeerSet() for peer in self.peer_set: if peer.namespace is None: continue # Note: It seems as if the semantics of NotIn is "either key does not exist, or its value is not in values" # Reference: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ if action == self.FilterActionType.In: if peer.namespace.labels.get(key, '') in values: res.add(peer) elif action == self.FilterActionType.NotIn: if peer.namespace.labels.get(key, '') not in values: res.add(peer) elif action == self.FilterActionType.Contain: if values[0] in peer.namespace.labels.get(key, ''): res.add(peer) elif action == self.FilterActionType.StartWith: if peer.namespace.labels.get(key, '').startswith(values[0]): res.add(peer) elif action == self.FilterActionType.EndWith: if peer.namespace.labels.get(key, '').endswith(values[0]): res.add(peer) return res
def get_peers_with_label(self, key, values, action=FilterActionType.In, namespace=None): """ Return all peers that have a specific key-value label (in a specific namespace) :param str key: The relevant key :param list[str] values: A list of possible values to match :param FilterActionType action: how to filter the values :param K8sNamespace namespace: If not None, only consider peers in this namespace :return PeerSet: All peers that (do not) have the key-value as their label """ res = PeerSet() for peer in self.peer_set: # Note: It seems as if the semantics of NotIn is "either key does not exist, or its value is not in values" # Reference: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ if namespace is not None and peer.namespace != namespace: continue if action == self.FilterActionType.In: if peer.labels.get(key) in values or peer.extra_labels.get( key) in values: res.add(peer) elif action == self.FilterActionType.NotIn: if peer.labels.get( key) not in values and peer.extra_labels.get( key) not in values: res.add(peer) elif action == self.FilterActionType.Contain: if values[0] in peer.labels.get( key, '') or values[0] in peer.extra_labels.get( key, ''): res.add(peer) elif action == self.FilterActionType.StartWith: if peer.labels.get(key, '').startswith( values[0]) or peer.extra_labels.get( key, '').startswith(values[0]): res.add(peer) elif action == self.FilterActionType.EndWith: if peer.labels.get( key, '').endswith(values[0]) or peer.extra_labels.get( key, '').endswith(values[0]): res.add(peer) return res