-
Notifications
You must be signed in to change notification settings - Fork 48
Support encryption context on Message #276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -166,6 +166,122 @@ def wrap(cls, msg_id: _pulsar.MessageId): | |||||
| self._msg_id = msg_id | ||||||
| return self | ||||||
|
|
||||||
|
|
||||||
| class EncryptionKey: | ||||||
| """ | ||||||
| The key used for encryption. | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, key: _pulsar.EncryptionKey): | ||||||
| """ | ||||||
| Create EncryptionKey instance. | ||||||
|
|
||||||
| Parameters | ||||||
| ---------- | ||||||
| key: _pulsar.EncryptionKey | ||||||
| The underlying EncryptionKey instance from the C extension. | ||||||
| """ | ||||||
| self._key = key | ||||||
|
|
||||||
| @property | ||||||
| def key(self) -> str: | ||||||
| """ | ||||||
| Returns the key, which is usually the key file's name. | ||||||
| """ | ||||||
| return self._key.key | ||||||
|
|
||||||
| @property | ||||||
| def value(self) -> bytes: | ||||||
| """ | ||||||
| Returns the value, which is usually the key bytes used for encryption. | ||||||
| """ | ||||||
| return self._key.value() | ||||||
|
|
||||||
| @property | ||||||
| def metadata(self) -> dict: | ||||||
| """ | ||||||
| Returns the metadata associated with the key. | ||||||
| """ | ||||||
| return self._key.metadata | ||||||
|
|
||||||
| def __str__(self) -> str: | ||||||
| return f"EncryptionKey(key={self.key}, value_len={len(self.value)}, metadata={self.metadata})" | ||||||
|
|
||||||
| def __repr__(self) -> str: | ||||||
| return self.__str__() | ||||||
|
|
||||||
|
|
||||||
| class EncryptionContext: | ||||||
| """ | ||||||
| It contains encryption and compression information in it using which application can decrypt | ||||||
| consumed message with encrypted-payload. | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, context: _pulsar.EncryptionContext): | ||||||
| """ | ||||||
| Create EncryptionContext instance. | ||||||
|
|
||||||
| Parameters | ||||||
| ---------- | ||||||
| key: _pulsar.EncryptionContext | ||||||
| The underlying EncryptionContext instance from the C extension. | ||||||
| """ | ||||||
| self._context = context | ||||||
|
|
||||||
| def keys(self) -> List[EncryptionKey]: | ||||||
| """ | ||||||
| Returns all EncryptionKey instances when performing encryption. | ||||||
| """ | ||||||
| keys = self._context.keys() | ||||||
| return [EncryptionKey(key) for key in keys] | ||||||
|
|
||||||
| def param(self) -> bytes: | ||||||
| """ | ||||||
| Returns the encryption param bytes. | ||||||
| """ | ||||||
| return self._context.param() | ||||||
|
|
||||||
| def algorithm(self) -> str: | ||||||
| """ | ||||||
| Returns the encryption algorithm. | ||||||
| """ | ||||||
| return self._context.algorithm() | ||||||
|
|
||||||
| def compression_type(self) -> CompressionType: | ||||||
| """ | ||||||
| Returns the compression type of the message. | ||||||
| """ | ||||||
| return self._context.compression_type() | ||||||
|
|
||||||
| def uncompressed_message_size(self) -> int: | ||||||
| """ | ||||||
| Returns the uncompressed message size or 0 if the compression type is NONE. | ||||||
| """ | ||||||
| return self._context.uncompressed_message_size() | ||||||
|
|
||||||
| def batch_size(self) -> int: | ||||||
| """ | ||||||
| Returns the number of messages in the batch or -1 if the message is not batched. | ||||||
| """ | ||||||
| return self._context.batch_size() | ||||||
|
|
||||||
| def is_decryption_failed(self) -> bool: | ||||||
| """ | ||||||
| Returns whether decryption has failed for this message. | ||||||
| """ | ||||||
| return self._context.is_decryption_failed() | ||||||
|
|
||||||
| def __str__(self) -> str: | ||||||
| return f"EncryptionContext(algorithm={self.algorithm()}, " \ | ||||||
| f"compression_type={self.compression_type().name}, " \ | ||||||
| f"uncompressed_message_size={self.uncompressed_message_size()}, " \ | ||||||
| f"is_decryption_failed={self.is_decryption_failed()}, " \ | ||||||
| f"keys=[{', '.join(str(key) for key in self.keys())}])" | ||||||
|
|
||||||
| def __repr__(self) -> str: | ||||||
| return self.__str__() | ||||||
|
|
||||||
|
|
||||||
| class Message: | ||||||
| """ | ||||||
| Message objects are returned by a consumer, either by calling `receive` or | ||||||
|
|
@@ -250,6 +366,15 @@ def producer_name(self) -> str: | |||||
| """ | ||||||
| return self._message.producer_name() | ||||||
|
|
||||||
| def encryption_context(self) -> EncryptionContext: | ||||||
|
||||||
| def encryption_context(self) -> EncryptionContext: | |
| def encryption_context(self) -> Optional[EncryptionContext]: |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -167,6 +167,7 @@ def test_producer_send(self): | |||||||||||||||||||||||||||||||||
| consumer.acknowledge(msg) | ||||||||||||||||||||||||||||||||||
| print("receive from {}".format(msg.message_id())) | ||||||||||||||||||||||||||||||||||
| self.assertEqual(msg_id, msg.message_id()) | ||||||||||||||||||||||||||||||||||
| self.assertIsNone(msg.encryption_context()) | ||||||||||||||||||||||||||||||||||
| client.close() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def test_producer_access_mode_exclusive(self): | ||||||||||||||||||||||||||||||||||
|
|
@@ -489,15 +490,37 @@ def test_encryption_failure(self): | |||||||||||||||||||||||||||||||||
| client = Client(self.serviceUrl) | ||||||||||||||||||||||||||||||||||
| topic = "my-python-test-end-to-end-encryption-failure-" + str(time.time()) | ||||||||||||||||||||||||||||||||||
| producer = client.create_producer( | ||||||||||||||||||||||||||||||||||
| topic=topic, encryption_key="client-rsa.pem", crypto_key_reader=crypto_key_reader | ||||||||||||||||||||||||||||||||||
| topic=topic, encryption_key="client-rsa.pem", crypto_key_reader=crypto_key_reader, | ||||||||||||||||||||||||||||||||||
| compression_type=CompressionType.LZ4 | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| producer.send(b"msg-0") | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| enc_key = None | ||||||||||||||||||||||||||||||||||
| def verify_encryption_context(context: pulsar.EncryptionContext, failed: bool, batch_size: int): | ||||||||||||||||||||||||||||||||||
| nonlocal enc_key | ||||||||||||||||||||||||||||||||||
| keys = context.keys() | ||||||||||||||||||||||||||||||||||
| self.assertEqual(len(keys), 1) | ||||||||||||||||||||||||||||||||||
| key = keys[0] | ||||||||||||||||||||||||||||||||||
| self.assertEqual(key.key, "client-rsa.pem") | ||||||||||||||||||||||||||||||||||
| self.assertTrue(len(key.value) > 0) | ||||||||||||||||||||||||||||||||||
| if enc_key is None: | ||||||||||||||||||||||||||||||||||
| enc_key = key.value | ||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||
| self.assertEqual(key.value, enc_key) | ||||||||||||||||||||||||||||||||||
| self.assertEqual(key.metadata, {}) | ||||||||||||||||||||||||||||||||||
| self.assertTrue(len(context.param()) > 0) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+505
to
+511
|
||||||||||||||||||||||||||||||||||
| self.assertTrue(len(key.value) > 0) | |
| if enc_key is None: | |
| enc_key = key.value | |
| else: | |
| self.assertEqual(key.value, enc_key) | |
| self.assertEqual(key.metadata, {}) | |
| self.assertTrue(len(context.param()) > 0) | |
| self.assertGreater(len(key.value), 0) | |
| if enc_key is None: | |
| enc_key = key.value | |
| else: | |
| self.assertEqual(key.value, enc_key) | |
| self.assertEqual(key.metadata, {}) | |
| self.assertGreater(len(context.param()), 0) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assertTrue(a > b) cannot provide an informative message. Using assertGreater(a, b) instead will give more informative messages.
| self.assertTrue(len(context.param()) > 0) | |
| self.assertGreater(len(context.param()), 0) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop iterates only 3 times but there are 5 messages sent (msg-0, msg-1, msg-2, msg-3, msg-4). This should iterate 5 times using range(5) to verify all messages including the batched messages msg-3 and msg-4.
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop iterates only 3 times but there are 5 messages sent (msg-0, msg-1, msg-2, msg-3, msg-4). This should iterate 5 times using range(5) to verify all messages including the batched messages msg-3 and msg-4.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter name in the docstring is incorrect. It says "key: _pulsar.EncryptionContext" but should be "context: _pulsar.EncryptionContext" to match the actual parameter name.