Skip to content

Commit 416d9ae

Browse files
authored
Merge pull request #364 from dkropachev/dk/sync-with-upstream-3.28.0
Sync with upstream 3.28.0
2 parents 18f5f01 + 569271f commit 416d9ae

35 files changed

+815
-103
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*.swo
44
*.so
55
*.egg
6+
*.eggs
67
*.egg-info
78
*.attr
89
.tox

CHANGELOG.rst

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,33 @@
1+
3.28.0
2+
======
3+
June 5, 2023
4+
5+
Features
6+
--------
7+
* Add support for vector type (PYTHON-1352)
8+
* Cryptography module is now an optional dependency (PYTHON-1351)
9+
10+
Bug Fixes
11+
---------
12+
* Store IV along with encrypted text when using column-level encryption (PYTHON-1350)
13+
* Create session-specific protocol handlers to contain session-specific CLE policies (PYTHON-1356)
14+
15+
Others
16+
------
17+
* Use Cython for smoke builds (PYTHON-1343)
18+
* Don't fail when inserting UDTs with prepared queries with some missing fields (PR 1151)
19+
* Convert print statement to function in docs (PR 1157)
20+
* Update comment for retry policy (DOC-3278)
21+
* Added error handling blog reference (DOC-2813)
22+
23+
3.27.0
24+
======
25+
May 1, 2023
26+
27+
Features
28+
--------
29+
* Add support for client-side encryption (PYTHON-1341)
30+
131
3.26.0
232
======
333
March 13, 2023
@@ -17,7 +47,7 @@ Others
1747
* Fix deprecation warning in query tracing (PR 1103)
1848
* Remove mutable default values from some tests (PR 1116)
1949
* Remove dependency on unittest2 (PYTHON-1289)
20-
* Fix deprecation warnings for asyncio.coroutine annotation in asyncioreactor (PYTTHON-1290)
50+
* Fix deprecation warnings for asyncio.coroutine annotation in asyncioreactor (PYTHON-1290)
2151
* Fix typos in source files (PR 1126)
2252
* HostFilterPolicyInitTest fix for Python 3.11 (PR 1131)
2353
* Fix for DontPrepareOnIgnoredHostsTest (PYTHON-1287)

README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ Contributing
6969
------------
7070
See `CONTRIBUTING <https://github.com/scylladb/python-driver/blob/master/CONTRIBUTING.rst>`_.
7171

72+
Error Handling
73+
------------
74+
While originally written for the Java driver, users may reference the `Cassandra error handling done right blog <https://www.datastax.com/blog/cassandra-error-handling-done-right>`_ for resolving error handling scenarios with Apache Cassandra.
75+
7276
Reporting Problems
7377
------------------
7478
Please report any bugs and make any feature requests by clicking the New Issue button in

cassandra/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def emit(self, record):
2323

2424
logging.getLogger('cassandra').addHandler(NullHandler())
2525

26-
__version_info__ = (3, 26, 9)
26+
__version_info__ = (3, 28, 0)
2727
__version__ = '.'.join(map(str, __version_info__))
2828

2929

cassandra/cluster.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ def default_retry_policy(self, policy):
10151015
cloud = None
10161016
"""
10171017
A dict of the cloud configuration. Example::
1018-
1018+
10191019
{
10201020
# path to the secure connect bundle
10211021
'secure_connect_bundle': '/path/to/secure-connect-dbname.zip',
@@ -1034,6 +1034,12 @@ def default_retry_policy(self, policy):
10341034
or to disable the shardaware port (advanced shardaware)
10351035
"""
10361036

