blob: d0d4fb094d913a530aa1b8960d0fa1a0c6b33fed [file] [log] [blame]
tierno87858ca2018-10-08 16:30:15 +02001# -*- 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.
Gulsum Aticia06b8542023-05-09 13:42:13 +030017
aticig3dd0db62022-03-04 19:35:45 +030018from base64 import b64decode, b64encode
19from copy import deepcopy
20from http import HTTPStatus
tierno87858ca2018-10-08 16:30:15 +020021import logging
delacruzramo54a54642019-10-25 16:50:13 +020022import re
tierno1e9a3292018-11-05 18:18:45 +010023from threading import Lock
Gulsum Atici76394ef2023-01-09 23:19:18 +030024import typing
25
tierno5c012612018-04-19 16:01:59 +020026
aticig3dd0db62022-03-04 19:35:45 +030027from Crypto.Cipher import AES
Gulsum Atici76394ef2023-01-09 23:19:18 +030028from motor.motor_asyncio import AsyncIOMotorClient
aticig3dd0db62022-03-04 19:35:45 +030029from osm_common.common_utils import FakeLock
30import yaml
31
tierno5c012612018-04-19 16:01:59 +020032__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
33
34
Gulsum Atici76394ef2023-01-09 23:19:18 +030035DB_NAME = "osm"
36
37
tierno5c012612018-04-19 16:01:59 +020038class DbException(Exception):
tierno5c012612018-04-19 16:01:59 +020039 def __init__(self, message, http_code=HTTPStatus.NOT_FOUND):
40 self.http_code = http_code
tierno87858ca2018-10-08 16:30:15 +020041 Exception.__init__(self, "database exception " + str(message))
tierno5c012612018-04-19 16:01:59 +020042
43
44class DbBase(object):
Gulsum Atici76394ef2023-01-09 23:19:18 +030045 def __init__(self, encoding_type="ascii", logger_name="db", lock=False):
tierno87858ca2018-10-08 16:30:15 +020046 """
tierno1e9a3292018-11-05 18:18:45 +010047 Constructor of dbBase
tierno87858ca2018-10-08 16:30:15 +020048 :param logger_name: logging name
tierno1e9a3292018-11-05 18:18:45 +010049 :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
tierno87858ca2018-10-08 16:30:15 +020053 """
54 self.logger = logging.getLogger(logger_name)
tiernoeef7cb72018-11-12 11:51:49 +010055 self.secret_key = None # 32 bytes length array used for encrypt/decrypt
Gulsum Atici76394ef2023-01-09 23:19:18 +030056 self.encrypt_mode = AES.MODE_ECB
57 self.encoding_type = encoding_type
tierno1e9a3292018-11-05 18:18:45 +010058 if not lock:
59 self.lock = FakeLock()
60 elif lock is True:
61 self.lock = Lock()
62 elif isinstance(lock, Lock):
63 self.lock = lock
64 else:
65 raise ValueError("lock parameter must be a Lock classclass or boolean")
tierno5c012612018-04-19 16:01:59 +020066
tierno136f2952018-10-19 13:01:03 +020067 def db_connect(self, config, target_version=None):
tierno87858ca2018-10-08 16:30:15 +020068 """
69 Connect to database
tiernocfc52722018-10-23 11:41:49 +020070 :param config: Configuration of database. Contains among others:
tierno81b47d52020-01-21 10:11:34 +000071 host: database host (mandatory)
tiernocfc52722018-10-23 11:41:49 +020072 port: database port (mandatory)
73 name: database name (mandatory)
74 user: database username
75 password: database password
tiernoeef7cb72018-11-12 11:51:49 +010076 commonkey: common OSM key used for sensible information encryption
77 materpassword: same as commonkey, for backward compatibility. Deprecated, to be removed in the future
tierno136f2952018-10-19 13:01:03 +020078 :param target_version: if provided it checks if database contains required version, raising exception otherwise.
tierno87858ca2018-10-08 16:30:15 +020079 :return: None or raises DbException on error
80 """
tierno136f2952018-10-19 13:01:03 +020081 raise DbException("Method 'db_connect' not implemented")
tierno5c012612018-04-19 16:01:59 +020082
83 def db_disconnect(self):
tierno87858ca2018-10-08 16:30:15 +020084 """
85 Disconnect from database
86 :return: None
87 """
tierno5c012612018-04-19 16:01:59 +020088 pass
89
tierno87858ca2018-10-08 16:30:15 +020090 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 """
tiernoebbf3532018-05-03 17:49:37 +020097 raise DbException("Method 'get_list' not implemented")
tierno5c012612018-04-19 16:01:59 +020098
delacruzramoae049d82019-09-17 16:05:17 +020099 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 raise DbException("Method 'count' not implemented")
108
tierno87858ca2018-10-08 16:30:15 +0200109 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 """
tiernoebbf3532018-05-03 17:49:37 +0200120 raise DbException("Method 'get_one' not implemented")
tierno5c012612018-04-19 16:01:59 +0200121
tierno87858ca2018-10-08 16:30:15 +0200122 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 """
tiernoebbf3532018-05-03 17:49:37 +0200129 raise DbException("Method 'del_list' not implemented")
tierno5c012612018-04-19 16:01:59 +0200130
tierno87858ca2018-10-08 16:30:15 +0200131 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 """
tiernoebbf3532018-05-03 17:49:37 +0200140 raise DbException("Method 'del_one' not implemented")
tiernob3e750b2018-09-05 11:25:23 +0200141
tierno87858ca2018-10-08 16:30:15 +0200142 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
tierno2c9794c2020-04-29 10:24:28 +0000147 :return: database '_id' of the inserted element. Raises a DbException on error
tierno87858ca2018-10-08 16:30:15 +0200148 """
149 raise DbException("Method 'create' not implemented")
tiernob3e750b2018-09-05 11:25:23 +0200150
tierno2c9794c2020-04-29 10:24:28 +0000151 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 raise DbException("Method 'create_list' not implemented")
160
garciadeblas2644b762021-03-24 09:21:01 +0100161 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 ):
tierno87858ca2018-10-08 16:30:15 +0200173 """
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
tiernod63ea272018-11-27 12:03:36 +0100180 :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
tierno0d8e4bc2020-06-22 12:18:18 +0000186 :param pull_list: Same as pull but values are arrays where each item is removed from the array
tierno399f6c32020-05-12 07:36:41 +0000187 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
188 whole array
tierno87858ca2018-10-08 16:30:15 +0200189 :return: Dict with the number of entries modified. None if no matching is found.
190 """
191 raise DbException("Method 'set_one' not implemented")
192
garciadeblas2644b762021-03-24 09:21:01 +0100193 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 ):
tierno87858ca2018-10-08 16:30:15 +0200204 """
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
delacruzramof71fcff2020-02-11 11:14:07 +0000209 :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
tierno0d8e4bc2020-06-22 12:18:18 +0000215 :param pull_list: Same as pull but values are arrays where each item is removed from the array
tierno399f6c32020-05-12 07:36:41 +0000216 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
217 whole array
tierno87858ca2018-10-08 16:30:15 +0200218 :return: Dict with the number of entries modified
219 """
220 raise DbException("Method 'set_list' not implemented")
221
222 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 raise DbException("Method 'replace' not implemented")
233
tiernoeef7cb72018-11-12 11:51:49 +0100234 def _join_secret_key(self, update_key):
tierno136f2952018-10-19 13:01:03 +0200235 """
tiernoeef7cb72018-11-12 11:51:49 +0100236 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
tierno136f2952018-10-19 13:01:03 +0200241 """
tiernoeef7cb72018-11-12 11:51:49 +0100242 if not update_key:
243 return self.secret_key
244 elif isinstance(update_key, str):
245 update_key_bytes = update_key.encode()
246 else:
247 update_key_bytes = update_key
tierno136f2952018-10-19 13:01:03 +0200248
garciadeblas2644b762021-03-24 09:21:01 +0100249 new_secret_key = (
250 bytearray(self.secret_key) if self.secret_key else bytearray(32)
251 )
tiernoeef7cb72018-11-12 11:51:49 +0100252 for i, b in enumerate(update_key_bytes):
253 new_secret_key[i % 32] ^= b
254 return bytes(new_secret_key)
255
256 def set_secret_key(self, new_secret_key, replace=False):
tierno136f2952018-10-19 13:01:03 +0200257 """
tiernoeef7cb72018-11-12 11:51:49 +0100258 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
tierno136f2952018-10-19 13:01:03 +0200261 :return: None
262 """
tiernoeef7cb72018-11-12 11:51:49 +0100263 if replace:
264 self.secret_key = None
265 self.secret_key = self._join_secret_key(new_secret_key)
tierno136f2952018-10-19 13:01:03 +0200266
tiernoc5297e42019-12-11 12:32:41 +0000267 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 pass
273
Gulsum Atici76394ef2023-01-09 23:19:18 +0300274 @staticmethod
275 def pad_data(value: str) -> str:
276 if not isinstance(value, str):
277 raise DbException(
278 f"Incorrect data type: type({value}), string is expected."
279 )
280 return value + ("\0" * ((16 - len(value)) % 16))
tierno87858ca2018-10-08 16:30:15 +0200281
Gulsum Atici76394ef2023-01-09 23:19:18 +0300282 @staticmethod
283 def unpad_data(value: str) -> str:
284 if not isinstance(value, str):
285 raise DbException(
286 f"Incorrect data type: type({value}), string is expected."
287 )
288 return value.rstrip("\0")
289
290 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
tierno87858ca2018-10-08 16:30:15 +0200302 """
garciadeblas2644b762021-03-24 09:21:01 +0100303 if not self.secret_key or not schema_version or schema_version == "1.0":
tierno136f2952018-10-19 13:01:03 +0200304 return value
Gulsum Atici76394ef2023-01-09 23:19:18 +0300305
306 else:
307 # Secret key as bytes
308 secret_key = self._join_secret_key(salt)
309 cipher = AES.new(secret_key, self.encrypt_mode)
310 # Padded data as string
311 padded_private_msg = self.pad_data(value)
312 # Padded data as bytes
313 padded_private_msg_bytes = padded_private_msg.encode(self.encoding_type)
314 # Encrypt padded data
315 encrypted_msg = cipher.encrypt(padded_private_msg_bytes)
316 # Base64 encoded encrypted data
317 encoded_encrypted_msg = b64encode(encrypted_msg)
318 # Converting to string
319 return encoded_encrypted_msg.decode(self.encoding_type)
320
321 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 self.get_secret_key()
335 return self._encrypt_value(value, schema_version, salt)
336
337 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 if not self.secret_key or not schema_version or schema_version == "1.0":
352 return value
353
tierno136f2952018-10-19 13:01:03 +0200354 else:
tiernoeef7cb72018-11-12 11:51:49 +0100355 secret_key = self._join_secret_key(salt)
Gulsum Atici76394ef2023-01-09 23:19:18 +0300356 # Decoding encrypted data, output bytes
tierno136f2952018-10-19 13:01:03 +0200357 encrypted_msg = b64decode(value)
Gulsum Atici76394ef2023-01-09 23:19:18 +0300358 cipher = AES.new(secret_key, self.encrypt_mode)
359 # Decrypted data, output bytes
tierno136f2952018-10-19 13:01:03 +0200360 decrypted_msg = cipher.decrypt(encrypted_msg)
tiernobd5a4022019-01-30 09:48:38 +0000361 try:
Gulsum Atici76394ef2023-01-09 23:19:18 +0300362 # Converting to string
363 private_msg = decrypted_msg.decode(self.encoding_type)
tiernobd5a4022019-01-30 09:48:38 +0000364 except UnicodeDecodeError:
garciadeblas2644b762021-03-24 09:21:01 +0100365 raise DbException(
366 "Cannot decrypt information. Are you using same COMMONKEY in all OSM components?",
367 http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
368 )
Gulsum Atici76394ef2023-01-09 23:19:18 +0300369 # Unpadded data as string
370 return self.unpad_data(private_msg)
371
372 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 self.get_secret_key()
387 return self._decrypt_value(value, schema_version, salt)
tierno87858ca2018-10-08 16:30:15 +0200388
garciadeblas2644b762021-03-24 09:21:01 +0100389 def encrypt_decrypt_fields(
390 self, item, action, fields=None, flags=None, schema_version=None, salt=None
391 ):
delacruzramo54a54642019-10-25 16:50:13 +0200392 if not fields:
393 return
tiernoc5297e42019-12-11 12:32:41 +0000394 self.get_secret_key()
garciadeblas2644b762021-03-24 09:21:01 +0100395 actions = ["encrypt", "decrypt"]
delacruzramo54a54642019-10-25 16:50:13 +0200396 if action.lower() not in actions:
garciadeblas2644b762021-03-24 09:21:01 +0100397 raise DbException(
398 "Unknown action ({}): Must be one of {}".format(action, actions),
399 http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
400 )
401 method = self.encrypt if action.lower() == "encrypt" else self.decrypt
tiernoafc5cb62020-05-12 11:17:54 +0000402 if flags is None:
403 flags = re.I
delacruzramo54a54642019-10-25 16:50:13 +0200404
tiernoafc5cb62020-05-12 11:17:54 +0000405 def process(_item):
406 if isinstance(_item, list):
407 for elem in _item:
delacruzramo54a54642019-10-25 16:50:13 +0200408 process(elem)
tiernoafc5cb62020-05-12 11:17:54 +0000409 elif isinstance(_item, dict):
410 for key, val in _item.items():
411 if isinstance(val, str):
412 if any(re.search(f, key, flags) for f in fields):
413 _item[key] = method(val, schema_version, salt)
delacruzramo54a54642019-10-25 16:50:13 +0200414 else:
415 process(val)
garciadeblas2644b762021-03-24 09:21:01 +0100416
delacruzramo54a54642019-10-25 16:50:13 +0200417 process(item)
418
tierno87858ca2018-10-08 16:30:15 +0200419
420def deep_update_rfc7396(dict_to_change, dict_reference, key_list=None):
tiernob3e750b2018-09-05 11:25:23 +0200421 """
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
tierno3e759152019-08-28 16:08:25 +0000435 format '{key: val, key: val, ...}'; and all of them must match. Nothing happens if no match is
tiernob3e750b2018-09-05 11:25:23 +0200436 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 """
garciadeblas2644b762021-03-24 09:21:01 +0100449
tiernob3e750b2018-09-05 11:25:23 +0200450 def _deep_update_array(array_to_change, _dict_reference, _key_list):
451 to_append = {}
452 to_insert_at_index = {}
453 values_to_edit_delete = {}
454 indexes_to_edit_delete = []
455 array_edition = None
456 _key_list.append("")
457 for k in _dict_reference:
458 _key_list[-1] = str(k)
459 if not isinstance(k, str) or not k.startswith("$"):
460 if array_edition is True:
garciadeblas2644b762021-03-24 09:21:01 +0100461 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 )
tiernob3e750b2018-09-05 11:25:23 +0200465 array_edition = False
466 continue
467 else:
468 if array_edition is False:
garciadeblas2644b762021-03-24 09:21:01 +0100469 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 )
tiernob3e750b2018-09-05 11:25:23 +0200473 array_edition = True
474 insert = False
475 indexes = [] # indexes to edit or insert
476 kitem = k[1:]
garciadeblas2644b762021-03-24 09:21:01 +0100477 if kitem.startswith("+"):
tiernob3e750b2018-09-05 11:25:23 +0200478 insert = True
479 kitem = kitem[1:]
480 if _dict_reference[k] is None:
garciadeblas2644b762021-03-24 09:21:01 +0100481 raise DbException(
482 "A value of None has not sense for insertions at '{}'".format(
483 ":".join(_key_list)
484 )
485 )
tiernob3e750b2018-09-05 11:25:23 +0200486
garciadeblas2644b762021-03-24 09:21:01 +0100487 if kitem.startswith("[") and kitem.endswith("]"):
tiernob3e750b2018-09-05 11:25:23 +0200488 try:
489 index = int(kitem[1:-1])
490 if index < 0:
491 index += len(array_to_change)
492 if index < 0:
493 index = 0 # skip outside index edition
494 indexes.append(index)
495 except Exception:
garciadeblas2644b762021-03-24 09:21:01 +0100496 raise DbException(
497 "Wrong format at '{}'. Expecting integer index inside quotes".format(
498 ":".join(_key_list)
499 )
500 )
tiernob3e750b2018-09-05 11:25:23 +0200501 elif kitem:
502 # match_found_skip = False
503 try:
504 filter_in = yaml.safe_load(kitem)
505 except Exception:
garciadeblas2644b762021-03-24 09:21:01 +0100506 raise DbException(
507 "Wrong format at '{}'. Expecting '$<yaml-format>'".format(
508 ":".join(_key_list)
509 )
510 )
tiernob3e750b2018-09-05 11:25:23 +0200511 if isinstance(filter_in, dict):
512 for index, item in enumerate(array_to_change):
513 for filter_k, filter_v in filter_in.items():
garciadeblas2644b762021-03-24 09:21:01 +0100514 if (
515 not isinstance(item, dict)
516 or filter_k not in item
517 or item[filter_k] != filter_v
518 ):
tiernob3e750b2018-09-05 11:25:23 +0200519 break
520 else: # match found
521 if insert:
522 # match_found_skip = True
523 insert = False
524 break
525 else:
526 indexes.append(index)
527 else:
528 index = 0
529 try:
530 while True: # if not match a ValueError exception will be raise
531 index = array_to_change.index(filter_in, index)
532 if insert:
533 # match_found_skip = True
534 insert = False
535 break
536 indexes.append(index)
537 index += 1
538 except ValueError:
539 pass
540
541 # if match_found_skip:
542 # continue
543 elif not insert:
garciadeblas2644b762021-03-24 09:21:01 +0100544 raise DbException(
545 "Wrong format at '{}'. Expecting '$+', '$[<index]' or '$[<filter>]'".format(
546 ":".join(_key_list)
547 )
548 )
tiernob3e750b2018-09-05 11:25:23 +0200549 for index in indexes:
550 if insert:
garciadeblas2644b762021-03-24 09:21:01 +0100551 if (
552 index in to_insert_at_index
553 and to_insert_at_index[index] != _dict_reference[k]
554 ):
tiernob3e750b2018-09-05 11:25:23 +0200555 # Several different insertions on the same item of the array
garciadeblas2644b762021-03-24 09:21:01 +0100556 raise DbException(
557 "Conflict at '{}'. Several insertions on same array index {}".format(
558 ":".join(_key_list), index
559 )
560 )
tiernob3e750b2018-09-05 11:25:23 +0200561 to_insert_at_index[index] = _dict_reference[k]
562 else:
garciadeblas2644b762021-03-24 09:21:01 +0100563 if (
564 index in indexes_to_edit_delete
565 and values_to_edit_delete[index] != _dict_reference[k]
566 ):
tiernob3e750b2018-09-05 11:25:23 +0200567 # Several different editions on the same item of the array
garciadeblas2644b762021-03-24 09:21:01 +0100568 raise DbException(
569 "Conflict at '{}'. Several editions on array index {}".format(
570 ":".join(_key_list), index
571 )
572 )
tiernob3e750b2018-09-05 11:25:23 +0200573 indexes_to_edit_delete.append(index)
574 values_to_edit_delete[index] = _dict_reference[k]
575 if not indexes:
576 if insert:
577 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 indexes_to_edit_delete.sort(reverse=True)
584 for index in indexes_to_edit_delete:
585 _key_list[-1] = str(index)
586 try:
587 if values_to_edit_delete[index] is None: # None->Anything
588 try:
garciadeblas2644b762021-03-24 09:21:01 +0100589 del array_to_change[index]
tiernob3e750b2018-09-05 11:25:23 +0200590 except IndexError:
591 pass # it is not consider an error if this index does not exist
garciadeblas2644b762021-03-24 09:21:01 +0100592 elif not isinstance(
593 values_to_edit_delete[index], dict
594 ): # NotDict->Anything
tiernob3e750b2018-09-05 11:25:23 +0200595 array_to_change[index] = deepcopy(values_to_edit_delete[index])
596 elif isinstance(array_to_change[index], dict): # Dict->Dict
garciadeblas2644b762021-03-24 09:21:01 +0100597 deep_update_rfc7396(
598 array_to_change[index], values_to_edit_delete[index], _key_list
599 )
tiernob3e750b2018-09-05 11:25:23 +0200600 else: # Dict->NotDict
garciadeblas2644b762021-03-24 09:21:01 +0100601 if isinstance(
602 array_to_change[index], list
603 ): # Dict->List. Check extra array edition
604 if _deep_update_array(
605 array_to_change[index],
606 values_to_edit_delete[index],
607 _key_list,
608 ):
tiernob3e750b2018-09-05 11:25:23 +0200609 continue
610 array_to_change[index] = deepcopy(values_to_edit_delete[index])
tierno87858ca2018-10-08 16:30:15 +0200611 # calling deep_update_rfc7396 to delete the None values
garciadeblas2644b762021-03-24 09:21:01 +0100612 deep_update_rfc7396(
613 array_to_change[index], values_to_edit_delete[index], _key_list
614 )
tiernob3e750b2018-09-05 11:25:23 +0200615 except IndexError:
garciadeblas2644b762021-03-24 09:21:01 +0100616 raise DbException(
617 "Array edition index out of range at '{}'".format(
618 ":".join(_key_list)
619 )
620 )
tiernob3e750b2018-09-05 11:25:23 +0200621
622 # insertion with indexes
623 to_insert_indexes = list(to_insert_at_index.keys())
624 to_insert_indexes.sort(reverse=True)
625 for index in to_insert_indexes:
626 array_to_change.insert(index, to_insert_at_index[index])
627
628 # append
629 for k, insert_value in to_append.items():
630 _key_list[-1] = str(k)
631 insert_value_copy = deepcopy(insert_value)
632 if isinstance(insert_value_copy, dict):
tierno87858ca2018-10-08 16:30:15 +0200633 # calling deep_update_rfc7396 to delete the None values
634 deep_update_rfc7396(insert_value_copy, insert_value, _key_list)
tiernob3e750b2018-09-05 11:25:23 +0200635 array_to_change.append(insert_value_copy)
636
637 _key_list.pop()
638 if array_edition:
639 return True
640 return False
641
642 if key_list is None:
643 key_list = []
644 key_list.append("")
645 for k in dict_reference:
646 key_list[-1] = str(k)
garciadeblas2644b762021-03-24 09:21:01 +0100647 if dict_reference[k] is None: # None->Anything
tiernob3e750b2018-09-05 11:25:23 +0200648 if k in dict_to_change:
649 del dict_to_change[k]
650 elif not isinstance(dict_reference[k], dict): # NotDict->Anything
651 dict_to_change[k] = deepcopy(dict_reference[k])
652 elif k not in dict_to_change: # Dict->Empty
653 dict_to_change[k] = deepcopy(dict_reference[k])
tierno87858ca2018-10-08 16:30:15 +0200654 # calling deep_update_rfc7396 to delete the None values
655 deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list)
tiernob3e750b2018-09-05 11:25:23 +0200656 elif isinstance(dict_to_change[k], dict): # Dict->Dict
tierno87858ca2018-10-08 16:30:15 +0200657 deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list)
garciadeblas2644b762021-03-24 09:21:01 +0100658 else: # Dict->NotDict
659 if isinstance(
660 dict_to_change[k], list
661 ): # Dict->List. Check extra array edition
tiernob3e750b2018-09-05 11:25:23 +0200662 if _deep_update_array(dict_to_change[k], dict_reference[k], key_list):
663 continue
664 dict_to_change[k] = deepcopy(dict_reference[k])
tierno87858ca2018-10-08 16:30:15 +0200665 # calling deep_update_rfc7396 to delete the None values
666 deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list)
tiernob3e750b2018-09-05 11:25:23 +0200667 key_list.pop()
tierno87858ca2018-10-08 16:30:15 +0200668
669
670def deep_update(dict_to_change, dict_reference):
garciadeblas2644b762021-03-24 09:21:01 +0100671 """Maintained for backward compatibility. Use deep_update_rfc7396 instead"""
tierno87858ca2018-10-08 16:30:15 +0200672 return deep_update_rfc7396(dict_to_change, dict_reference)
Gulsum Atici76394ef2023-01-09 23:19:18 +0300673
674
675class Encryption(DbBase):
Gulsum Aticia06b8542023-05-09 13:42:13 +0300676 def __init__(self, uri, config, encoding_type="ascii", logger_name="db"):
Gulsum Atici76394ef2023-01-09 23:19:18 +0300677 """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.
Gulsum Atici76394ef2023-01-09 23:19:18 +0300683 logger_name (str): Logger name
684
685 """
Gulsum Atici76394ef2023-01-09 23:19:18 +0300686 self._secret_key = None # 32 bytes length array used for encrypt/decrypt
687 self.encrypt_mode = AES.MODE_ECB
688 super(Encryption, self).__init__(
689 encoding_type=encoding_type, logger_name=logger_name
690 )
691 self._client = AsyncIOMotorClient(uri)
692 self._config = config
693
694 @property
695 def secret_key(self):
696 return self._secret_key
697
698 @secret_key.setter
699 def secret_key(self, value):
700 self._secret_key = value
701
702 @property
703 def _database(self):
704 return self._client[DB_NAME]
705
706 @property
707 def _admin_collection(self):
708 return self._database["admin"]
709
710 @property
711 def database_key(self):
712 return self._config.get("database_commonkey")
713
714 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 flags = re.I
732
733 async def process(_item):
734 if isinstance(_item, list):
735 for elem in _item:
736 await process(elem)
737 elif isinstance(_item, dict):
738 for key, val in _item.items():
739 if isinstance(val, str):
740 if any(re.search(f, key, flags) for f in fields):
741 _item[key] = await self.decrypt(val, schema_version, salt)
742 else:
743 await process(val)
744
745 await process(item)
746
747 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 await self.get_secret_key()
763 return self._encrypt_value(value, schema_version, salt)
764
765 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 await self.get_secret_key()
782 return self._decrypt_value(value, schema_version, salt)
783
784 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 return self._join_keys(update_key, self.secret_key)
796
797 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 if isinstance(key, str):
810 update_key_bytes = key.encode(self.encoding_type)
811 else:
812 update_key_bytes = key
813 new_secret_key = bytearray(secret_key) if secret_key else bytearray(32)
814 for i, b in enumerate(update_key_bytes):
815 new_secret_key[i % 32] ^= b
816 return bytes(new_secret_key)
817
818 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 if self.secret_key:
823 return
824 secret_key = None
825 if self.database_key:
826 secret_key = self._join_keys(self.database_key, None)
827 version_data = await self._admin_collection.find_one({"_id": "version"})
828 if version_data and version_data.get("serial"):
829 secret_key = self._join_keys(b64decode(version_data["serial"]), secret_key)
830 self._secret_key = secret_key