From dcb1cab1a410648b3bbff96536a744ef7d787fad Mon Sep 17 00:00:00 2001 From: Lance-Drane Date: Thu, 2 Oct 2025 14:45:16 -0400 Subject: [PATCH] #30 - initial scaffolding for SDK encryption Signed-off-by: Lance-Drane --- src/intersect_sdk/__init__.py | 4 +- src/intersect_sdk/_internal/constants.py | 1 + .../data_plane/data_plane_manager.py | 40 +++- .../_internal/data_plane/minio_utils.py | 24 +++ .../_internal/function_metadata.py | 10 +- .../_internal/messages/userspace.py | 11 +- src/intersect_sdk/_internal/schema.py | 6 + src/intersect_sdk/client.py | 21 +- src/intersect_sdk/core_definitions.py | 5 +- src/intersect_sdk/service.py | 90 +++++--- src/intersect_sdk/service_definitions.py | 16 +- .../shared_callback_definitions.py | 9 +- tests/fixtures/example_schema.json | 201 ++++++++++++++++++ .../integration/test_return_type_mismatch.py | 1 + tests/integration/test_service.py | 10 + tests/unit/test_userspace_message_headers.py | 1 + 16 files changed, 409 insertions(+), 41 deletions(-) diff --git a/src/intersect_sdk/__init__.py b/src/intersect_sdk/__init__.py index f512d93..2fd9019 100644 --- a/src/intersect_sdk/__init__.py +++ b/src/intersect_sdk/__init__.py @@ -29,7 +29,7 @@ DataStoreConfigMap, HierarchyConfig, ) - from .core_definitions import IntersectDataHandler, IntersectMimeType + from .core_definitions import IntersectDataHandler, IntersectEncryptionScheme, IntersectMimeType from .exceptions import IntersectCapabilityError from .schema import get_schema_from_capability_implementations from .service import IntersectService @@ -67,6 +67,7 @@ 'IntersectClientConfig', 'IntersectDataHandler', 'IntersectDirectMessageParams', + 'IntersectEncryptionScheme', 'IntersectEventDefinition', 'IntersectEventMessageParams', 'IntersectMimeType', @@ -101,6 +102,7 @@ 'IntersectClientConfig': '.config.client', 'IntersectDataHandler': '.core_definitions', 'IntersectDirectMessageParams': '.shared_callback_definitions', + 'IntersectEncryptionScheme': '.core_definitions', 'IntersectEventDefinition': '.service_definitions', 'IntersectEventMessageParams': '.shared_callback_definitions', 'IntersectMimeType': '.core_definitions', diff --git a/src/intersect_sdk/_internal/constants.py b/src/intersect_sdk/_internal/constants.py index fc3557a..dc5973f 100644 --- a/src/intersect_sdk/_internal/constants.py +++ b/src/intersect_sdk/_internal/constants.py @@ -6,3 +6,4 @@ RESPONSE_DATA = '__response_data_transfer_handler__' STRICT_VALIDATION = '__strict_validation__' SHUTDOWN_KEYS = '__ignore_message__' +ENCRYPTION_SCHEMES = '__intersect_encryption_schemes__' diff --git a/src/intersect_sdk/_internal/data_plane/data_plane_manager.py b/src/intersect_sdk/_internal/data_plane/data_plane_manager.py index 98e55b9..4d80fc0 100644 --- a/src/intersect_sdk/_internal/data_plane/data_plane_manager.py +++ b/src/intersect_sdk/_internal/data_plane/data_plane_manager.py @@ -8,7 +8,13 @@ from ...core_definitions import IntersectDataHandler, IntersectMimeType from ..exceptions import IntersectError from ..logger import logger -from .minio_utils import MinioPayload, create_minio_store, get_minio_object, send_minio_object +from .minio_utils import ( + MinioPayload, + create_minio_store, + delete_minio_object, + get_minio_object, + send_minio_object, +) if TYPE_CHECKING: from ...config.shared import DataStoreConfigMap, HierarchyConfig @@ -111,3 +117,35 @@ def outgoing_message_data_handler( f'No support implemented for code {data_handler}, please upgrade your intersect-sdk version.' ) raise IntersectError + + def remove_remote_data( + self, message: bytes, request_data_handler: IntersectDataHandler + ) -> None: + """Removes data from the request data provider. + + This does not raise an exception if unable to remove the data, just logs the problem. + In general, this should only be called if you can verify an issue in the headers + + Params: + message: the message sent externally to this location + Returns: + the actual data we want to submit to the user function + """ + if request_data_handler == IntersectDataHandler.MINIO: + # TODO - we may want to send additional provider information in the payload + try: + payload: MinioPayload = MINIO_ADAPTER.validate_json(message) + except ValidationError as e: + logger.warning('remove_remote - invalid params', e) + return + provider = None + for store in self._minio_providers: + if store._base_url._url.geturl() == payload['minio_url']: # noqa: SLF001 (only way to get URL from MINIO API) + provider = store + break + if not provider: + logger.error( + f"You did not configure listening to MINIO instance '{payload['minio_url']}'. You must fix this to handle this data." + ) + return + delete_minio_object(provider, payload) diff --git a/src/intersect_sdk/_internal/data_plane/minio_utils.py b/src/intersect_sdk/_internal/data_plane/minio_utils.py index 6f94137..fa0831d 100644 --- a/src/intersect_sdk/_internal/data_plane/minio_utils.py +++ b/src/intersect_sdk/_internal/data_plane/minio_utils.py @@ -152,3 +152,27 @@ def get_minio_object(provider: Minio, payload: MinioPayload) -> bytes: raise IntersectError from e else: return response.data + + +def delete_minio_object(provider: Minio, payload: MinioPayload) -> None: + """Delete an object from the bucket, without returning it. + + Params: + provider: a pre-cached MinIO provider from the data provider store + payload: the payload from the message (at this point, the minio_url should exist) + + Raises: + IntersectException - if any non-fatal MinIO error is caught + """ + try: + provider.remove_object( + bucket_name=payload['minio_bucket'], object_name=payload['minio_object_id'] + ) + except MaxRetryError as e: + logger.warning( + f'Non-fatal MinIO error when retrieving object, the server may be under stress but you should double-check your configuration. Details: \n{e}' + ) + except MinioException as e: + logger.error( + f'Important MinIO error when retrieving object, this usually indicates a problem with your configuration. Details: \n{e}' + ) diff --git a/src/intersect_sdk/_internal/function_metadata.py b/src/intersect_sdk/_internal/function_metadata.py index 2f86c0c..cc2449b 100644 --- a/src/intersect_sdk/_internal/function_metadata.py +++ b/src/intersect_sdk/_internal/function_metadata.py @@ -5,7 +5,11 @@ if TYPE_CHECKING: from pydantic import TypeAdapter - from ..core_definitions import IntersectDataHandler, IntersectMimeType + from ..core_definitions import ( + IntersectDataHandler, + IntersectEncryptionScheme, + IntersectMimeType, + ) class FunctionMetadata(NamedTuple): @@ -42,6 +46,10 @@ class FunctionMetadata(NamedTuple): """ How we intend on handling the response value """ + encryption_schemes: set[IntersectEncryptionScheme] + """ + Supported encryption schemes + """ strict_validation: bool """ Whether or not we're using lenient Pydantic validation (default, False) or strict diff --git a/src/intersect_sdk/_internal/messages/userspace.py b/src/intersect_sdk/_internal/messages/userspace.py index 0912b88..c28097e 100644 --- a/src/intersect_sdk/_internal/messages/userspace.py +++ b/src/intersect_sdk/_internal/messages/userspace.py @@ -23,9 +23,7 @@ from pydantic import AwareDatetime, BaseModel, Field, field_serializer from ...constants import SYSTEM_OF_SYSTEM_REGEX -from ...core_definitions import ( - IntersectDataHandler, -) +from ...core_definitions import IntersectDataHandler, IntersectEncryptionScheme from ...version import version_string @@ -116,6 +114,11 @@ class UserspaceMessageHeaders(BaseModel): This should only be set to "True" on return messages sent by services - NEVER clients. """ + encryption_scheme: IntersectEncryptionScheme = 'NONE' + """ + The encryption scheme of the message itself. This determines the requested payload. + """ + # make sure all non-string fields are serialized into strings, even in Python code @field_serializer('message_id', mode='plain') @@ -140,6 +143,7 @@ def create_userspace_message_headers( destination: str, operation_id: str, data_handler: IntersectDataHandler, + encryption_scheme: IntersectEncryptionScheme, message_id: uuid.UUID | None = None, has_error: bool = False, ) -> dict[str, str]: @@ -153,6 +157,7 @@ def create_userspace_message_headers( created_at=datetime.datetime.now(tz=datetime.timezone.utc), operation_id=operation_id, data_handler=data_handler, + encryption_scheme=encryption_scheme, has_error=has_error, ).model_dump(by_alias=True) diff --git a/src/intersect_sdk/_internal/schema.py b/src/intersect_sdk/_internal/schema.py index aeb66b3..d9156f1 100644 --- a/src/intersect_sdk/_internal/schema.py +++ b/src/intersect_sdk/_internal/schema.py @@ -23,6 +23,7 @@ from .constants import ( BASE_RESPONSE_ATTR, BASE_STATUS_ATTR, + ENCRYPTION_SCHEMES, REQUEST_CONTENT, RESPONSE_CONTENT, RESPONSE_DATA, @@ -411,6 +412,7 @@ def _introspection_baseline( request_content = getattr(method, REQUEST_CONTENT) response_content = getattr(method, RESPONSE_CONTENT) + encryption_schemes = getattr(method, ENCRYPTION_SCHEMES) docstring = inspect.cleandoc(method.__doc__) if method.__doc__ else None signature = inspect.signature(method) @@ -434,6 +436,7 @@ def _introspection_baseline( 'message': { 'schemaFormat': f'application/vnd.aai.asyncapi+json;version={ASYNCAPI_VERSION}', 'contentType': request_content, + 'encryption_schemes': sorted(encryption_schemes), 'traits': {'$ref': '#/components/messageTraits/commonHeaders'}, } }, @@ -441,6 +444,7 @@ def _introspection_baseline( 'message': { 'schemaFormat': f'application/vnd.aai.asyncapi+json;version={ASYNCAPI_VERSION}', 'contentType': response_content, + 'encryption_schemes': sorted(encryption_schemes), 'traits': {'$ref': '#/components/messageTraits/commonHeaders'}, } }, @@ -532,6 +536,7 @@ def _introspection_baseline( request_content, response_content, data_handler, + encryption_schemes, getattr(method, STRICT_VALIDATION), getattr(method, SHUTDOWN_KEYS), ) @@ -561,6 +566,7 @@ def _introspection_baseline( getattr(status_fn, REQUEST_CONTENT), getattr(status_fn, RESPONSE_CONTENT), getattr(status_fn, RESPONSE_DATA), + {'NONE'}, # status functions should always be assumed to send out unencrypted messages. getattr(status_fn, STRICT_VALIDATION), getattr(status_fn, SHUTDOWN_KEYS), ) diff --git a/src/intersect_sdk/client.py b/src/intersect_sdk/client.py index b8f5d59..ee991a7 100644 --- a/src/intersect_sdk/client.py +++ b/src/intersect_sdk/client.py @@ -59,6 +59,7 @@ class IntersectClient: - startup() - shutdown() - is_connected() + - considered_unrecoverable() No other functions or parameters are guaranteed to remain stable. @@ -262,6 +263,13 @@ def _handle_userspace_message( request_params = self._data_plane_manager.incoming_message_data_handler( payload, headers.data_handler ) + if not headers.has_error: + match headers.encryption_scheme: + case 'RSA': + # TODO - decrypt and reassign request_params here + pass + case _: + pass if content_type == 'application/json': request_params = GENERIC_MESSAGE_SERIALIZER.validate_json(request_params) except ValidationError as e: @@ -415,7 +423,15 @@ def _send_userspace_message(self, params: IntersectDirectMessageParams) -> None: return serialized_msg = params.payload - # TWO: SEND DATA TO APPROPRIATE DATA STORE + # TWO: encrypt message + match params.encryption_scheme: + case 'RSA': + # TODO reassign serialized_msg here to encrypted value + pass + case _: + pass + + # THREE: SEND DATA TO APPROPRIATE DATA STORE try: payload = self._data_plane_manager.outgoing_message_data_handler( serialized_msg, params.content_type, params.data_handler @@ -427,12 +443,13 @@ def _send_userspace_message(self, params: IntersectDirectMessageParams) -> None: send_os_signal() return - # THREE: SEND MESSAGE + # FOUR: SEND MESSAGE headers = create_userspace_message_headers( source=self._hierarchy.hierarchy_string('.'), destination=params.destination, data_handler=params.data_handler, operation_id=params.operation, + encryption_scheme=params.encryption_scheme, ) logger.debug(f'Send userspace message:\n{headers}') channel = f'{params.destination.replace(".", "/")}/request' diff --git a/src/intersect_sdk/core_definitions.py b/src/intersect_sdk/core_definitions.py index b9b0047..93f3a50 100644 --- a/src/intersect_sdk/core_definitions.py +++ b/src/intersect_sdk/core_definitions.py @@ -1,7 +1,7 @@ """Core enumerations and structures used throughout INTERSECT, for both client and service.""" from enum import Enum -from typing import Annotated +from typing import Annotated, Literal from pydantic import Field @@ -45,3 +45,6 @@ class IntersectDataHandler(Enum): - If your Content-Type value is ANYTHING ELSE, you MUST mark it as "bytes" . In this instance, INTERSECT will not base64-encode or base64-decode the value. """ + +IntersectEncryptionScheme = Literal['NONE', 'RSA'] +"""Supported encryption schemes throughout INTERSECT. 'NONE' implies no encryption scheme.""" diff --git a/src/intersect_sdk/service.py b/src/intersect_sdk/service.py index 5006589..e2ab8db 100644 --- a/src/intersect_sdk/service.py +++ b/src/intersect_sdk/service.py @@ -812,12 +812,27 @@ def _handle_service_message_inner( self._make_error_message_headers(headers), ) + # FOUR: DECRYPT DATA + if not headers.has_error: + if headers.encryption_scheme not in operation_meta.encryption_schemes: + return ( + f'Invalid encryption scheme {headers.encryption_scheme}, supported encryption schemes are: {operation_meta.encryption_schemes}'.encode(), + 'text/plain', + self._make_error_message_headers(headers), + ) + match headers.encryption_scheme: + case 'RSA': + # TODO - decrypt request_params here + pass + case _: + pass + try: - # FOUR: CALL USER FUNCTION AND GET MESSAGE + # FIVE: CALL USER FUNCTION AND GET MESSAGE response = self._call_user_function( target_capability, operation_method, operation_meta, request_params ) - # FIVE: SEND DATA TO APPROPRIATE DATA STORE + # SIX: SEND DATA TO APPROPRIATE DATA STORE response_data_handler = operation_meta.response_data_transfer_handler response_content_type = operation_meta.response_content_type response_payload = self._data_plane_manager.outgoing_message_data_handler( @@ -851,13 +866,14 @@ def _handle_service_message_inner( self._make_error_message_headers(headers), ) - # SIX: SEND MESSAGE + # SEVEN: SEND MESSAGE response_headers = create_userspace_message_headers( source=headers.destination, destination=headers.source, data_handler=response_data_handler, operation_id=headers.operation_id, message_id=headers.message_id, # associate response with request + encryption_scheme=headers.encryption_scheme, ) return (response_payload, response_content_type, response_headers) @@ -880,36 +896,43 @@ def _handle_client_message( extreq = self._get_external_request(headers.message_id) if extreq is not None: error_msg: str | None = None - try: - msg_payload = self._data_plane_manager.incoming_message_data_handler( - payload, headers.data_handler - ) - if content_type == 'application/json': - msg_payload = GENERIC_MESSAGE_SERIALIZER.validate_json(msg_payload) - except ValidationError as e: - error_msg = f'Service sent back invalid response:\n{e}' - logger.warning(error_msg) - except IntersectError: - error_msg = 'INTERNAL ERROR: failed to get message payload from data handler' - logger.error(error_msg) - - if error_msg: - # we did not get a valid INTERSECT message back, so just mark it for cleanup - extreq.request_state = 'finalized' + if ( + not headers.has_error + and headers.encryption_scheme != extreq.request.encryption_scheme + ): + error_msg = f'Invalid encryption scheme {headers.encryption_scheme}, expected {extreq.request.encryption_scheme}' + self._data_plane_manager.remove_remote_data(payload, headers.data_handler) elif ( extreq.request.destination != headers.source or extreq.request.operation != headers.operation_id ): - logger.warning( - 'Possible spoof message, discarding. Target destination', - extreq.request.destination, - 'Actual source', - headers.source, - 'Target operation', - extreq.request.operation, - 'Actual operation', - headers.operation_id, - ) + error_msg = f'Possible spoof message, discarding. Target destination: {extreq.request.destination} -- actual source: {headers.source} -- target operation: {extreq.request.operation} -- actual operation: {headers.operation_id}' + self._data_plane_manager.remove_remote_data(payload, headers.data_handler) + logger.warning(error_msg) + else: + try: + msg_payload = self._data_plane_manager.incoming_message_data_handler( + payload, headers.data_handler + ) + if not headers.has_error: + # error messages should never be encrypted + match headers.encryption_scheme: + case 'RSA': + # TODO - decrypt message here + pass + case _: + pass + if content_type == 'application/json': + msg_payload = GENERIC_MESSAGE_SERIALIZER.validate_json(msg_payload) + except ValidationError as e: + error_msg = f'Service sent back invalid response:\n{e}' + logger.warning(error_msg) + except IntersectError: + error_msg = 'INTERNAL ERROR: failed to get message payload from data handler' + logger.error(error_msg) + + if error_msg: + # we did not get a valid INTERSECT message back, so just mark it for cleanup extreq.request_state = 'finalized' else: # success @@ -934,6 +957,13 @@ def _send_client_message(self, request_id: UUID, params: IntersectDirectMessageP return False request = params.payload + match params.encryption_scheme: + case 'RSA': + # TODO encrypt message + pass + case _: + pass + # TWO: SEND DATA TO APPROPRIATE DATA STORE try: request_payload = self._data_plane_manager.outgoing_message_data_handler( @@ -949,6 +979,7 @@ def _send_client_message(self, request_id: UUID, params: IntersectDirectMessageP data_handler=params.data_handler, operation_id=params.operation, message_id=request_id, + encryption_scheme=params.encryption_scheme, ) logger.debug(f'Sending client message:\n{headers}') request_channel = f'{params.destination.replace(".", "/")}/request' @@ -1148,6 +1179,7 @@ def _make_error_message_headers( data_handler=IntersectDataHandler.MESSAGE, operation_id=original_headers.operation_id, message_id=original_headers.message_id, # associate error reply with original + encryption_scheme='NONE', has_error=True, ) diff --git a/src/intersect_sdk/service_definitions.py b/src/intersect_sdk/service_definitions.py index ece3391..be46809 100644 --- a/src/intersect_sdk/service_definitions.py +++ b/src/intersect_sdk/service_definitions.py @@ -13,7 +13,7 @@ import functools from collections.abc import Callable, Mapping, Sequence -from typing import Any +from typing import Any, get_args from pydantic import BaseModel, ConfigDict, field_validator, validate_call from typing_extensions import final @@ -21,13 +21,14 @@ from ._internal.constants import ( BASE_RESPONSE_ATTR, BASE_STATUS_ATTR, + ENCRYPTION_SCHEMES, REQUEST_CONTENT, RESPONSE_CONTENT, RESPONSE_DATA, SHUTDOWN_KEYS, STRICT_VALIDATION, ) -from .core_definitions import IntersectDataHandler, IntersectMimeType +from .core_definitions import IntersectDataHandler, IntersectEncryptionScheme, IntersectMimeType @final @@ -85,6 +86,7 @@ def intersect_message( response_data_transfer_handler: IntersectDataHandler = IntersectDataHandler.MESSAGE, response_content_type: IntersectMimeType = 'application/json', strict_request_validation: bool = False, + encryption_schemes: set[IntersectEncryptionScheme] | None = None, ) -> Callable[..., Any]: """Use this annotation to mark your capability method as an entrypoint to external requests. @@ -133,6 +135,8 @@ def some_external_function(self, request: MyBaseModelRequest) -> MyBaseModelResp - strict_request_validation: if this is set to True, use pydantic strict validation for requests - otherwise, use lenient validation (default: False) See https://docs.pydantic.dev/latest/concepts/conversion_table/ for more info about this. NOTE: If you are using a Mapping type (i.e. Dict) with integer or float keys, you MUST leave this on False. + - encryption_schemes: This is a set of the encryption schemes the endpoint will support. If no encryption scheme is specified, all encryption schemes (including no encryption scheme) will be supported. + This is a way to either enforce encrypted data for a specific endpoint, or to enforce that a specific endpoint is _not_ encrypted. """ def inner_decorator(func: Callable[..., Any]) -> Callable[..., Any]: @@ -153,6 +157,13 @@ def __intersect_sdk_wrapper(*args: Any, **kwargs: Any) -> Any: setattr(__intersect_sdk_wrapper, RESPONSE_DATA, response_data_transfer_handler) setattr(__intersect_sdk_wrapper, STRICT_VALIDATION, strict_request_validation) setattr(__intersect_sdk_wrapper, SHUTDOWN_KEYS, set(ignore_keys) if ignore_keys else set()) + setattr( + __intersect_sdk_wrapper, + ENCRYPTION_SCHEMES, + set(encryption_schemes) + if encryption_schemes + else set(get_args(IntersectEncryptionScheme)), + ) return __intersect_sdk_wrapper @@ -178,6 +189,7 @@ def intersect_status( A status message MUST NOT send events out. It should be a simple query of the general service (no specifics). A status message MUST send its response back in a value which can be serialized into JSON. A status message MUST have a fairly small response size (no large data). + A status message MUST be able to be sent out unencrypted (encryption is not supported). Use @intersect_message if you want to manually get status information which is potentially sensitive. """ def inner_decorator(func: Callable[..., Any]) -> Callable[..., Any]: diff --git a/src/intersect_sdk/shared_callback_definitions.py b/src/intersect_sdk/shared_callback_definitions.py index 4172cc9..a0b37e2 100644 --- a/src/intersect_sdk/shared_callback_definitions.py +++ b/src/intersect_sdk/shared_callback_definitions.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, ConfigDict, Field from .constants import CAPABILITY_REGEX, SYSTEM_OF_SYSTEM_REGEX -from .core_definitions import IntersectDataHandler, IntersectMimeType +from .core_definitions import IntersectDataHandler, IntersectEncryptionScheme, IntersectMimeType INTERSECT_JSON_VALUE: TypeAlias = ( list['INTERSECT_JSON_VALUE'] @@ -70,6 +70,13 @@ class IntersectDirectMessageParams(BaseModel): default: IntersectDataHandler.MESSAGE """ + encryption_scheme: IntersectEncryptionScheme = 'NONE' + """ + The encryption scheme used for messaging. + + default: 'NONE' + """ + # pydantic config model_config = ConfigDict(revalidate_instances='always') diff --git a/tests/fixtures/example_schema.json b/tests/fixtures/example_schema.json index 2720e04..c1eb76f 100644 --- a/tests/fixtures/example_schema.json +++ b/tests/fixtures/example_schema.json @@ -23,6 +23,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -43,6 +47,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -65,6 +73,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "image/png", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -80,6 +92,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "image/png", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -98,6 +114,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -115,6 +135,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -133,6 +157,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -150,6 +178,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -176,6 +208,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -189,6 +225,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -204,6 +244,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -231,6 +275,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -249,6 +297,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -264,6 +316,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -281,6 +337,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -294,6 +354,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -311,6 +375,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -324,6 +392,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" } @@ -335,6 +407,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -348,6 +424,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" } @@ -359,6 +439,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -373,6 +457,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -388,6 +476,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -402,6 +494,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -419,6 +515,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -433,6 +533,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -456,6 +560,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -473,6 +581,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -492,6 +604,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -506,6 +622,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -521,6 +641,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -538,6 +662,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -554,6 +682,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -575,6 +707,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -593,6 +729,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -605,6 +745,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -619,6 +763,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -633,6 +781,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -650,6 +802,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -670,6 +826,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -689,6 +849,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -729,6 +893,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" } @@ -741,6 +909,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -755,6 +927,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -772,6 +948,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -791,6 +971,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -812,6 +996,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -825,6 +1013,10 @@ "message": { "schemaFormat": "application/vnd.aai.asyncapi+json;version=2.6.0", "contentType": "application/json", + "encryption_schemes": [ + "NONE", + "RSA" + ], "traits": { "$ref": "#/components/messageTraits/commonHeaders" }, @@ -1190,6 +1382,15 @@ "description": "If this value is True, the payload will contain the error message (a string)", "title": "Has Error", "type": "string" + }, + "encryption_scheme": { + "default": "NONE", + "enum": [ + "NONE", + "RSA" + ], + "title": "Encryption Scheme", + "type": "string" } }, "required": [ diff --git a/tests/integration/test_return_type_mismatch.py b/tests/integration/test_return_type_mismatch.py index 57706ce..af9158a 100644 --- a/tests/integration/test_return_type_mismatch.py +++ b/tests/integration/test_return_type_mismatch.py @@ -112,6 +112,7 @@ def userspace_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='ReturnTypeMismatchCapability.wrong_return_annotation', + encryption_scheme='NONE', ), True, ) diff --git a/tests/integration/test_service.py b/tests/integration/test_service.py index 32709c3..e83fdf7 100644 --- a/tests/integration/test_service.py +++ b/tests/integration/test_service.py @@ -128,6 +128,7 @@ def userspace_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='DummyCapability.calculate_fibonacci', + encryption_scheme='NONE', ), True, ) @@ -166,6 +167,7 @@ def userspace_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='DummyCapability.test_generator', + encryption_scheme='NONE', ), True, ) @@ -203,6 +205,7 @@ def userspace_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='DummyCapability.valid_default_argument', + encryption_scheme='NONE', ), True, ) @@ -241,6 +244,7 @@ def userspace_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='DummyCapability.calculate_fibonacci', + encryption_scheme='NONE', ), True, ) @@ -282,6 +286,7 @@ def userspace_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='DummyCapability.THIS_FUNCTION_DOES_NOT_EXIST', + encryption_scheme='NONE', ), True, ) @@ -320,6 +325,7 @@ def userspace_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='DummyCapability.divide_by_zero_exceptions', + encryption_scheme='NONE', ), True, ) @@ -333,6 +339,7 @@ def userspace_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='DummyCapability.divide_by_zero_exceptions', + encryption_scheme='NONE', ), True, ) @@ -346,6 +353,7 @@ def userspace_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='DummyCapability.raise_exception_no_param', + encryption_scheme='NONE', ), True, ) @@ -392,6 +400,7 @@ def userspace_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='DummyCapability.test_datetime', + encryption_scheme='NONE', ), True, ) @@ -445,6 +454,7 @@ def lifecycle_msg_callback( destination='test.test.test.test.test', data_handler=IntersectDataHandler.MESSAGE, operation_id='DummyCapability.verify_float_dict', + encryption_scheme='NONE', # note that the dict key MUST be a string, even though the input wants a float key ), True, diff --git a/tests/unit/test_userspace_message_headers.py b/tests/unit/test_userspace_message_headers.py index 3b8cd10..600e0c7 100644 --- a/tests/unit/test_userspace_message_headers.py +++ b/tests/unit/test_userspace_message_headers.py @@ -103,6 +103,7 @@ def test_create_userspace_message() -> None: destination='destination', operation_id='operation', data_handler=IntersectDataHandler.MESSAGE, + encryption_scheme='NONE', ) # make sure all values are serialized as strings, this is necessary for some protocols i.e. MQTT5 Properties