Skip to content

Commit dcb1cab

Browse files
committed
#30 - initial scaffolding for SDK encryption
Signed-off-by: Lance-Drane <Lance-Drane@users.noreply.github.com>
1 parent e09f13f commit dcb1cab

16 files changed

Lines changed: 409 additions & 41 deletions

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

@@ -116,6 +114,11 @@ class UserspaceMessageHeaders(BaseModel):
116114
This should only be set to "True" on return messages sent by services - NEVER clients.
117115
"""
118116

117+
encryption_scheme: IntersectEncryptionScheme = 'NONE'
118+
"""
119+
The encryption scheme of the message itself. This determines the requested payload.
120+
"""
121+
119122
# make sure all non-string fields are serialized into strings, even in Python code
120123

121124
@field_serializer('message_id', mode='plain')
@@ -140,6 +143,7 @@ def create_userspace_message_headers(
140143
destination: str,
141144
operation_id: str,
142145
data_handler: IntersectDataHandler,
146+
encryption_scheme: IntersectEncryptionScheme,
143147
message_id: uuid.UUID | None = None,
144148
has_error: bool = False,
145149
) -> dict[str, str]:
@@ -153,6 +157,7 @@ def create_userspace_message_headers(
153157
created_at=datetime.datetime.now(tz=datetime.timezone.utc),
154158
operation_id=operation_id,
155159
data_handler=data_handler,
160+
encryption_scheme=encryption_scheme,
156161
has_error=has_error,
157162
).model_dump(by_alias=True)
158163

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
@@ -262,6 +263,13 @@ def _handle_userspace_message(
262263
request_params = self._data_plane_manager.incoming_message_data_handler(
263264
payload, headers.data_handler
264265
)
266+
if not headers.has_error:
267+
match headers.encryption_scheme:
268+
case 'RSA':
269+
# TODO - decrypt and reassign request_params here
270+
pass
271+
case _:
272+
pass
265273
if content_type == 'application/json':
266274
request_params = GENERIC_MESSAGE_SERIALIZER.validate_json(request_params)
267275
except ValidationError as e:
@@ -415,7 +423,15 @@ def _send_userspace_message(self, params: IntersectDirectMessageParams) -> None:
415423
return
416424
serialized_msg = params.payload
417425

418-
# TWO: SEND DATA TO APPROPRIATE DATA STORE
426+
# TWO: encrypt message
427+
match params.encryption_scheme:
428+
case 'RSA':
429+
# TODO reassign serialized_msg here to encrypted value
430+
pass
431+
case _:
432+
pass
433+
434+
# THREE: SEND DATA TO APPROPRIATE DATA STORE
419435
try:
420436
payload = self._data_plane_manager.outgoing_message_data_handler(
421437
serialized_msg, params.content_type, params.data_handler
@@ -427,12 +443,13 @@ def _send_userspace_message(self, params: IntersectDirectMessageParams) -> None:
427443
send_os_signal()
428444
return
429445

430-
# THREE: SEND MESSAGE
446+
# FOUR: SEND MESSAGE
431447
headers = create_userspace_message_headers(
432448
source=self._hierarchy.hierarchy_string('.'),
433449
destination=params.destination,
434450
data_handler=params.data_handler,
435451
operation_id=params.operation,
452+
encryption_scheme=params.encryption_scheme,
436453
)
437454
logger.debug(f'Send userspace message:\n{headers}')
438455
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)