Skip to content

Commit 94e17fb

Browse files
Lance-Dranemarshallmcdonnell
authored andcommitted
30 - initial scaffolding for SDK encryption
Signed-off-by: Lance-Drane <Lance-Drane@users.noreply.github.com>
1 parent 9009e59 commit 94e17fb

16 files changed

+409
-41
lines changed

src/intersect_sdk/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
DataStoreConfigMap,
3030
HierarchyConfig,
3131
)
32-
from .core_definitions import IntersectDataHandler, IntersectMimeType
32+
from .core_definitions import IntersectDataHandler, IntersectEncryptionScheme, IntersectMimeType
3333
from .exceptions import IntersectCapabilityError
3434
from .schema import get_schema_from_capability_implementations
3535
from .service import IntersectService
@@ -67,6 +67,7 @@
6767
'IntersectClientConfig',
6868
'IntersectDataHandler',
6969
'IntersectDirectMessageParams',
70+
'IntersectEncryptionScheme',
7071
'IntersectEventDefinition',
7172
'IntersectEventMessageParams',
7273
'IntersectMimeType',
@@ -101,6 +102,7 @@
101102
'IntersectClientConfig': '.config.client',
102103
'IntersectDataHandler': '.core_definitions',
103104
'IntersectDirectMessageParams': '.shared_callback_definitions',
105+
'IntersectEncryptionScheme': '.core_definitions',
104106
'IntersectEventDefinition': '.service_definitions',
105107
'IntersectEventMessageParams': '.shared_callback_definitions',
106108
'IntersectMimeType': '.core_definitions',

src/intersect_sdk/_internal/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
RESPONSE_DATA = '__response_data_transfer_handler__'
77
STRICT_VALIDATION = '__strict_validation__'
88
SHUTDOWN_KEYS = '__ignore_message__'
9+
ENCRYPTION_SCHEMES = '__intersect_encryption_schemes__'

src/intersect_sdk/_internal/data_plane/data_plane_manager.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
from ...core_definitions import IntersectDataHandler, IntersectMimeType
99
from ..exceptions import IntersectError
1010
from ..logger import logger
11-
from .minio_utils import MinioPayload, create_minio_store, get_minio_object, send_minio_object
11+
from .minio_utils import (
12+
MinioPayload,
13+
create_minio_store,
14+
delete_minio_object,
15+
get_minio_object,
16+
send_minio_object,
17+
)
1218

1319
if TYPE_CHECKING:
1420
from ...config.shared import DataStoreConfigMap, HierarchyConfig
@@ -111,3 +117,35 @@ def outgoing_message_data_handler(
111117
f'No support implemented for code {data_handler}, please upgrade your intersect-sdk version.'
112118
)
113119
raise IntersectError
120+
121+
def remove_remote_data(
122+
self, message: bytes, request_data_handler: IntersectDataHandler
123+
) -> None:
124+
"""Removes data from the request data provider.
125+
126+
This does not raise an exception if unable to remove the data, just logs the problem.
127+
In general, this should only be called if you can verify an issue in the headers
128+
129+
Params:
130+
message: the message sent externally to this location
131+
Returns:
132+
the actual data we want to submit to the user function
133+
"""
134+
if request_data_handler == IntersectDataHandler.MINIO:
135+
# TODO - we may want to send additional provider information in the payload
136+
try:
137+
payload: MinioPayload = MINIO_ADAPTER.validate_json(message)
138+
except ValidationError as e:
139+
logger.warning('remove_remote - invalid params', e)
140+
return
141+
provider = None
142+
for store in self._minio_providers:
143+
if store._base_url._url.geturl() == payload['minio_url']: # noqa: SLF001 (only way to get URL from MINIO API)
144+
provider = store
145+
break
146+
if not provider:
147+
logger.error(
148+
f"You did not configure listening to MINIO instance '{payload['minio_url']}'. You must fix this to handle this data."
149+
)
150+
return
151+
delete_minio_object(provider, payload)

