diff --git a/CHANGELOG.md b/CHANGELOG.md index f23e74d..6755f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. +## 1.7.1 - 2024-11-15 +### Added +- Adding support for _x_tapis_tracking_id variable with validation. This sends the header when users call tapipy operations and specify the value in the call. + +### Changed +- No change. + +### Removed +- No change. + + ## 1.7.0 - 2024-09-13 ### Added - Poetry lock update diff --git a/pyproject.toml b/pyproject.toml index 4d71028..8be1134 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tapipy" -version = "1.7.0" +version = "1.7.1" description = "Python lib for interacting with an instance of the Tapis API Framework" license = "BSD-4-Clause" authors = ["Joe Stubbs "] diff --git a/tapipy/__init__.py b/tapipy/__init__.py index 0e1a38d..48c2f6b 100644 --- a/tapipy/__init__.py +++ b/tapipy/__init__.py @@ -1 +1 @@ -__version__ = '1.7.0' +__version__ = '1.7.1' diff --git a/tapipy/tapis.py b/tapipy/tapis.py index b552a95..be3d23e 100644 --- a/tapipy/tapis.py +++ b/tapipy/tapis.py @@ -1100,6 +1100,57 @@ def __call__(self, **kwargs): raise errors.InvalidInputError( msg="The headers argument, if passed, must be a dictionary-like object.") + # if X-Tapis-Tracking_ID (regardless of case) is in the headers we need to set tracking_id for validation + tracking_id = None + for k, v in headers.items(): + if k.lower() == 'x-tapis-tracking-id' or k.lower() == 'x_tapis_tracking_id': + tracking_id = headers.pop(k) + break + if '_x_tapis_tracking_id' in kwargs: + if tracking_id: + raise errors.InvalidInputError(msg="The _x_tapis_tracking_id argument and the X-Tapis-Tracking-ID header cannot both be set.") + else: + tracking_id = kwargs.pop('_x_tapis_tracking_id') + + # tracking_id header needs to be passed through to __call__ headers for splunk audit trails + if tracking_id: + try: + if not isinstance(tracking_id, str): + raise errors.InvalidInputError( + msg="The _x_tapis_tracking_id argument, if passed, must be a string.") + + if not tracking_id.isascii(): + raise errors.InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed be a single period, followed by an ASCII universally unique identifier string. Must be an entirely ASCII string.") + + if len(tracking_id) > 126: + raise errors.InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed be a single period, followed by an ASCII universally unique identifier string. Must be less than 126 characters.") + + # only one . in string + if tracking_id.count('.') != 1: + raise errors.InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed be a single period, followed by an ASCII universally unique identifier string. count('.') != 1.") + + # ensure doesn't start or end with . + if tracking_id.startswith('.') or tracking_id.endswith('.'): + raise errors.InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed be a single period, followed by an ASCII universally unique identifier string. Cannot start or end with '.'.") + + tracking_namespace, tracking_unique_identifier = tracking_id.split('.') + # check namespace is alphanumeric + underscores + if not all(c.isalnum() or c == '_' for c in tracking_namespace): + raise errors.InvalidInputError(msg="Error: tracking_namespace contains invalid characters. Alphanumeric + underscores only.") + + # check tracking_unique_identifier is alphanumeric + hyphens + if not all(c.isalnum() or c == '-' for c in tracking_unique_identifier): + raise errors.InvalidInputError(msg="Error: tracking_unique_identifier contains invalid characters. Alphanumeric + hyphens only.") + + headers.update({'X-Tapis-Tracking-ID': tracking_id}) + except ValueError: + raise errors.InvalidInputError( + msg=f"_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed be a single period, followed by an ASCII universally unique identifier string. Got x_tapis_tracking_id: {tracking_id}") + # construct the data - data = None files = None diff --git a/tests/tapipy-tests.py b/tests/tapipy-tests.py index d2980d3..606b4c4 100644 --- a/tests/tapipy-tests.py +++ b/tests/tapipy-tests.py @@ -6,6 +6,7 @@ import subprocess import pytest from tapipy.tapis import Tapis, TapisResult +from tapipy.errors import InvalidInputError BASE_URL = os.getenv("base_url", "https://dev.develop.tapis.io") @@ -24,6 +25,7 @@ def client(): # ----------------------------------------------------- # Tests to check parsing of different result structures - # ----------------------------------------------------- + def test_tapisresult_list_simple(): result = ['a', 1, 'b', True, None, 3.14159, b'some bytes'] tr = TapisResult(result) @@ -308,6 +310,74 @@ def test_debug_flag_tenants(client): assert hasattr(debug.request, 'url') assert hasattr(debug.response, 'content') +# ---------------- +# tracking_id tests - Confluence: Proposal for File Provenance Auditing +# ---------------- + +def validate_tracking_id(tracking_id): + if not isinstance(tracking_id, str): + raise InvalidInputError( + msg="The _x_tapis_tracking_id argument, if passed, must be a string.") + + if not tracking_id.isascii(): + raise InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed by a single period, followed by an ASCII universally unique identifier string. Must be an entirely ASCII string.") + + if len(tracking_id) > 126: + raise InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed by a single period, followed by an ASCII universally unique identifier string. Must be less than 126 characters.") + + if tracking_id.count('.') != 1: + raise InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed by a single period, followed by an ASCII universally unique identifier string. count('.') != 1.") + + tracking_namespace, tracking_id = tracking_id.split('.') + if not all(c.isalnum() or c == '_' for c in tracking_namespace): + raise InvalidInputError(msg="Error: tracking_namespace contains invalid characters. Alphanumeric + underscores only.") + + if not all(c.isalnum() or c == '-' for c in tracking_id): + raise InvalidInputError(msg="Error: tracking_id contains invalid characters. Alphanumeric + hyphens only.") + +def test_tracking_id_validation(client): + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id=True) # Not a string + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.iden.tifier") # More than one period + + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifier") # Should work + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifier-with-non-ascii-字符") # Non-ASCII characters + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifier" * 10) # Length > 126 + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespaceidentifier") # No period + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace..identifier") # More than one period + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifi.er") # More than one period + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifi_er") # id only allowed alphanumeric and hyphens after . + + result = client.tenants.list_tenants(_x_tapis_tracking_id="names_ace.identifi-er") # namespace only allowed alphanumeric and underscores (This is proper) + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace!@#.identifier") # Invalid characters in namespace + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifier!@#") # Invalid characters in identifier + + # Valid case + try: + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifier") + except InvalidInputError: + pytest.fail("validate_tracking_id() raised InvalidInputError unexpectedly!") # ----------------------- # Tapipy import timing test -