1 |
|
# -*- coding: utf-8 -*- |
2 |
|
|
3 |
|
# Copyright 2018 Telefonica S.A. |
4 |
|
# |
5 |
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
6 |
|
# you may not use this file except in compliance with the License. |
7 |
|
# You may obtain a copy of the License at |
8 |
|
# |
9 |
|
# http://www.apache.org/licenses/LICENSE-2.0 |
10 |
|
# |
11 |
|
# Unless required by applicable law or agreed to in writing, software |
12 |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
13 |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
14 |
|
# implied. |
15 |
|
# See the License for the specific language governing permissions and |
16 |
|
# limitations under the License. |
17 |
|
|
18 |
1 |
from base64 import b64decode, b64encode |
19 |
1 |
from copy import deepcopy |
20 |
1 |
from http import HTTPStatus |
21 |
1 |
import logging |
22 |
1 |
import re |
23 |
1 |
from threading import Lock |
24 |
1 |
import typing |
25 |
|
|
26 |
|
|
27 |
1 |
from Crypto.Cipher import AES |
28 |
1 |
from motor.motor_asyncio import AsyncIOMotorClient |
29 |
1 |
from osm_common.common_utils import FakeLock |
30 |
1 |
import yaml |
31 |
|
|
32 |
1 |
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>" |
33 |
|
|
34 |
|
|
35 |
1 |
DB_NAME = "osm" |
36 |
|
|
37 |
|
|
38 |
1 |
class DbException(Exception): |
39 |
1 |
def __init__(self, message, http_code=HTTPStatus.NOT_FOUND): |
40 |
1 |
self.http_code = http_code |
41 |
1 |
Exception.__init__(self, "database exception " + str(message)) |
42 |
|
|
43 |
|
|
44 |
1 |
class DbBase(object): |
45 |
1 |
def __init__(self, encoding_type="ascii", logger_name="db", lock=False): |
46 |
|
""" |
47 |
|
Constructor of dbBase |
48 |
|
:param logger_name: logging name |
49 |
|
:param lock: Used to protect simultaneous access to the same instance class by several threads: |
50 |
|
False, None: Do not protect, this object will only be accessed by one thread |
51 |
|
True: This object needs to be protected by several threads accessing. |
52 |
|
Lock object. Use thi Lock for the threads access protection |
53 |
|
""" |
54 |
1 |
self.logger = logging.getLogger(logger_name) |
55 |
1 |
self.secret_key = None # 32 bytes length array used for encrypt/decrypt |
56 |
1 |
self.encrypt_mode = AES.MODE_ECB |
57 |
1 |
self.encoding_type = encoding_type |
58 |
1 |
if not lock: |
59 |
1 |
self.lock = FakeLock() |
60 |
1 |
elif lock is True: |
61 |
1 |
self.lock = Lock() |
62 |
0 |
elif isinstance(lock, Lock): |
63 |
0 |
self.lock = lock |
64 |
|
else: |
65 |
0 |
raise ValueError("lock parameter must be a Lock classclass or boolean") |
66 |
|
|
67 |
1 |
def db_connect(self, config, target_version=None): |
68 |
|
""" |
69 |
|
Connect to database |
70 |
|
:param config: Configuration of database. Contains among others: |
71 |
|
host: database host (mandatory) |
72 |
|
port: database port (mandatory) |
73 |
|
name: database name (mandatory) |
74 |
|
user: database username |
75 |
|
password: database password |
76 |
|
commonkey: common OSM key used for sensible information encryption |
77 |
|
materpassword: same as commonkey, for backward compatibility. Deprecated, to be removed in the future |
78 |
|
:param target_version: if provided it checks if database contains required version, raising exception otherwise. |
79 |
|
:return: None or raises DbException on error |
80 |
|
""" |
81 |
0 |
raise DbException("Method 'db_connect' not implemented") |
82 |
|
|
83 |
1 |
def db_disconnect(self): |
84 |
|
""" |
85 |
|
Disconnect from database |
86 |
|
:return: None |
87 |
|
""" |
88 |
0 |
pass |
89 |
|
|
90 |
1 |
def get_list(self, table, q_filter=None): |
91 |
|
""" |
92 |
|
Obtain a list of entries matching q_filter |
93 |
|
:param table: collection or table |
94 |
|
:param q_filter: Filter |
95 |
|
:return: a list (can be empty) with the found entries. Raises DbException on error |
96 |
|
""" |
97 |
0 |
raise DbException("Method 'get_list' not implemented") |
98 |
|
|
99 |
1 |
def count(self, table, q_filter=None): |
100 |
|
""" |
101 |
|
Count the number of entries matching q_filter |
102 |
|
:param table: collection or table |
103 |
|
:param q_filter: Filter |
104 |
|
:return: number of entries found (can be zero) |
105 |
|
:raise: DbException on error |
106 |
|
""" |
107 |
0 |
raise DbException("Method 'count' not implemented") |
108 |
|
|
109 |
1 |
def get_one(self, table, q_filter=None, fail_on_empty=True, fail_on_more=True): |
110 |
|
""" |
111 |
|
Obtain one entry matching q_filter |
112 |
|
:param table: collection or table |
113 |
|
:param q_filter: Filter |
114 |
|
:param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case |
115 |
|
it raises a DbException |
116 |
|
:param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so |
117 |
|
that it raises a DbException |
118 |
|
:return: The requested element, or None |
119 |
|
""" |
120 |
0 |
raise DbException("Method 'get_one' not implemented") |
121 |
|
|
122 |
1 |
def del_list(self, table, q_filter=None): |
123 |
|
""" |
124 |
|
Deletes all entries that match q_filter |
125 |
|
:param table: collection or table |
126 |
|
:param q_filter: Filter |
127 |
|
:return: Dict with the number of entries deleted |
128 |
|
""" |
129 |
0 |
raise DbException("Method 'del_list' not implemented") |
130 |
|
|
131 |
1 |
def del_one(self, table, q_filter=None, fail_on_empty=True): |
132 |
|
""" |
133 |
|
Deletes one entry that matches q_filter |
134 |
|
:param table: collection or table |
135 |
|
:param q_filter: Filter |
136 |
|
:param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in |
137 |
|
which case it raises a DbException |
138 |
|
:return: Dict with the number of entries deleted |
139 |
|
""" |
140 |
0 |
raise DbException("Method 'del_one' not implemented") |
141 |
|
|
142 |
1 |
def create(self, table, indata): |
143 |
|
""" |
144 |
|
Add a new entry at database |
145 |
|
:param table: collection or table |
146 |
|
:param indata: content to be added |
147 |
|
:return: database '_id' of the inserted element. Raises a DbException on error |
148 |
|
""" |
149 |
0 |
raise DbException("Method 'create' not implemented") |
150 |
|
|
151 |
1 |
def create_list(self, table, indata_list): |
152 |
|
""" |
153 |
|
Add several entries at once |
154 |
|
:param table: collection or table |
155 |
|
:param indata_list: list of elements to insert. Each element must be a dictionary. |
156 |
|
An '_id' key based on random uuid is added at each element if missing |
157 |
|
:return: list of inserted '_id's. Exception on error |
158 |
|
""" |
159 |
0 |
raise DbException("Method 'create_list' not implemented") |
160 |
|
|
161 |
1 |
def set_one( |
162 |
|
self, |
163 |
|
table, |
164 |
|
q_filter, |
165 |
|
update_dict, |
166 |
|
fail_on_empty=True, |
167 |
|
unset=None, |
168 |
|
pull=None, |
169 |
|
push=None, |
170 |
|
push_list=None, |
171 |
|
pull_list=None, |
172 |
|
): |
173 |
|
""" |
174 |
|
Modifies an entry at database |
175 |
|
:param table: collection or table |
176 |
|
:param q_filter: Filter |
177 |
|
:param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value |
178 |
|
:param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case |
179 |
|
it raises a DbException |
180 |
|
:param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is |
181 |
|
ignored. If not exist, it is ignored |
182 |
|
:param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value |
183 |
|
if exist in the array is removed. If not exist, it is ignored |
184 |
|
:param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value |
185 |
|
is appended to the end of the array |
186 |
|
:param pull_list: Same as pull but values are arrays where each item is removed from the array |
187 |
|
:param push_list: Same as push but values are arrays where each item is and appended instead of appending the |
188 |
|
whole array |
189 |
|
:return: Dict with the number of entries modified. None if no matching is found. |
190 |
|
""" |
191 |
0 |
raise DbException("Method 'set_one' not implemented") |
192 |
|
|
193 |
1 |
def set_list( |
194 |
|
self, |
195 |
|
table, |
196 |
|
q_filter, |
197 |
|
update_dict, |
198 |
|
unset=None, |
199 |
|
pull=None, |
200 |
|
push=None, |
201 |
|
push_list=None, |
202 |
|
pull_list=None, |
203 |
|
): |
204 |
|
""" |
205 |
|
Modifies al matching entries at database |
206 |
|
:param table: collection or table |
207 |
|
:param q_filter: Filter |
208 |
|
:param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value |
209 |
|
:param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is |
210 |
|
ignored. If not exist, it is ignored |
211 |
|
:param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value |
212 |
|
if exist in the array is removed. If not exist, it is ignored |
213 |
|
:param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value |
214 |
|
is appended to the end of the array |
215 |
|
:param pull_list: Same as pull but values are arrays where each item is removed from the array |
216 |
|
:param push_list: Same as push but values are arrays where each item is and appended instead of appending the |
217 |
|
whole array |
218 |
|
:return: Dict with the number of entries modified |
219 |
|
""" |
220 |
0 |
raise DbException("Method 'set_list' not implemented") |
221 |
|
|
222 |
1 |
def replace(self, table, _id, indata, fail_on_empty=True): |
223 |
|
""" |
224 |
|
Replace the content of an entry |
225 |
|
:param table: collection or table |
226 |
|
:param _id: internal database id |
227 |
|
:param indata: content to replace |
228 |
|
:param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case |
229 |
|
it raises a DbException |
230 |
|
:return: Dict with the number of entries replaced |
231 |
|
""" |
232 |
0 |
raise DbException("Method 'replace' not implemented") |
233 |
|
|
234 |
1 |
def _join_secret_key(self, update_key): |
235 |
|
""" |
236 |
|
Returns a xor byte combination of the internal secret_key and the provided update_key. |
237 |
|
It does not modify the internal secret_key. Used for adding salt, join keys, etc. |
238 |
|
:param update_key: Can be a string, byte or None. Recommended a long one (e.g. 32 byte length) |
239 |
|
:return: joined key in bytes with a 32 bytes length. Can be None if both internal secret_key and update_key |
240 |
|
are None |
241 |
|
""" |
242 |
1 |
if not update_key: |
243 |
1 |
return self.secret_key |
244 |
1 |
elif isinstance(update_key, str): |
245 |
1 |
update_key_bytes = update_key.encode() |
246 |
|
else: |
247 |
1 |
update_key_bytes = update_key |
248 |
|
|
249 |
1 |
new_secret_key = ( |
250 |
|
bytearray(self.secret_key) if self.secret_key else bytearray(32) |
251 |
|
) |
252 |
1 |
for i, b in enumerate(update_key_bytes): |
253 |
1 |
new_secret_key[i % 32] ^= b |
254 |
1 |
return bytes(new_secret_key) |
255 |
|
|
256 |
1 |
def set_secret_key(self, new_secret_key, replace=False): |
257 |
|
""" |
258 |
|
Updates internal secret_key used for encryption, with a byte xor |
259 |
|
:param new_secret_key: string or byte array. It is recommended a 32 byte length |
260 |
|
:param replace: if True, old value of internal secret_key is ignored and replaced. If false, a byte xor is used |
261 |
|
:return: None |
262 |
|
""" |
263 |
1 |
if replace: |
264 |
1 |
self.secret_key = None |
265 |
1 |
self.secret_key = self._join_secret_key(new_secret_key) |
266 |
|
|
267 |
1 |
def get_secret_key(self): |
268 |
|
""" |
269 |
|
Get the database secret key in case it is not done when "connect" is called. It can happens when database is |
270 |
|
empty after an initial install. It should skip if secret is already obtained. |
271 |
|
""" |
272 |
1 |
pass |
273 |
|
|
274 |
1 |
@staticmethod |
275 |
1 |
def pad_data(value: str) -> str: |
276 |
1 |
if not isinstance(value, str): |
277 |
1 |
raise DbException( |
278 |
|
f"Incorrect data type: type({value}), string is expected." |
279 |
|
) |
280 |
1 |
return value + ("\0" * ((16 - len(value)) % 16)) |
281 |
|
|
282 |
1 |
@staticmethod |
283 |
1 |
def unpad_data(value: str) -> str: |
284 |
1 |
if not isinstance(value, str): |
285 |
1 |
raise DbException( |
286 |
|
f"Incorrect data type: type({value}), string is expected." |
287 |
|
) |
288 |
1 |
return value.rstrip("\0") |
289 |
|
|
290 |
1 |
def _encrypt_value(self, value: str, schema_version: str, salt: str): |
291 |
|
"""Encrypt a value. |
292 |
|
|
293 |
|
Args: |
294 |
|
value (str): value to be encrypted. It is string/unicode |
295 |
|
schema_version (str): used for version control. If None or '1.0' no encryption is done. |
296 |
|
If '1.1' symmetric AES encryption is done |
297 |
|
salt (str): optional salt to be used. Must be str |
298 |
|
|
299 |
|
Returns: |
300 |
|
Encrypted content of value (str) |
301 |
|
|
302 |
|
""" |
303 |
1 |
if not self.secret_key or not schema_version or schema_version == "1.0": |
304 |
1 |
return value |
305 |
|
|
306 |
|
else: |
307 |
|
# Secret key as bytes |
308 |
1 |
secret_key = self._join_secret_key(salt) |
309 |
1 |
cipher = AES.new(secret_key, self.encrypt_mode) |
310 |
|
# Padded data as string |
311 |
1 |
padded_private_msg = self.pad_data(value) |
312 |
|
# Padded data as bytes |
313 |
1 |
padded_private_msg_bytes = padded_private_msg.encode(self.encoding_type) |
314 |
|
# Encrypt padded data |
315 |
1 |
encrypted_msg = cipher.encrypt(padded_private_msg_bytes) |
316 |
|
# Base64 encoded encrypted data |
317 |
1 |
encoded_encrypted_msg = b64encode(encrypted_msg) |
318 |
|
# Converting to string |
319 |
1 |
return encoded_encrypted_msg.decode(self.encoding_type) |
320 |
|
|
321 |
1 |
def encrypt(self, value: str, schema_version: str = None, salt: str = None) -> str: |
322 |
|
"""Encrypt a value. |
323 |
|
|
324 |
|
Args: |
325 |
|
value (str): value to be encrypted. It is string/unicode |
326 |
|
schema_version (str): used for version control. If None or '1.0' no encryption is done. |
327 |
|
If '1.1' symmetric AES encryption is done |
328 |
|
salt (str): optional salt to be used. Must be str |
329 |
|
|
330 |
|
Returns: |
331 |
|
Encrypted content of value (str) |
332 |
|
|
333 |
|
""" |
334 |
1 |
self.get_secret_key() |
335 |
1 |
return self._encrypt_value(value, schema_version, salt) |
336 |
|
|
337 |
1 |
def _decrypt_value(self, value: str, schema_version: str, salt: str) -> str: |
338 |
|
"""Decrypt an encrypted value. |
339 |
|
Args: |
340 |
|
|
341 |
|
value (str): value to be decrypted. It is a base64 string |
342 |
|
schema_version (str): used for known encryption method used. |
343 |
|
If None or '1.0' no encryption has been done. |
344 |
|
If '1.1' symmetric AES encryption has been done |
345 |
|
salt (str): optional salt to be used |
346 |
|
|
347 |
|
Returns: |
348 |
|
Plain content of value (str) |
349 |
|
|
350 |
|
""" |
351 |
1 |
if not self.secret_key or not schema_version or schema_version == "1.0": |
352 |
1 |
return value |
353 |
|
|
354 |
|
else: |
355 |
1 |
secret_key = self._join_secret_key(salt) |
356 |
|
# Decoding encrypted data, output bytes |
357 |
1 |
encrypted_msg = b64decode(value) |
358 |
1 |
cipher = AES.new(secret_key, self.encrypt_mode) |
359 |
|
# Decrypted data, output bytes |
360 |
1 |
decrypted_msg = cipher.decrypt(encrypted_msg) |
361 |
1 |
try: |
362 |
|
# Converting to string |
363 |
1 |
private_msg = decrypted_msg.decode(self.encoding_type) |
364 |
1 |
except UnicodeDecodeError: |
365 |
1 |
raise DbException( |
366 |
|
"Cannot decrypt information. Are you using same COMMONKEY in all OSM components?", |
367 |
|
http_code=HTTPStatus.INTERNAL_SERVER_ERROR, |
368 |
|
) |
369 |
|
# Unpadded data as string |
370 |
1 |
return self.unpad_data(private_msg) |
371 |
|
|
372 |
1 |
def decrypt(self, value: str, schema_version: str = None, salt: str = None) -> str: |
373 |
|
"""Decrypt an encrypted value. |
374 |
|
Args: |
375 |
|
|
376 |
|
value (str): value to be decrypted. It is a base64 string |
377 |
|
schema_version (str): used for known encryption method used. |
378 |
|
If None or '1.0' no encryption has been done. |
379 |
|
If '1.1' symmetric AES encryption has been done |
380 |
|
salt (str): optional salt to be used |
381 |
|
|
382 |
|
Returns: |
383 |
|
Plain content of value (str) |
384 |
|
|
385 |
|
""" |
386 |
1 |
self.get_secret_key() |
387 |
1 |
return self._decrypt_value(value, schema_version, salt) |
388 |
|
|
389 |
1 |
def encrypt_decrypt_fields( |
390 |
|
self, item, action, fields=None, flags=None, schema_version=None, salt=None |
391 |
|
): |
392 |
0 |
if not fields: |
393 |
0 |
return |
394 |
0 |
self.get_secret_key() |
395 |
0 |
actions = ["encrypt", "decrypt"] |
396 |
0 |
if action.lower() not in actions: |
397 |
0 |
raise DbException( |
398 |
|
"Unknown action ({}): Must be one of {}".format(action, actions), |
399 |
|
http_code=HTTPStatus.INTERNAL_SERVER_ERROR, |
400 |
|
) |
401 |
0 |
method = self.encrypt if action.lower() == "encrypt" else self.decrypt |
402 |
0 |
if flags is None: |
403 |
0 |
flags = re.I |
404 |
|
|
405 |
0 |
def process(_item): |
406 |
0 |
if isinstance(_item, list): |
407 |
0 |
for elem in _item: |
408 |
0 |
process(elem) |
409 |
0 |
elif isinstance(_item, dict): |
410 |
0 |
for key, val in _item.items(): |
411 |
0 |
if isinstance(val, str): |
412 |
0 |
if any(re.search(f, key, flags) for f in fields): |
413 |
0 |
_item[key] = method(val, schema_version, salt) |
414 |
|
else: |
415 |
0 |
process(val) |
416 |
|
|
417 |
0 |
process(item) |
418 |
|
|
419 |
|
|
420 |
1 |
def deep_update_rfc7396(dict_to_change, dict_reference, key_list=None): |
421 |
|
""" |
422 |
|
Modifies one dictionary with the information of the other following https://tools.ietf.org/html/rfc7396 |
423 |
|
Basically is a recursive python 'dict_to_change.update(dict_reference)', but a value of None is used to delete. |
424 |
|
It implements an extra feature that allows modifying an array. RFC7396 only allows replacing the entire array. |
425 |
|
For that, dict_reference should contains a dict with keys starting by "$" with the following meaning: |
426 |
|
$[index] <index> is an integer for targeting a concrete index from dict_to_change array. If the value is None |
427 |
|
the element of the array is deleted, otherwise it is edited. |
428 |
|
$+[index] The value is inserted at this <index>. A value of None has not sense and an exception is raised. |
429 |
|
$+ The value is appended at the end. A value of None has not sense and an exception is raised. |
430 |
|
$val It looks for all the items in the array dict_to_change equal to <val>. <val> is evaluated as yaml, |
431 |
|
that is, numbers are taken as type int, true/false as boolean, etc. Use quotes to force string. |
432 |
|
Nothing happens if no match is found. If the value is None the matched elements are deleted. |
433 |
|
$key: val In case a dictionary is passed in yaml format, if looks for all items in the array dict_to_change |
434 |
|
that are dictionaries and contains this <key> equal to <val>. Several keys can be used by yaml |
435 |
|
format '{key: val, key: val, ...}'; and all of them must match. Nothing happens if no match is |
436 |
|
found. If value is None the matched items are deleted, otherwise they are edited. |
437 |
|
$+val If no match if found (see '$val'), the value is appended to the array. If any match is found nothing |
438 |
|
is changed. A value of None has not sense. |
439 |
|
$+key: val If no match if found (see '$key: val'), the value is appended to the array. If any match is found |
440 |
|
nothing is changed. A value of None has not sense. |
441 |
|
If there are several editions, insertions and deletions; editions and deletions are done first in reverse index |
442 |
|
order; then insertions also in reverse index order; and finally appends in any order. So indexes used at |
443 |
|
insertions must take into account the deleted items. |
444 |
|
:param dict_to_change: Target dictionary to be changed. |
445 |
|
:param dict_reference: Dictionary that contains changes to be applied. |
446 |
|
:param key_list: This is used internally for recursive calls. Do not fill this parameter. |
447 |
|
:return: none or raises and exception only at array modification when there is a bad format or conflict. |
448 |
|
""" |
449 |
|
|
450 |
1 |
def _deep_update_array(array_to_change, _dict_reference, _key_list): |
451 |
1 |
to_append = {} |
452 |
1 |
to_insert_at_index = {} |
453 |
1 |
values_to_edit_delete = {} |
454 |
1 |
indexes_to_edit_delete = [] |
455 |
1 |
array_edition = None |
456 |
1 |
_key_list.append("") |
457 |
1 |
for k in _dict_reference: |
458 |
1 |
_key_list[-1] = str(k) |
459 |
1 |
if not isinstance(k, str) or not k.startswith("$"): |
460 |
1 |
if array_edition is True: |
461 |
1 |
raise DbException( |
462 |
|
"Found array edition (keys starting with '$') and pure dictionary edition in the" |
463 |
|
" same dict at '{}'".format(":".join(_key_list[:-1])) |
464 |
|
) |
465 |
1 |
array_edition = False |
466 |
1 |
continue |
467 |
|
else: |
468 |
1 |
if array_edition is False: |
469 |
0 |
raise DbException( |
470 |
|
"Found array edition (keys starting with '$') and pure dictionary edition in the" |
471 |
|
" same dict at '{}'".format(":".join(_key_list[:-1])) |
472 |
|
) |
473 |
1 |
array_edition = True |
474 |
1 |
insert = False |
475 |
1 |
indexes = [] # indexes to edit or insert |
476 |
1 |
kitem = k[1:] |
477 |
1 |
if kitem.startswith("+"): |
478 |
1 |
insert = True |
479 |
1 |
kitem = kitem[1:] |
480 |
1 |
if _dict_reference[k] is None: |
481 |
1 |
raise DbException( |
482 |
|
"A value of None has not sense for insertions at '{}'".format( |
483 |
|
":".join(_key_list) |
484 |
|
) |
485 |
|
) |
486 |
|
|
487 |
1 |
if kitem.startswith("[") and kitem.endswith("]"): |
488 |
1 |
try: |
489 |
1 |
index = int(kitem[1:-1]) |
490 |
1 |
if index < 0: |
491 |
1 |
index += len(array_to_change) |
492 |
1 |
if index < 0: |
493 |
0 |
index = 0 # skip outside index edition |
494 |
1 |
indexes.append(index) |
495 |
0 |
except Exception: |
496 |
0 |
raise DbException( |
497 |
|
"Wrong format at '{}'. Expecting integer index inside quotes".format( |
498 |
|
":".join(_key_list) |
499 |
|
) |
500 |
|
) |
501 |
1 |
elif kitem: |
502 |
|
# match_found_skip = False |
503 |
1 |
try: |
504 |
1 |
filter_in = yaml.safe_load(kitem) |
505 |
1 |
except Exception: |
506 |
1 |
raise DbException( |
507 |
|
"Wrong format at '{}'. Expecting '$<yaml-format>'".format( |
508 |
|
":".join(_key_list) |
509 |
|
) |
510 |
|
) |
511 |
1 |
if isinstance(filter_in, dict): |
512 |
1 |
for index, item in enumerate(array_to_change): |
513 |
1 |
for filter_k, filter_v in filter_in.items(): |
514 |
1 |
if ( |
515 |
|
not isinstance(item, dict) |
516 |
|
or filter_k not in item |
517 |
|
or item[filter_k] != filter_v |
518 |
|
): |
519 |
1 |
break |
520 |
|
else: # match found |
521 |
1 |
if insert: |
522 |
|
# match_found_skip = True |
523 |
0 |
insert = False |
524 |
0 |
break |
525 |
|
else: |
526 |
1 |
indexes.append(index) |
527 |
|
else: |
528 |
1 |
index = 0 |
529 |
1 |
try: |
530 |
1 |
while True: # if not match a ValueError exception will be raise |
531 |
1 |
index = array_to_change.index(filter_in, index) |
532 |
1 |
if insert: |
533 |
|
# match_found_skip = True |
534 |
1 |
insert = False |
535 |
1 |
break |
536 |
1 |
indexes.append(index) |
537 |
1 |
index += 1 |
538 |
1 |
except ValueError: |
539 |
1 |
pass |
540 |
|
|
541 |
|
# if match_found_skip: |
542 |
|
# continue |
543 |
1 |
elif not insert: |
544 |
1 |
raise DbException( |
545 |
|
"Wrong format at '{}'. Expecting '$+', '$[<index]' or '$[<filter>]'".format( |
546 |
|
":".join(_key_list) |
547 |
|
) |
548 |
|
) |
549 |
1 |
for index in indexes: |
550 |
1 |
if insert: |
551 |
1 |
if ( |
552 |
|
index in to_insert_at_index |
553 |
|
and to_insert_at_index[index] != _dict_reference[k] |
554 |
|
): |
555 |
|
# Several different insertions on the same item of the array |
556 |
0 |
raise DbException( |
557 |
|
"Conflict at '{}'. Several insertions on same array index {}".format( |
558 |
|
":".join(_key_list), index |
559 |
|
) |
560 |
|
) |
561 |
1 |
to_insert_at_index[index] = _dict_reference[k] |
562 |
|
else: |
563 |
1 |
if ( |
564 |
|
index in indexes_to_edit_delete |
565 |
|
and values_to_edit_delete[index] != _dict_reference[k] |
566 |
|
): |
567 |
|
# Several different editions on the same item of the array |
568 |
1 |
raise DbException( |
569 |
|
"Conflict at '{}'. Several editions on array index {}".format( |
570 |
|
":".join(_key_list), index |
571 |
|
) |
572 |
|
) |
573 |
1 |
indexes_to_edit_delete.append(index) |
574 |
1 |
values_to_edit_delete[index] = _dict_reference[k] |
575 |
1 |
if not indexes: |
576 |
1 |
if insert: |
577 |
1 |
to_append[k] = _dict_reference[k] |
578 |
|
# elif _dict_reference[k] is not None: |
579 |
|
# raise DbException("Not found any match to edit in the array, or wrong format at '{}'".format( |
580 |
|
# ":".join(_key_list))) |
581 |
|
|
582 |
|
# edition/deletion is done before insertion |
583 |
1 |
indexes_to_edit_delete.sort(reverse=True) |
584 |
1 |
for index in indexes_to_edit_delete: |
585 |
1 |
_key_list[-1] = str(index) |
586 |
1 |
try: |
587 |
1 |
if values_to_edit_delete[index] is None: # None->Anything |
588 |
1 |
try: |
589 |
1 |
del array_to_change[index] |
590 |
1 |
except IndexError: |
591 |
1 |
pass # it is not consider an error if this index does not exist |
592 |
1 |
elif not isinstance( |
593 |
|
values_to_edit_delete[index], dict |
594 |
|
): # NotDict->Anything |
595 |
1 |
array_to_change[index] = deepcopy(values_to_edit_delete[index]) |
596 |
1 |
elif isinstance(array_to_change[index], dict): # Dict->Dict |
597 |
1 |
deep_update_rfc7396( |
598 |
|
array_to_change[index], values_to_edit_delete[index], _key_list |
599 |
|
) |
600 |
|
else: # Dict->NotDict |
601 |
1 |
if isinstance( |
602 |
|
array_to_change[index], list |
603 |
|
): # Dict->List. Check extra array edition |
604 |
1 |
if _deep_update_array( |
605 |
|
array_to_change[index], |
606 |
|
values_to_edit_delete[index], |
607 |
|
_key_list, |
608 |
|
): |
609 |
1 |
continue |
610 |
1 |
array_to_change[index] = deepcopy(values_to_edit_delete[index]) |
611 |
|
# calling deep_update_rfc7396 to delete the None values |
612 |
1 |
deep_update_rfc7396( |
613 |
|
array_to_change[index], values_to_edit_delete[index], _key_list |
614 |
|
) |
615 |
1 |
except IndexError: |
616 |
1 |
raise DbException( |
617 |
|
"Array edition index out of range at '{}'".format( |
618 |
|
":".join(_key_list) |
619 |
|
) |
620 |
|
) |
621 |
|
|
622 |
|
# insertion with indexes |
623 |
1 |
to_insert_indexes = list(to_insert_at_index.keys()) |
624 |
1 |
to_insert_indexes.sort(reverse=True) |
625 |
1 |
for index in to_insert_indexes: |
626 |
1 |
array_to_change.insert(index, to_insert_at_index[index]) |
627 |
|
|
628 |
|
# append |
629 |
1 |
for k, insert_value in to_append.items(): |
630 |
1 |
_key_list[-1] = str(k) |
631 |
1 |
insert_value_copy = deepcopy(insert_value) |
632 |
1 |
if isinstance(insert_value_copy, dict): |
633 |
|
# calling deep_update_rfc7396 to delete the None values |
634 |
0 |
deep_update_rfc7396(insert_value_copy, insert_value, _key_list) |
635 |
1 |
array_to_change.append(insert_value_copy) |
636 |
|
|
637 |
1 |
_key_list.pop() |
638 |
1 |
if array_edition: |
639 |
1 |
return True |
640 |
1 |
return False |
641 |
|
|
642 |
1 |
if key_list is None: |
643 |
1 |
key_list = [] |
644 |
1 |
key_list.append("") |
645 |
1 |
for k in dict_reference: |
646 |
1 |
key_list[-1] = str(k) |
647 |
1 |
if dict_reference[k] is None: # None->Anything |
648 |
1 |
if k in dict_to_change: |
649 |
1 |
del dict_to_change[k] |
650 |
1 |
elif not isinstance(dict_reference[k], dict): # NotDict->Anything |
651 |
1 |
dict_to_change[k] = deepcopy(dict_reference[k]) |
652 |
1 |
elif k not in dict_to_change: # Dict->Empty |
653 |
1 |
dict_to_change[k] = deepcopy(dict_reference[k]) |
654 |
|
# calling deep_update_rfc7396 to delete the None values |
655 |
1 |
deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list) |
656 |
1 |
elif isinstance(dict_to_change[k], dict): # Dict->Dict |
657 |
1 |
deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list) |
658 |
|
else: # Dict->NotDict |
659 |
1 |
if isinstance( |
660 |
|
dict_to_change[k], list |
661 |
|
): # Dict->List. Check extra array edition |
662 |
1 |
if _deep_update_array(dict_to_change[k], dict_reference[k], key_list): |
663 |
1 |
continue |
664 |
1 |
dict_to_change[k] = deepcopy(dict_reference[k]) |
665 |
|
# calling deep_update_rfc7396 to delete the None values |
666 |
1 |
deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list) |
667 |
1 |
key_list.pop() |
668 |
|
|
669 |
|
|
670 |
1 |
def deep_update(dict_to_change, dict_reference): |
671 |
|
"""Maintained for backward compatibility. Use deep_update_rfc7396 instead""" |
672 |
1 |
return deep_update_rfc7396(dict_to_change, dict_reference) |
673 |
|
|
674 |
|
|
675 |
1 |
class Encryption(DbBase): |
676 |
1 |
def __init__(self, uri, config, encoding_type="ascii", logger_name="db"): |
677 |
|
"""Constructor. |
678 |
|
|
679 |
|
Args: |
680 |
|
uri (str): Connection string to connect to the database. |
681 |
|
config (dict): Additional database info |
682 |
|
encoding_type (str): ascii, utf-8 etc. |
683 |
|
logger_name (str): Logger name |
684 |
|
|
685 |
|
""" |
686 |
1 |
self._secret_key = None # 32 bytes length array used for encrypt/decrypt |
687 |
1 |
self.encrypt_mode = AES.MODE_ECB |
688 |
1 |
super(Encryption, self).__init__( |
689 |
|
encoding_type=encoding_type, logger_name=logger_name |
690 |
|
) |
691 |
1 |
self._client = AsyncIOMotorClient(uri) |
692 |
1 |
self._config = config |
693 |
|
|
694 |
1 |
@property |
695 |
1 |
def secret_key(self): |
696 |
1 |
return self._secret_key |
697 |
|
|
698 |
1 |
@secret_key.setter |
699 |
1 |
def secret_key(self, value): |
700 |
1 |
self._secret_key = value |
701 |
|
|
702 |
1 |
@property |
703 |
1 |
def _database(self): |
704 |
1 |
return self._client[DB_NAME] |
705 |
|
|
706 |
1 |
@property |
707 |
1 |
def _admin_collection(self): |
708 |
1 |
return self._database["admin"] |
709 |
|
|
710 |
1 |
@property |
711 |
1 |
def database_key(self): |
712 |
1 |
return self._config.get("database_commonkey") |
713 |
|
|
714 |
1 |
async def decrypt_fields( |
715 |
|
self, |
716 |
|
item: dict, |
717 |
|
fields: typing.List[str], |
718 |
|
schema_version: str = None, |
719 |
|
salt: str = None, |
720 |
|
) -> None: |
721 |
|
"""Decrypt fields from a dictionary. Follows the same logic as in osm_common. |
722 |
|
|
723 |
|
Args: |
724 |
|
|
725 |
|
item (dict): Dictionary with the keys to be decrypted |
726 |
|
fields (list): List of keys to decrypt |
727 |
|
schema version (str): Schema version. (i.e. 1.11) |
728 |
|
salt (str): Salt for the decryption |
729 |
|
|
730 |
|
""" |
731 |
1 |
flags = re.I |
732 |
|
|
733 |
1 |
async def process(_item): |
734 |
1 |
if isinstance(_item, list): |
735 |
0 |
for elem in _item: |
736 |
0 |
await process(elem) |
737 |
1 |
elif isinstance(_item, dict): |
738 |
1 |
for key, val in _item.items(): |
739 |
1 |
if isinstance(val, str): |
740 |
1 |
if any(re.search(f, key, flags) for f in fields): |
741 |
1 |
_item[key] = await self.decrypt(val, schema_version, salt) |
742 |
|
else: |
743 |
0 |
await process(val) |
744 |
|
|
745 |
1 |
await process(item) |
746 |
|
|
747 |
1 |
async def encrypt( |
748 |
|
self, value: str, schema_version: str = None, salt: str = None |
749 |
|
) -> str: |
750 |
|
"""Encrypt a value. |
751 |
|
|
752 |
|
Args: |
753 |
|
value (str): value to be encrypted. It is string/unicode |
754 |
|
schema_version (str): used for version control. If None or '1.0' no encryption is done. |
755 |
|
If '1.1' symmetric AES encryption is done |
756 |
|
salt (str): optional salt to be used. Must be str |
757 |
|
|
758 |
|
Returns: |
759 |
|
Encrypted content of value (str) |
760 |
|
|
761 |
|
""" |
762 |
1 |
await self.get_secret_key() |
763 |
1 |
return self._encrypt_value(value, schema_version, salt) |
764 |
|
|
765 |
1 |
async def decrypt( |
766 |
|
self, value: str, schema_version: str = None, salt: str = None |
767 |
|
) -> str: |
768 |
|
"""Decrypt an encrypted value. |
769 |
|
Args: |
770 |
|
|
771 |
|
value (str): value to be decrypted. It is a base64 string |
772 |
|
schema_version (str): used for known encryption method used. |
773 |
|
If None or '1.0' no encryption has been done. |
774 |
|
If '1.1' symmetric AES encryption has been done |
775 |
|
salt (str): optional salt to be used |
776 |
|
|
777 |
|
Returns: |
778 |
|
Plain content of value (str) |
779 |
|
|
780 |
|
""" |
781 |
1 |
await self.get_secret_key() |
782 |
1 |
return self._decrypt_value(value, schema_version, salt) |
783 |
|
|
784 |
1 |
def _join_secret_key(self, update_key: typing.Any) -> bytes: |
785 |
|
"""Join key with secret key. |
786 |
|
|
787 |
|
Args: |
788 |
|
|
789 |
|
update_key (str or bytes): str or bytes with the to update |
790 |
|
|
791 |
|
Returns: |
792 |
|
|
793 |
|
Joined key (bytes) |
794 |
|
""" |
795 |
1 |
return self._join_keys(update_key, self.secret_key) |
796 |
|
|
797 |
1 |
def _join_keys(self, key: typing.Any, secret_key: bytes) -> bytes: |
798 |
|
"""Join key with secret_key. |
799 |
|
|
800 |
|
Args: |
801 |
|
|
802 |
|
key (str or bytes): str or bytes of the key to update |
803 |
|
secret_key (bytes): bytes of the secret key |
804 |
|
|
805 |
|
Returns: |
806 |
|
|
807 |
|
Joined key (bytes) |
808 |
|
""" |
809 |
1 |
if isinstance(key, str): |
810 |
1 |
update_key_bytes = key.encode(self.encoding_type) |
811 |
|
else: |
812 |
1 |
update_key_bytes = key |
813 |
1 |
new_secret_key = bytearray(secret_key) if secret_key else bytearray(32) |
814 |
1 |
for i, b in enumerate(update_key_bytes): |
815 |
1 |
new_secret_key[i % 32] ^= b |
816 |
1 |
return bytes(new_secret_key) |
817 |
|
|
818 |
1 |
async def get_secret_key(self): |
819 |
|
"""Get secret key using the database key and the serial key in the DB. |
820 |
|
The key is populated in the property self.secret_key. |
821 |
|
""" |
822 |
1 |
if self.secret_key: |
823 |
1 |
return |
824 |
1 |
secret_key = None |
825 |
1 |
if self.database_key: |
826 |
1 |
secret_key = self._join_keys(self.database_key, None) |
827 |
1 |
version_data = await self._admin_collection.find_one({"_id": "version"}) |
828 |
1 |
if version_data and version_data.get("serial"): |
829 |
1 |
secret_key = self._join_keys(b64decode(version_data["serial"]), secret_key) |
830 |
1 |
self._secret_key = secret_key |