src/intersect_sdk/_internal/data_plane/minio_utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,27 @@ def get_minio_object(provider: Minio, payload: MinioPayload) -> bytes:
152152
raise IntersectError from e
153153
else:
154154
return response.data
155+
156+
157+
def delete_minio_object(provider: Minio, payload: MinioPayload) -> None:
158+
"""Delete an object from the bucket, without returning it.
159+
160+
Params:
161+
provider: a pre-cached MinIO provider from the data provider store
162+
payload: the payload from the message (at this point, the minio_url should exist)
163+
164+
Raises:
165+
IntersectException - if any non-fatal MinIO error is caught
166+
"""
167+
try:
168+
provider.remove_object(
169+
bucket_name=payload['minio_bucket'], object_name=payload['minio_object_id']
170+
)
171+
except MaxRetryError as e:
172+
logger.warning(
173+
f'Non-fatal MinIO error when retrieving object, the server may be under stress but you should double-check your configuration. Details: \n{e}'
174+
)
175+
except MinioException as e:
176+
logger.error(
177+
f'Important MinIO error when retrieving object, this usually indicates a problem with your configuration. Details: \n{e}'
178+
)

src/intersect_sdk/_internal/function_metadata.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
if TYPE_CHECKING:
66
from pydantic import TypeAdapter
77

8-
from ..core_definitions import IntersectDataHandler, IntersectMimeType
8+
from ..core_definitions import (
9+
IntersectDataHandler,
10+
IntersectEncryptionScheme,
11+
IntersectMimeType,
12+
)
913

1014

1115
class FunctionMetadata(NamedTuple):
@@ -42,6 +46,10 @@ class FunctionMetadata(NamedTuple):
4246
"""
4347
How we intend on handling the response value
4448
"""
49+
encryption_schemes: set[IntersectEncryptionScheme]
50+
"""
51+
Supported encryption schemes
52+
"""
4553
strict_validation: bool
4654
"""
4755
Whether or not we're using lenient Pydantic validation (default, False) or strict

src/intersect_sdk/_internal/messages/userspace.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
from pydantic import AwareDatetime, BaseModel, Field, field_serializer
2424

2525
from ...constants import SYSTEM_OF_SYSTEM_REGEX
26-
from ...core_definitions import (
27-
IntersectDataHandler,
28-
)
26+
from ...core_definitions import IntersectDataHandler, IntersectEncryptionScheme
2927
from ...version import version_string
3028

3129

@@ -131,6 +129,11 @@ class UserspaceMessageHeaders(BaseModel):
131129
This should only be set to "True" on return messages sent by services - NEVER clients.
132130
"""
133131

132+
encryption_scheme: IntersectEncryptionScheme = 'NONE'
133+
"""
134+
The encryption scheme of the message itself. This determines the requested payload.
135+
"""
136+
134137
# make sure all non-string fields are serialized into strings, even in Python code
135138

136139
@field_serializer('message_id', 'request_id', 'campaign_id', mode='plain')
@@ -157,6 +160,7 @@ def create_userspace_message_headers(
157160
data_handler: IntersectDataHandler,
158161
campaign_id: uuid.UUID,
159162
request_id: uuid.UUID,
163+
encryption_scheme: IntersectEncryptionScheme,
160164
has_error: bool = False,
161165
) -> dict[str, str]:
162166
"""Generate raw headers and write them into a generic data structure which can be handled by any broker protocol."""
@@ -170,6 +174,7 @@ def create_userspace_message_headers(
170174
created_at=datetime.datetime.now(tz=datetime.timezone.utc),
171175
operation_id=operation_id,
172176
data_handler=data_handler,
177+
encryption_scheme=encryption_scheme,
173178
has_error=has_error,
174179
).model_dump(by_alias=True)
175180

