diff --git a/HISTORY.rst b/HISTORY.rst index 300c93a6..22dc0921 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,25 @@ History ======= +0.14.0 +------ + +Released: 2020-01-14 + +Status: Alpha + +New Classes: + +- `objects.DynamicUserGroup` +- `policies.PolicyBasedForwarding` + +Other Updates: + +- Added dynamic user group (DUG) support to the userid namespace +- Fixes to `network.AggregateInterface` +- Removed default value from `network.IkeGateway.peer_id_check` +- Docstring updates + 0.13.0 ------ diff --git a/pandevice/__init__.py b/pandevice/__init__.py index 778d61a4..f7f100d8 100755 --- a/pandevice/__init__.py +++ b/pandevice/__init__.py @@ -23,7 +23,7 @@ __author__ = 'Palo Alto Networks' __email__ = 'techpartners@paloaltonetworks.com' -__version__ = '0.13.0' +__version__ = '0.14.0' import logging diff --git a/pandevice/device.py b/pandevice/device.py index 2cac9443..36bef3aa 100644 --- a/pandevice/device.py +++ b/pandevice/device.py @@ -118,6 +118,7 @@ class Vsys(VersionedPanObject): "objects.SecurityProfileGroup", "objects.CustomUrlCategory", "objects.LogForwardingProfile", + "objects.DynamicUserGroup", "policies.Rulebase", "network.EthernetInterface", "network.AggregateInterface", diff --git a/pandevice/firewall.py b/pandevice/firewall.py index 52194fac..9c5654fc 100644 --- a/pandevice/firewall.py +++ b/pandevice/firewall.py @@ -83,6 +83,7 @@ class Firewall(PanDevice): "objects.SecurityProfileGroup", "objects.CustomUrlCategory", "objects.LogForwardingProfile", + "objects.DynamicUserGroup", "policies.Rulebase", "network.EthernetInterface", "network.AggregateInterface", diff --git a/pandevice/network.py b/pandevice/network.py index a55260a1..a372436f 100644 --- a/pandevice/network.py +++ b/pandevice/network.py @@ -736,7 +736,7 @@ class Layer3Subinterface(Subinterface): management_profile (ManagementProfile): Interface Management Profile mtu(int): MTU for interface adjust_tcp_mss (bool): Adjust TCP MSS - netflow_profile (NetflowProfile): Netflow profile + netflow_profile (str): Netflow profile comment (str): The interface's comment ipv4_mss_adjust(int): TCP MSS adjustment for ipv4 ipv6_mss_adjust(int): TCP MSS adjustment for ipv6 @@ -826,7 +826,7 @@ class Layer2Subinterface(Subinterface): tag (int): Tag for the interface, aka vlan id lldp_enabled (bool): Enable LLDP lldp_profile (str): Reference to an lldp profile - netflow_profile_l2 (NetflowProfile): Reference to a netflow profile + netflow_profile_l2 (str): Netflow profile comment (str): The interface's comment """ @@ -934,10 +934,10 @@ class EthernetInterface(PhysicalInterface): Profile mtu(int): Layer3: MTU for interface adjust_tcp_mss (bool): Layer3: Adjust TCP MSS - netflow_profile (NetflowProfile): Netflow profile + netflow_profile (str): Netflow profile lldp_enabled (bool): Layer2: Enable LLDP lldp_profile (str): Layer2: Reference to an lldp profile - netflow_profile_l2 (NetflowProfile): Netflow profile + netflow_profile_l2 (str): Netflow profile link_speed (str): Link speed: eg. auto, 10, 100, 1000 link_duplex (str): Link duplex: eg. auto, full, half link_state (str): Link state: eg. auto, up, down @@ -1109,7 +1109,7 @@ class AggregateInterface(PhysicalInterface): management_profile (ManagementProfile): Layer3: Interface Management Profile mtu(int): Layer3: MTU for interface adjust_tcp_mss (bool): Layer3: Adjust TCP MSS - netflow_profile (NetflowProfile): Netflow profile + netflow_profile (str): Netflow profile lldp_enabled (bool): Enable LLDP lldp_profile (str): Reference to an lldp profile comment (str): The interface's comment @@ -1171,13 +1171,15 @@ def _setup(self): vartype='yesno', path='{mode}/adjust-tcp-mss/enable') params.append(VersionedParamPath( 'netflow_profile', - condition={'mode': ['layer3', 'layer2', 'vwire']}, + condition={'mode': ['layer3', 'layer2', 'virtual-wire']}, path='{mode}/netflow-profile')) params.append(VersionedParamPath( - 'lldp_enabled', condition={'mode': ['layer3', 'layer2', 'vwire']}, + 'lldp_enabled', + condition={'mode': ['layer3', 'layer2', 'virtual-wire']}, path='{mode}/lldp/enable', vartype='yesno')) params.append(VersionedParamPath( - 'lldp_profile', condition={'mode': ['layer3', 'layer2', 'vwire']}, + 'lldp_profile', + condition={'mode': ['layer3', 'layer2', 'virtual-wire']}, path='{mode}/lldp/profile')) params.append(VersionedParamPath( 'comment', path='comment')) @@ -1231,7 +1233,7 @@ class VlanInterface(Interface): management_profile (ManagementProfile): Interface Management Profile mtu(int): MTU for interface adjust_tcp_mss (bool): Adjust TCP MSS - netflow_profile (NetflowProfile): Netflow profile + netflow_profile (str): Netflow profile comment (str): The interface's comment ipv4_mss_adjust(int): TCP MSS adjustment for ipv4 ipv6_mss_adjust(int): TCP MSS adjustment for ipv6 @@ -1337,7 +1339,7 @@ class LoopbackInterface(Interface): management_profile (ManagementProfile): Interface Management Profile mtu(int): MTU for interface adjust_tcp_mss (bool): Adjust TCP MSS - netflow_profile (NetflowProfile): Netflow profile + netflow_profile (str): Netflow profile comment (str): The interface's comment ipv4_mss_adjust(int): TCP MSS adjustment for ipv4 ipv6_mss_adjust(int): TCP MSS adjustment for ipv6 @@ -1399,7 +1401,7 @@ class TunnelInterface(Interface): ipv6_enabled (bool): IPv6 Enabled (requires IPv6Address child object) management_profile (ManagementProfile): Interface Management Profile mtu(int): MTU for interface - netflow_profile (NetflowProfile): Netflow profile + netflow_profile (str): Netflow profile comment (str): The interface's comment """ @@ -3060,7 +3062,7 @@ def _setup(self): params.append(VersionedParamPath( 'peer_id_value', path='peer-id/id')) params.append(VersionedParamPath( - 'peer_id_check', default='exact', + 'peer_id_check', values=('exact', 'wildcard'), path='peer-id/matching')) params.append(VersionedParamPath( 'local_cert', condition={'auth_type': 'certificate'}, diff --git a/pandevice/objects.py b/pandevice/objects.py index 87ad31bd..dbd2edc5 100644 --- a/pandevice/objects.py +++ b/pandevice/objects.py @@ -702,3 +702,35 @@ def _setup(self): condition={'action_type': 'tagging'}) self._params = tuple(params) + + +class DynamicUserGroup(VersionedPanObject): + """Dynamic user group. + + Note: PAN-OS 9.1+ + + Args: + name: Name of the dynamic user group + description (str): Description of this object + filter: Tag-based filter. + tag (list): Administrative tags + + """ + ROOT = Root.VSYS + SUFFIX = ENTRY + + def _setup(self): + # xpaths + self._xpaths.add_profile(value='/dynamic-user-group') + + # params + params = [] + + params.append(VersionedParamPath( + 'description', path='description')) + params.append(VersionedParamPath( + 'filter', path='filter')) + params.append(VersionedParamPath( + 'tag', path='tag', vartype='member')) + + self._params = tuple(params) diff --git a/pandevice/policies.py b/pandevice/policies.py index 5135c2a2..33a36666 100644 --- a/pandevice/policies.py +++ b/pandevice/policies.py @@ -39,8 +39,9 @@ class Rulebase(VersionedPanObject): """ ROOT = Root.VSYS CHILDTYPES = ( - "policies.SecurityRule", "policies.NatRule", + "policies.PolicyBasedForwarding", + "policies.SecurityRule", ) def _setup(self): @@ -112,6 +113,7 @@ class SecurityRule(VersionedPanObject): (applies to panorama/device groups only) target (list): Apply this policy to the listed firewalls only (applies to panorama/device groups only) + uuid (str): (PAN-OS 9.0+) The UUID for this rule. """ # TODO: Add QoS variables @@ -249,6 +251,7 @@ class NatRule(VersionedPanObject): target (list): Apply this policy to the listed firewalls only (applies to panorama/device groups only) tag (list): Administrative tags + uuid (str): (PAN-OS 9.0+) The UUID for this rule. """ SUFFIX = ENTRY @@ -442,3 +445,141 @@ def _setup(self): vartype='attrib', path='uuid') self._params = tuple(params) + + +class PolicyBasedForwarding(VersionedPanObject): + """PBF rule. + + Args: + description (str): The descripton + tags (str/list): List of tags + from_type (str): Source from type. Valid values are 'zone' (default) + or 'interface'. + from_values (str/list): The source values for the given type. + source_addresses (str/list): List of source IP addresses. + source_users (str/list): List of source users. + negate_source (bool): Set to negate the source. + destination_addresses (str/list): List of destination addresses. + negate_destination (bool): Set to negate the destination. + applications (str/list): List of applications. + services (str/list): List of services. + schedule (str): The schedule. + disabled (bool): Set to disable this rule. + action (str): The action to take. Valid values are 'forward' + (default), 'forward-to-vsys', 'discard', or 'no-pbf'. + forward_vsys (str): The vsys to forward to if action is set to + forward to a vsys. + forward_egress_interface (str): The egress interface. + forward_next_hop_type (str): The next hop type. Valid values + are 'ip-address', 'fqdn', or None (default). + forward_next_hop_value (str): The next hop value if the forward + next hop type is not None. + forward_monitor_profile (str): The monitor profile to use. + forward_monitor_ip_address (str): The monitor IP address. + forward_monitor_disable_if_unreachable (bool): Set to disable + this rule if nexthop / monitor IP is unreachable. + enable_enforce_symmetric_return (bool): Set to enforce + symmetric return. + symmetric_return_addresses (str/list): List of symmetric return + addresses. + target (list): Apply this policy to the listed firewalls only + (applies to panorama/device groups only) + negate_target (bool): Target all but the listed target firewalls + (applies to panorama/device groups only) + uuid (str): (PAN-OS 9.0+) The UUID for this rule. + + """ + SUFFIX = ENTRY + ROOT = Root.VSYS + + def _setup(self): + # xpaths + self._xpaths.add_profile(value='/pbf/rules') + + # params + params = [] + + params.append(VersionedParamPath( + 'description', path='description')) + params.append(VersionedParamPath( + 'tags', vartype='member', path='tag')) + params.append(VersionedParamPath( + 'from_type', default='zone', + values=['zone', 'interface'], path='from/{from_type}')) + params.append(VersionedParamPath( + 'from_value', vartype='member', + path='from/{from_type}')) + params.append(VersionedParamPath( + 'source_addresses', vartype='member', path='source')) + params.append(VersionedParamPath( + 'source_users', vartype='member', path='source-user')) + params.append(VersionedParamPath( + 'negate_source', vartype='yesno', path='negate-source')) + params.append(VersionedParamPath( + 'destination_addresses', vartype='member', path='destination')) + params.append(VersionedParamPath( + 'negate_destination', vartype='yesno', path='negate-destination')) + params.append(VersionedParamPath( + 'applications', vartype='member', path='application')) + params.append(VersionedParamPath( + 'services', vartype='member', path='service')) + params.append(VersionedParamPath( + 'schedule', path='schedule')) + params.append(VersionedParamPath( + 'disabled', vartype='yesno', path='disabled')) + params.append(VersionedParamPath( + 'action', default='forward', + values=['forward', 'forward-to-vsys', 'discard', 'no-pbf'], + path='action/{action}')) + params.append(VersionedParamPath( + 'forward_vsys', + condition={'action': 'forward-to-vsys'}, + path='action/{action}/forward-to-vsys')) + params.append(VersionedParamPath( + 'forward_egress_interface', + condition={'action': 'forward'}, + path='action/{action}/egress-interface')) + params.append(VersionedParamPath( + 'forward_next_hop_type', + condition={'action': 'forward'}, + values=['ip-address', 'fqdn', None], + path='action/{action}/nexthop/{forward_next_hop_type}')) + params.append(VersionedParamPath( + 'forward_next_hop_value', + condition={ + 'action': 'forward', + 'forward_next_hop_type': ['ip-address', 'fqdn'], + }, + path='action/{action}/nexthop/{forward_next_hop_type}')) + params.append(VersionedParamPath( + 'forward_monitor_profile', + condition={'action': 'forward'}, + path='action/{action}/monitor/profile')) + params.append(VersionedParamPath( + 'forward_monitor_ip_address', + condition={'action': 'forward'}, + path='action/{action}/monitor/ip-address')) + params.append(VersionedParamPath( + 'forward_monitor_disable_if_unreachable', vartype='yesno', + condition={'action': 'forward'}, + path='action/{action}/monitor/disable-if-unreachable')) + params.append(VersionedParamPath( + 'enable_enforce_symmetric_return', vartype='yesno', + path='enforce-symmetric-return/enabled')) + params.append(VersionedParamPath( + 'symmetric_return_addresses', vartype='entry', + path='enforce-symmetric-return/nexthop-address-list')) + params.append(VersionedParamPath( + 'active_active_device_binding', + path='active-active-device-binding')) + params.append(VersionedParamPath( + 'target', vartype='entry', path='target/devices')) + params.append(VersionedParamPath( + 'negate_target', vartype='yesno', path='target/negate')) + params.append(VersionedParamPath( + 'uuid', exclude=True)) + params[-1].add_profile( + '9.0.0', + vartype='attrib', path='uuid') + + self._params = tuple(params) diff --git a/pandevice/userid.py b/pandevice/userid.py index b214ea88..9fac34f2 100644 --- a/pandevice/userid.py +++ b/pandevice/userid.py @@ -450,3 +450,275 @@ def audit_registered_ip(self, ip_tags_pairs): for ip, tags in requested_list.items(): self.register(ip, tags) self.batch_end() + + def set_group(self, group, users): + """ + Set a group's membership to the specified users. + + This method can be batched with batch_start() and batch_end(). + + Args: + group: The group name. + users (list): The users to be in this group. + + """ + root, payload = self._create_uidmessage() + + # Find the groups section. + groups = payload.find('./groups') + if groups is None: + groups = ET.SubElement(payload, 'groups') + + # Find the group. + entries = groups.findall('./entry') + for entry in entries: + if entry.attrib['name'] == group: + ge = entry.find('./members') + break + else: + entry = ET.SubElement(groups, 'entry', {'name': group}) + ge = ET.SubElement(entry, 'members') + + # Now add in the users to this group. + for user in users: + ET.SubElement(ge, 'entry', {'name': user}) + + # Done. + self.send(root) + + def get_groups(self, style=None): + """ + Get a list of groups. + + Args: + style: The type of groups to retrieve. If unspecified, returns a list of + all groups. Can be "custom-group", "dynamic", or "xmlapi". + + Returns: + list + + """ + msg = ['', ] + if style is not None: + msg.append("".format(style)) + msg.append('') + cmd = ''.join(msg) + vsys = self.device.vsys or 'vsys1' + + resp = self.device.op(cmd, vsys=self.device.vsys, cmd_xml=False) + if resp is None: + return + + ''' + Example returned XML: + + 9.1: + + + + ''' + + data = resp.find('./result') + if data is None: + return + + lines = data.text.split('\n') + ans = [] + for line in lines: + if line.startswith('Total: '): + break + val = line.strip() + if val: + ans.append(val) + + return ans + + def get_group_members(self, group): + """ + Returns a list of users in the given group. + + Args: + group: The name of the group. + + Returns: + list + + """ + cmd = ( + '' + + group + + '' + ) + vsys = self.device.vsys or 'vsys1' + + resp = self.device.op(cmd, vsys=vsys, cmd_xml=False) + if resp is None: + return + + ''' + Example returned XML: + + 9.1: + + + ''' + + data = resp.find('./result') + if data is None: + return + + lines = data.text.split('\n') + ans = [x.split(']')[1].strip() for x in lines if len(x.split(']')) == 2] + + return ans + + def get_user_tags(self, user=None, prefix=None): + """ + Get the dynamic user tags. + + Note: PAN-OS 9.1+ + + Args: + user: Get only this user's tags, not all users and all tags. + prefix: Override class tag prefix. + + Returns: + dict: Dict where the user is the key and the value is a list of tags. + + """ + if prefix is None: + prefix = self.prefix + + limit = 500 + start = 1 + start_elm = None + msg = ['', ] + if user is None: + msg.append('' + + '{0}'.format(limit) + + '{0}'.format(start) + + '') + else: + msg.append('{0}'.format(user)) + msg.append('') + + cmd = ET.fromstring(''.join(msg)) + if user is None: + start_elm = cmd.find('./object/registered-user/all/start-point') + + ans = {} + while True: + resp = self.device.op(cmd=ET.tostring(cmd, encoding='utf-8'), + vsys=self.device.vsys, cmd_xml=False) + entries = resp.findall('./result/entry') + for entry in entries: + key = entry.attrib['user'] + val = [] + members = entry.findall('./tag/member') + for member in members: + tag = member.text + if not prefix or tag.startswith(prefix): + val.append(tag) + ans[key] = val + + if start_elm is None or limit <= 0 or len(entries) < limit: + break + + start += len(entries) + start_elm.text = '{0}'.format(start) + + # Done. + return ans + + def tag_user(self, user, tags, timeout=None, prefix=None): + """ + Tags the user with the specified tags. + + This method can be batched with batch_start() and batch_end(). + + Note: PAN-OS 9.1+ + + Args: + user: The user. + tags (list): The list of tags to apply. + timeout (int): (Optional) The timeout for the given tags. + prefix: Override class tag prefix. + + """ + if timeout is not None: + timeout = int(timeout) + + if prefix is None: + prefix = self.prefix or '' + + root, payload = self._create_uidmessage() + + # Find the register user tags section. + ru = payload.find('./register-user') + if ru is None: + ru = ET.SubElement(payload, 'register-user') + + # Find the tags section for this specific user. + entries = ru.findall('./entry') + for entry in entries: + if entry.attrib['name'] == user: + te = entry.find('./tag') + break + else: + entry = ET.SubElement(ru, 'entry', {'user': user, }) + te = ET.SubElement(entry, 'tag') + + # Now add in the tags with the specified timeout. + props = {} + if timeout is not None: + props['timeout'] = '{0}'.format(timeout) + for tag in tags: + ET.SubElement(te, 'member', props).text = prefix + tag + + # Done. + self.send(root) + + def untag_user(self, user, tags=None, prefix=None): + """ + Removes tags associated with a user. + + This method can be batched with batch_start() and batch_end(). + + Note: PAN-OS 9.1+ + + Args: + user: The user. + tags (list): (Optional) Remove only these tags instead of all tags. + prefix: Override class tag prefix. + + """ + root, payload = self._create_uidmessage() + + if prefix is None: + prefix = self.prefix or '' + + # Find the unregister user tags section. + uu = payload.find('./unregister-user') + if uu is None: + uu = ET.SubElement(payload, 'unregister-user') + + # Find the tags section for this specific user. + entries = uu.findall('./entry') + for entry in entries: + if entry.attrib['name'] == user: + break + else: + entry = ET.SubElement(uu, 'entry', {'user': user, }) + + # Do tag removal. + te = entry.find('./tag') + if tags is not None: + if te is None: + te = ET.SubElement(entry, 'tag') + for tag in tags: + ET.SubElement(te, 'member').text = prefix + tag + elif te is not None: + entry.remove(te) + + # Done. + self.send(root) diff --git a/setup.py b/setup.py index d869e3df..83e55c45 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setup_kwargs = dict( name='pandevice', - version='0.13.0', + version='0.14.0', description='Framework for interacting with Palo Alto Networks devices via API', long_description='The Palo Alto Networks Device Framework is a way to interact with Palo Alto Networks devices (including Next-generation Firewalls and Panorama) using the device API that is object oriented and conceptually similar to interaction with the device via the GUI or CLI.', author='Palo Alto Networks',