cdot65 / pan-scm-sdk

Python SDK for Palo Alto Networks Strata Cloud Manager.
https://cdot65.github.io/pan-scm-sdk/
Apache License 2.0
1 stars 1 forks source link

Filter addresses by name returns an empty list #26

Closed mrcloudmustache closed 3 weeks ago

mrcloudmustache commented 1 month ago

Problem: An empty list of addresses is returned when addresses are filtered by name

Reason: The API returns the "data" key when filtering by folder.

/config/objects/v1/addresses?folder=Shared&limit=200

{
    "data": [
        {
            "id": "184e87ef-c4b5-4cc1-a7c3-610d7c70b443",
            "name": "Palo Alto Networks Sinkhole",
            "folder": "All",
            "snippet": "default",
            "fqdn": "sinkhole.paloaltonetworks.com",
            "description": "Palo Alto Networks sinkhole"
        },
        {
            "id": "7df80ada-72e7-4cc0-849a-8354a19dc923",
            "name": "test_internal_network",
            "folder": "Shared",
            "description": "Test Internal network address",
            "ip_netmask": "192.168.1.0/24"
        }
    ],
    "offset": 0,
    "total": 2,
    "limit": 200
}

The API does NOT return the "data" key when filtering by folder and name

/config/objects/v1/addresses?name=test_internal_network&folder=Shared&limit=200

{
    "id": "7df80ada-72e7-4cc0-849a-8354a19dc923",
    "name": "test_internal_network",
    "folder": "Shared",
    "description": "Test Internal network address",
    "ip_netmask": "192.168.1.0/24"
}

https://github.com/cdot65/pan-scm-sdk/blob/5635e44c43b471a45342c8c7471b167ced7d8413/scm/config/objects/address.py#L102

Solution: Update the above code to account for both scenarios.

cdot65 commented 1 month ago

acknowledged, you are correct. the filter logic has yet to be worked out and will require another glance after the coverage of the SDK is a bit better.

For now, the best way to retrieve a specific instance is by UUID, but the API does support retrieving by name so I don't see this being a challenge that we cannot overcome later.

cdot65 commented 3 weeks ago

how do you feel about this?

return a single instance of an object by name:

x = security_rules.fetch(folder='Prisma Access', name='Monitor WAN to GlobalProtect')

validate object type

type(x)
scm.models.security.security_rules.SecurityRuleResponseModel

update an attribute of the object

x.description = "ABC"

validate the model object as a Python dictionary

x.model_dump(exclude_none=True)
{'name': 'Monitor WAN to GlobalProtect',
 'disabled': True,
 'description': 'ABC',
 'tag': [],
 'from_': ['any'],
 'source': ['any'],
 'negate_source': False,
 'source_user': ['any'],
 'source_hip': ['any'],
 'to_': ['any'],
 'destination': ['any'],
 'negate_destination': False,
 'destination_hip': ['any'],
 'application': ['panos-global-protect', 'ssl', 'web-browsing'],
 'service': ['any'],
 'category': ['any'],
 'action': <Action.allow: 'allow'>,
 'log_setting': 'Cortex Data Lake',
 'log_end': True,
 'id': '8a5cb583-85bd-45d9-be7e-2a6f79888f4c',
 'folder': 'Shared'}

push the update

security_rules.update(object_id=x.id, data=x.model_dump(exclude_none=True))

get the response modeled object back

SecurityRuleResponseModel(name='Monitor WAN to GlobalProtect', disabled=True, description='ABC', tag=[], from_=['any'], source=['any'], negate_source=False, source_user=['any'], source_hip=['any'], to_=['any'], destination=['any'], negate_destination=False, destination_hip=['any'], application=['panos-global-protect', 'ssl', 'web-browsing'], service=['any'], category=['any'], action=<Action.allow: 'allow'>, profile_setting=None, log_setting='Cortex Data Lake', schedule=None, log_start=None, log_end=True, id='8a5cb583-85bd-45d9-be7e-2a6f79888f4c', folder='Shared', snippet=None, device=None)
cdot65 commented 3 weeks ago

One thing I'm not entirely sold on is the requirement to pass the updated object with model_dump(exclude_none=True), yes it will be what is required by the remote API model, but I feel like most people will forget how to perform this.