src/intersect_sdk/_internal/schema.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .constants import (
2424
BASE_RESPONSE_ATTR,
2525
BASE_STATUS_ATTR,
26+
ENCRYPTION_SCHEMES,
2627
REQUEST_CONTENT,
2728
RESPONSE_CONTENT,
2829
RESPONSE_DATA,
@@ -411,6 +412,7 @@ def _introspection_baseline(
411412

412413
request_content = getattr(method, REQUEST_CONTENT)
413414
response_content = getattr(method, RESPONSE_CONTENT)
415+
encryption_schemes = getattr(method, ENCRYPTION_SCHEMES)
414416

415417
docstring = inspect.cleandoc(method.__doc__) if method.__doc__ else None
416418
signature = inspect.signature(method)
@@ -434,13 +436,15 @@ def _introspection_baseline(
434436
'message': {
435437
'schemaFormat': f'application/vnd.aai.asyncapi+json;version={ASYNCAPI_VERSION}',
436438
'contentType': request_content,
439+
'encryption_schemes': sorted(encryption_schemes),
437440
'traits': {'$ref': '#/components/messageTraits/commonHeaders'},
438441
}
439442
},
440443
'subscribe': {
441444
'message': {
442445
'schemaFormat': f'application/vnd.aai.asyncapi+json;version={ASYNCAPI_VERSION}',
443446
'contentType': response_content,
447+
'encryption_schemes': sorted(encryption_schemes),
444448
'traits': {'$ref': '#/components/messageTraits/commonHeaders'},
445449
}
446450
},
@@ -532,6 +536,7 @@ def _introspection_baseline(
532536
request_content,
533537
response_content,
534538
data_handler,
539+
encryption_schemes,
535540
getattr(method, STRICT_VALIDATION),
536541
getattr(method, SHUTDOWN_KEYS),
537542
)
@@ -561,6 +566,7 @@ def _introspection_baseline(
561566
getattr(status_fn, REQUEST_CONTENT),
562567
getattr(status_fn, RESPONSE_CONTENT),
563568
getattr(status_fn, RESPONSE_DATA),
569+
{'NONE'}, # status functions should always be assumed to send out unencrypted messages.
564570
getattr(status_fn, STRICT_VALIDATION),
565571
getattr(status_fn, SHUTDOWN_KEYS),
566572
)

src/intersect_sdk/client.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class IntersectClient:
5959
- startup()
6060
- shutdown()
6161
- is_connected()
62+
- considered_unrecoverable()
6263
6364
No other functions or parameters are guaranteed to remain stable.
6465
@@ -264,6 +265,13 @@ def _handle_userspace_message(
264265
request_params = self._data_plane_manager.incoming_message_data_handler(
265266
payload, headers.data_handler
266267
)
268+
if not headers.has_error:
269+
match headers.encryption_scheme:
270+
case 'RSA':
271+
# TODO - decrypt and reassign request_params here
272+
pass
273+
case _:
274+
pass
267275
if content_type == 'application/json':
268276
request_params = GENERIC_MESSAGE_SERIALIZER.validate_json(request_params)
269277
except ValidationError as e:
@@ -417,7 +425,15 @@ def _send_userspace_message(self, params: IntersectDirectMessageParams) -> None:
417425
return
418426
serialized_msg = params.payload
419427

420-
# TWO: SEND DATA TO APPROPRIATE DATA STORE
428+
# TWO: encrypt message
429+
match params.encryption_scheme:
430+
case 'RSA':
431+
# TODO reassign serialized_msg here to encrypted value
432+
pass
433+
case _:
434+
pass
435+
436+
# THREE: SEND DATA TO APPROPRIATE DATA STORE
421437
try:
422438
payload = self._data_plane_manager.outgoing_message_data_handler(
423439
serialized_msg, params.content_type, params.data_handler
@@ -429,14 +445,15 @@ def _send_userspace_message(self, params: IntersectDirectMessageParams) -> None:
429445
send_os_signal()
430446
return
431447

432-
# THREE: SEND MESSAGE
448+
# FOUR: SEND MESSAGE
433449
headers = create_userspace_message_headers(
434450
source=self._hierarchy.hierarchy_string('.'),
435451
destination=params.destination,
436452
data_handler=params.data_handler,
437453
operation_id=params.operation,
438454
campaign_id=self._campaign_id,
439455
request_id=uuid4(),
456+
encryption_scheme=params.encryption_scheme,
440457
)
441458
logger.debug(f'Send userspace message:\n{headers}')
442459
channel = f'{params.destination.replace(".", "/")}/request'

src/intersect_sdk/core_definitions.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Core enumerations and structures used throughout INTERSECT, for both client and service."""
22

33
from enum import Enum
4-
from typing import Annotated
4+
from typing import Annotated, Literal
55

66
from pydantic import Field
77

@@ -45,3 +45,6 @@ class IntersectDataHandler(Enum):
4545
4646
- 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.
4747
"""
48+
49+
IntersectEncryptionScheme = Literal['NONE', 'RSA']
50+
"""Supported encryption schemes throughout INTERSECT. 'NONE' implies no encryption scheme."""

0 commit comments

Comments
 (0)