1037+
column_encryption_policy = None
1038+
"""
1039+
An instance of :class:`cassandra.policies.ColumnEncryptionPolicy` specifying encryption materials to be
1040+
used for columns in this cluster.
1041+
"""
1042+
10371043
metadata_request_timeout = datetime.timedelta(seconds=2)
10381044
"""
10391045
Timeout for all queries used by driver it self.
@@ -1157,6 +1163,7 @@ def __init__(self,
11571163
scylla_cloud=None,
11581164
shard_aware_options=None,
11591165
metadata_request_timeout=None,
1166+
column_encryption_policy=None,
11601167
):
11611168
"""
11621169
``executor_threads`` defines the number of threads in a pool for handling asynchronous tasks such as
@@ -1234,6 +1241,9 @@ def __init__(self,
12341241

12351242
self.port = port
12361243

1244+
if column_encryption_policy is not None:
1245+
self.column_encryption_policy = column_encryption_policy
1246+
12371247
self.endpoint_factory = endpoint_factory or DefaultEndPointFactory(port=self.port)
12381248
self.endpoint_factory.configure(self)
12391249

@@ -1533,7 +1543,7 @@ def __init__(self, street, zipcode):
15331543
# results will include Address instances
15341544
results = session.execute("SELECT * FROM users")
15351545
row = results[0]
1536-
print row.id, row.location.street, row.location.zipcode
1546+
print(row.id, row.location.street, row.location.zipcode)
15371547
15381548
"""
15391549
if self.protocol_version < 3:
@@ -2678,6 +2688,17 @@ def __init__(self, cluster, hosts, keyspace=None):
26782688
self.session_id = uuid.uuid4()
26792689
self._graph_paging_available = self._check_graph_paging_available()
26802690

2691+
if self.cluster.column_encryption_policy is not None:
2692+
try:
2693+
self.client_protocol_handler = type(
2694+
str(self.session_id) + "-ProtocolHandler",
2695+
(ProtocolHandler,),
2696+
{"column_encryption_policy": self.cluster.column_encryption_policy})
2697+
except AttributeError:
2698+
log.info("Unable to set column encryption policy for session")
2699+
raise Exception(
2700+
"column_encryption_policy is temporary disabled, until https://github.com/scylladb/python-driver/issues/365 is sorted out")
2701+
26812702
if self.cluster.monitor_reporting_enabled:
26822703
cc_host = self.cluster.get_control_connection_host()
26832704
valid_insights_version = (cc_host and version_supports_insights(cc_host.dse_version))
@@ -3197,7 +3218,7 @@ def prepare(self, query, custom_payload=None, keyspace=None):
31973218
prepared_keyspace = keyspace if keyspace else None
31983219
prepared_statement = PreparedStatement.from_message(
31993220
response.query_id, response.bind_metadata, response.pk_indexes, self.cluster.metadata, query, prepared_keyspace,
3200-
self._protocol_version, response.column_metadata, response.result_metadata_id)
3221+
self._protocol_version, response.column_metadata, response.result_metadata_id, self.cluster.column_encryption_policy)
32013222
prepared_statement.custom_payload = future.custom_payload
32023223

32033224
self.cluster.add_prepared(response.query_id, prepared_statement)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Copyright DataStax, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from collections import namedtuple
16+
from functools import lru_cache
17+
18+
import logging
19+
import os
20+
21+
log = logging.getLogger(__name__)
22+
23+
from cassandra.cqltypes import _cqltypes
24+
from cassandra.policies import ColumnEncryptionPolicy
25+
26+
from cryptography.hazmat.primitives import padding
27+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
28+
29+
AES256_BLOCK_SIZE = 128
30+
AES256_BLOCK_SIZE_BYTES = int(AES256_BLOCK_SIZE / 8)
31+
AES256_KEY_SIZE = 256
32+
AES256_KEY_SIZE_BYTES = int(AES256_KEY_SIZE / 8)
33+
34+
ColData = namedtuple('ColData', ['key','type'])
35+
36+
class AES256ColumnEncryptionPolicy(ColumnEncryptionPolicy):
37+
38+
# Fix block cipher mode for now. IV size is a function of block cipher used
39+
# so fixing this avoids (possibly unnecessary) validation logic here.
40+
mode = modes.CBC
41+
42+
# "iv" param here expects a bytearray that's the same size as the block
43+
# size for AES-256 (128 bits or 16 bytes). If none is provided a new one
44+
# will be randomly generated, but in this case the IV should be recorded and
45+
# preserved or else you will not be able to decrypt any data encrypted by this
46+
# policy.
47+
def __init__(self, iv=None):
48+
49+
# CBC uses an IV that's the same size as the block size
50+
#
51+
# Avoid defining IV with a default arg in order to stay away from
52+
# any issues around the caching of default args
53+
self.iv = iv
54+
if self.iv:
55+
if not len(self.iv) == AES256_BLOCK_SIZE_BYTES:
56+
raise ValueError("This policy uses AES-256 with CBC mode and therefore expects a 128-bit initialization vector")
57+
else:
58+
self.iv = os.urandom(AES256_BLOCK_SIZE_BYTES)
59+
60+
# ColData for a given ColDesc is always preserved. We only create a Cipher
61+
# when there's an actual need to for a given ColDesc
62+
self.coldata = {}
63+
self.ciphers = {}
64+
65+
def encrypt(self, coldesc, obj_bytes):
66+
67+
# AES256 has a 128-bit block size so if the input bytes don't align perfectly on
68+
# those blocks we have to pad them. There's plenty of room for optimization here:
69+
#
70+
# * Instances of the PKCS7 padder should be managed in a bounded pool
71+
# * It would be nice if we could get a flag from encrypted data to indicate
72+
# whether it was padded or not
73+
# * Might be able to make this happen with a leading block of flags in encrypted data
74+
padder = padding.PKCS7(AES256_BLOCK_SIZE).padder()
75+
padded_bytes = padder.update(obj_bytes) + padder.finalize()
76+
77+
cipher = self._get_cipher(coldesc)
78+
encryptor = cipher.encryptor()
79+
return self.iv + encryptor.update(padded_bytes) + encryptor.finalize()
80+
81+
def decrypt(self, coldesc, bytes):
82+
83+
iv = bytes[:AES256_BLOCK_SIZE_BYTES]
84+
encrypted_bytes = bytes[AES256_BLOCK_SIZE_BYTES:]
85+
cipher = self._get_cipher(coldesc, iv=iv)
86+
decryptor = cipher.decryptor()
87+
padded_bytes = decryptor.update(encrypted_bytes) + decryptor.finalize()
88+
89+
unpadder = padding.PKCS7(AES256_BLOCK_SIZE).unpadder()
90+
return unpadder.update(padded_bytes) + unpadder.finalize()
91+
92+
def add_column(self, coldesc, key, type):
93+
94+
if not coldesc:
95+
raise ValueError("ColDesc supplied to add_column cannot be None")
96+
if not key:
97+
raise ValueError("Key supplied to add_column cannot be None")
98+
if not type:
99+
raise ValueError("Type supplied to add_column cannot be None")
100+
if type not in _cqltypes.keys():
101+
raise ValueError("Type %s is not a supported type".format(type))
102+
if not len(key) == AES256_KEY_SIZE_BYTES:
103+
raise ValueError("AES256 column encryption policy expects a 256-bit encryption key")
104+
self.coldata[coldesc] = ColData(key, _cqltypes[type])
105+
106+
def contains_column(self, coldesc):
107+
return coldesc in self.coldata
108+
109+
def encode_and_encrypt(self, coldesc, obj):
110+
if not coldesc:
111+
raise ValueError("ColDesc supplied to encode_and_encrypt cannot be None")
112+
if not obj:
113+
raise ValueError("Object supplied to encode_and_encrypt cannot be None")
114+
coldata = self.coldata.get(coldesc)
115+
if not coldata:
116+
raise ValueError("Could not find ColData for ColDesc %s".format(coldesc))
117+
return self.encrypt(coldesc, coldata.type.serialize(obj, None))
118+
119+
def cache_info(self):
120+
return AES256ColumnEncryptionPolicy._build_cipher.cache_info()
121+
122+
def column_type(self, coldesc):
123+
return self.coldata[coldesc].type
124+
125+
def _get_cipher(self, coldesc, iv=None):
126+
"""
127+
Access relevant state from this instance necessary to create a Cipher and then get one,
128+
hopefully returning a cached instance if we've already done so (and it hasn't been evicted)
129+
"""
130+
try:
131+
coldata = self.coldata[coldesc]
132+
return AES256ColumnEncryptionPolicy._build_cipher(coldata.key, iv or self.iv)
133+
except KeyError:
134+
raise ValueError("Could not find column {}".format(coldesc))
135+
136+
# Explicitly use a class method here to avoid caching self
137+
@lru_cache(maxsize=128)
138+
def _build_cipher(key, iv):
139+
return Cipher(algorithms.AES256(key), AES256ColumnEncryptionPolicy.mode(iv))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright DataStax, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
try:
16+
import cryptography
17+
from cassandra.column_encryption._policies import *
18+
except ImportError:
19+
# Cryptography is not installed
20+
pass

cassandra/cqlengine/query.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,15 +285,15 @@ class ContextQuery(object):
285285
286286
with ContextQuery(Automobile, keyspace='test2') as A:
287287
A.objects.create(manufacturer='honda', year=2008, model='civic')
288-
print len(A.objects.all()) # 1 result
288+
print(len(A.objects.all())) # 1 result
289289
290290
with ContextQuery(Automobile, keyspace='test4') as A:
291-
print len(A.objects.all()) # 0 result
291+
print(len(A.objects.all())) # 0 result
292292
293293
# Multiple models
294294
with ContextQuery(Automobile, Automobile2, connection='cluster2') as (A, A2):
295-
print len(A.objects.all())
296-
print len(A2.objects.all())
295+
print(len(A.objects.all()))
296+
print(len(A2.objects.all()))
297297
298298
"""
299299

@@ -808,11 +808,11 @@ class Comment(Model):
808808
809809
print("Normal")
810810
for comment in Comment.objects(photo_id=u):
811-
print comment.comment_id
811+
print(comment.comment_id)
812812
813813
print("Reversed")
814814
for comment in Comment.objects(photo_id=u).order_by("-comment_id"):
815-
print comment.comment_id
815+
print(comment.comment_id)
816816
"""
817817
if len(colnames) == 0:
818818
clone = copy.deepcopy(self)

0 commit comments

Comments
 (0)