cdot65 commented 3 weeks ago

An alternative approach would be to have the fetch() method return the modeled object back as a Python dictionary and simply pass that into the update object

Return the response modeled object as a Python dictionary, passing in the exclude_unset and exclude_none already:

        response = self.api_client.get(self.ENDPOINT, params=params)

        # Since response is a single object when 'name' is provided
        # We can directly create the SecurityRuleResponseModel
        rule = SecurityRuleResponseModel(**response)
        return rule.model_dump(exclude_unset=True, exclude_none=True)

what this would look like:

return a single instance of an object by name:

x = security_rules.fetch(folder='Prisma Access', name='Monitor WAN to GlobalProtect')

validate object type

type(x)
dict

update an attribute of the object

x['description'] = "ABCDEFG"

validate the Python dictionary object

{'name': 'Monitor WAN to GlobalProtect',
 'disabled': True,
 'description': 'ABCDEFG',
 'from_': ['any'],
 'source': ['any'],
 'negate_source': False,
 'source_user': ['any'],
 'source_hip': ['any'],
 'to_': ['any'],
 'destination': ['any'],
 'negate_destination': False,
 'destination_hip': ['any'],
 'application': ['panos-global-protect', 'ssl', 'web-browsing'],
 'service': ['any'],
 'category': ['any'],
 'action': <Action.allow: 'allow'>,
 'log_setting': 'Cortex Data Lake',
 'log_end': True,
 'id': '8a5cb583-85bd-45d9-be7e-2a6f79888f4c',
 'folder': 'Shared'}

push the update

security_rules.update(object_id=x['id'], data=x)

get the response modeled object back

SecurityRuleResponseModel(name='Monitor WAN to GlobalProtect', disabled=True, description='ABCDEFG', tag=[], from_=['any'], source=['any'], negate_source=False, source_user=['any'], source_hip=['any'], to_=['any'], destination=['any'], negate_destination=False, destination_hip=['any'], application=['panos-global-protect', 'ssl', 'web-browsing'], service=['any'], category=['any'], action=<Action.allow: 'allow'>, profile_setting=None, log_setting='Cortex Data Lake', schedule=None, log_start=None, log_end=True, id='8a5cb583-85bd-45d9-be7e-2a6f79888f4c', folder='Shared', snippet=None, device=None)
cdot65 commented 3 weeks ago

Full workflow for an address object:

In [1]: from scm.client import Scm

In [2]: from scm.config.objects import Address

In [3]: api_client = Scm(
   ...:     client_id="example@1234567890.iam.panserviceaccount.com",
   ...:     client_secret="12345678-abcd-1234-efgh-1234567890ab",
   ...:     tsg_id="1234567890",
   ...: )

In [4]: addresses = Address(api_client)

In [5]: google_dns = addresses.fetch(folder='Prisma Access', name='Google DNS2-1-4')

In [6]: type(google_dns)
Out[6]: dict

In [7]: google_dns
Out[7]: 
{'name': 'Google DNS2-1-4',
 'id': '64b7218d-7658-4ee6-97c6-d8056dd74201',
 'ip_netmask': '8.8.8.8',
 'folder': 'Shared'}

In [8]: google_dns['description'] = 'test123'

In [9]: addresses.update(google_dns['id'], google_dns)
Out[9]: AddressResponseModel(name='Google DNS2-1-4', id='64b7218d-7658-4ee6-97c6-d8056dd74201', description='test123', tag=None, ip_netmask='8.8.8.8', ip_range=None, ip_wildcard=None, fqdn=None, folder='Shared', snippet=None, device=None)
cdot65 commented 3 weeks ago

For this release of 0.2.0, I opted to have the fetch method return a python dictionary, updated the schema to support the passing of an object ID, and use this id value to simplify the update method by simply passing in the dictionary (instead of also requiring the object id as a separate argument)

updated doc example of the sdk

updated doc example of the models

Also created base models for each configuration item, leaving the other models that inherit it for the deviations (custom schema validations, attributes)

Complete overhaul on pytests, moving to class based approach to help build boilerplate tests going forward, and complete overhaul of docs.

Overall some big changes that can be reviewed here