dtPyAppFramework 1.5.2__tar.gz → 2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dtpyappframework-1.5.2/src/dtPyAppFramework.egg-info → dtpyappframework-2.0}/PKG-INFO +3 -3
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/requirements.txt +3 -3
- dtpyappframework-2.0/src/dtPyAppFramework/_version.txt +1 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/__init__.py +1 -1
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/__init__.py +9 -0
- dtpyappframework-2.0/src/dtPyAppFramework/settings/secrets/keystore.py +196 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/local_secret_store.py +22 -30
- dtpyappframework-2.0/src/dtPyAppFramework/settings/settings_reader.py +155 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0/src/dtPyAppFramework.egg-info}/PKG-INFO +3 -3
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework.egg-info/SOURCES.txt +4 -1
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework.egg-info/requires.txt +2 -2
- dtpyappframework-2.0/tests/test_keystore.py +70 -0
- dtpyappframework-2.0/tests/test_settings_reader.py +54 -0
- dtpyappframework-1.5.2/src/dtPyAppFramework/_version.txt +0 -1
- dtpyappframework-1.5.2/src/dtPyAppFramework/settings/settings_reader.py +0 -88
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/LICENCE.txt +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/MANIFEST.in +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/README.md +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/pyproject.toml +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/setup.cfg +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/setup.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/__init__.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/_description.txt +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/_licence.txt +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/_metadata.yaml +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/_name.txt +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/application.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/cloud/__init__.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/cloud/aws.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/cloud/azure.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/cloud/cloud_session.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/decorators/__init__.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/logging/__init__.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/logging/default_logging.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/misc/__init__.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/misc/packaging/__init__.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/misc/yaml/__init__.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/paths/__init__.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/process/__init__.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/process/multiprocessing.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/resources/__init__.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/aws_secret_store.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/azure_secret_store.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/secret_store.py +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework.egg-info/dependency_links.txt +0 -0
- {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dtPyAppFramework
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0
|
|
4
4
|
Summary: A Python library for common features in application development.
|
|
5
5
|
Author-email: Digital-Thought <dev@digital-thought.org>
|
|
6
6
|
Maintainer-email: Digital-Thought <dev@digital-thought.org>
|
|
@@ -54,13 +54,13 @@ Requires-Dist: colorlog
|
|
|
54
54
|
Requires-Dist: psutil
|
|
55
55
|
Requires-Dist: pybase64
|
|
56
56
|
Requires-Dist: boto3
|
|
57
|
-
Requires-Dist: cryptography
|
|
57
|
+
Requires-Dist: cryptography
|
|
58
58
|
Requires-Dist: azure-identity
|
|
59
59
|
Requires-Dist: azure-keyvault-secrets
|
|
60
|
-
Requires-Dist: pykeystore
|
|
61
60
|
Requires-Dist: pytest-mock
|
|
62
61
|
Requires-Dist: pytest-watch
|
|
63
62
|
Requires-Dist: pytest
|
|
63
|
+
Requires-Dist: watchdog
|
|
64
64
|
|
|
65
65
|
# dtPyAppFramework
|
|
66
66
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.0
|
|
@@ -177,7 +177,7 @@ class Settings(dict):
|
|
|
177
177
|
value: Value to set.
|
|
178
178
|
store_name (str, optional): Name of the store for persistent settings.
|
|
179
179
|
"""
|
|
180
|
-
self.secret_manager.
|
|
180
|
+
self.secret_manager.set_persistent_setting(key, value)
|
|
181
181
|
|
|
182
182
|
def __getattr__(self, key):
|
|
183
183
|
"""
|
{dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/__init__.py
RENAMED
|
@@ -153,6 +153,15 @@ class SecretsManager(object):
|
|
|
153
153
|
|
|
154
154
|
return value
|
|
155
155
|
|
|
156
|
+
def set_persistent_setting(self, key, value):
|
|
157
|
+
for store in self.stores:
|
|
158
|
+
if 'User_Local_Store' == store.store_name:
|
|
159
|
+
if store.store_available and not store.store_read_only:
|
|
160
|
+
store.set_persistent_setting(key, value)
|
|
161
|
+
else:
|
|
162
|
+
logging.warning(f'Secrets Store {store.store_name} is either not available or is read only.')
|
|
163
|
+
break
|
|
164
|
+
|
|
156
165
|
def set_secret(self, key, value, store_name='User_Local_Store'):
|
|
157
166
|
"""
|
|
158
167
|
Set a secret in the specified secret store.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
4
|
+
from cryptography.hazmat.primitives.hmac import HMAC
|
|
5
|
+
from cryptography.hazmat.primitives.hashes import SHA256
|
|
6
|
+
from cryptography.hazmat.backends import default_backend
|
|
7
|
+
from cryptography.fernet import Fernet
|
|
8
|
+
import base64
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
class PasswordProtectedKeystoreWithHMAC:
|
|
12
|
+
"""
|
|
13
|
+
PasswordProtectedKeystoreWithHMAC provides a mechanism to securely store key-value pairs
|
|
14
|
+
in a file. The data is protected using encryption and integrity is ensured by HMAC.
|
|
15
|
+
|
|
16
|
+
Methods:
|
|
17
|
+
__init__(keystore_path, password):
|
|
18
|
+
Initializes the keystore with the provided path and password.
|
|
19
|
+
|
|
20
|
+
_derive_key(salt):
|
|
21
|
+
Derives an encryption key from the password and salt using PBKDF2.
|
|
22
|
+
|
|
23
|
+
_generate_hmac(data, key):
|
|
24
|
+
Generates an HMAC for the given data using the derived key.
|
|
25
|
+
|
|
26
|
+
_verify_hmac(data, key, expected_hmac):
|
|
27
|
+
Verifies the HMAC for the given data against the expected HMAC.
|
|
28
|
+
|
|
29
|
+
_load_keystore():
|
|
30
|
+
Loads the keystore from the file, verifying its integrity and returning the stored data.
|
|
31
|
+
|
|
32
|
+
_save_keystore(data):
|
|
33
|
+
Saves the provided data to the keystore file, including the generated HMAC for integrity.
|
|
34
|
+
|
|
35
|
+
set(key, value):
|
|
36
|
+
Adds or updates a key-value pair in the keystore.
|
|
37
|
+
|
|
38
|
+
get(key):
|
|
39
|
+
Retrieves a value from the keystore by its key.
|
|
40
|
+
|
|
41
|
+
delete(key):
|
|
42
|
+
Deletes a key-value pair from the keystore.
|
|
43
|
+
"""
|
|
44
|
+
def __init__(self, keystore_path, password):
|
|
45
|
+
self.keystore_path = keystore_path
|
|
46
|
+
self.password = password.encode()
|
|
47
|
+
|
|
48
|
+
def _derive_key(self, salt):
|
|
49
|
+
"""
|
|
50
|
+
Args:
|
|
51
|
+
salt: A cryptographic salt used for deriving the key, which should be a byte sequence.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A base64-encoded, url-safe byte sequence representing the derived cryptographic key.
|
|
55
|
+
"""
|
|
56
|
+
kdf = PBKDF2HMAC(
|
|
57
|
+
algorithm=SHA256(),
|
|
58
|
+
length=32,
|
|
59
|
+
salt=salt,
|
|
60
|
+
iterations=100_000,
|
|
61
|
+
backend=default_backend()
|
|
62
|
+
)
|
|
63
|
+
return base64.urlsafe_b64encode(kdf.derive(self.password))
|
|
64
|
+
|
|
65
|
+
def _generate_hmac(self, data, key):
|
|
66
|
+
"""
|
|
67
|
+
Args:
|
|
68
|
+
data: The data that needs to be protected with an HMAC.
|
|
69
|
+
key: The cryptographic key used for HMAC generation.
|
|
70
|
+
"""
|
|
71
|
+
hmac = HMAC(key, SHA256(), backend=default_backend())
|
|
72
|
+
hmac.update(data)
|
|
73
|
+
return hmac.finalize()
|
|
74
|
+
|
|
75
|
+
def _verify_hmac(self, data, key, expected_hmac):
|
|
76
|
+
"""
|
|
77
|
+
Args:
|
|
78
|
+
data: The data for which the HMAC is to be verified.
|
|
79
|
+
key: The secret key used for the HMAC generation.
|
|
80
|
+
expected_hmac: The HMAC value that is expected for the provided data and key combination.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
bool: True if the computed HMAC matches the expected HMAC, False otherwise.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
hmac = HMAC(key, SHA256(), backend=default_backend())
|
|
87
|
+
hmac.update(data)
|
|
88
|
+
try:
|
|
89
|
+
hmac.verify(expected_hmac)
|
|
90
|
+
return True
|
|
91
|
+
except:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def _load_keystore(self):
|
|
95
|
+
"""
|
|
96
|
+
Loads and decrypts the keystore data from a specified file path.
|
|
97
|
+
|
|
98
|
+
If the keystore file exists, it reads the file content and extracts the salt,
|
|
99
|
+
encrypted data, and HMAC. The method then derives the key using the extracted
|
|
100
|
+
salt and verifies the HMAC to ensure data integrity. If the HMAC verification
|
|
101
|
+
fails, a ValueError is raised indicating potential file tampering. After successful
|
|
102
|
+
verification, the method decrypts the data using the derived key and
|
|
103
|
+
returns the decrypted JSON content. If the file does not exist, an empty
|
|
104
|
+
dictionary is returned.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If HMAC verification fails, indicating possible tampering.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
dict: The decrypted content of the keystore file as a dictionary, or an
|
|
111
|
+
empty dictionary if the keystore file does not exist.
|
|
112
|
+
"""
|
|
113
|
+
if os.path.exists(self.keystore_path):
|
|
114
|
+
with open(self.keystore_path, 'rb') as file:
|
|
115
|
+
# Read the entire file content
|
|
116
|
+
file_content = file.read()
|
|
117
|
+
|
|
118
|
+
# Extract the salt, encrypted data, and HMAC
|
|
119
|
+
salt = file_content[:16] # First 16 bytes
|
|
120
|
+
encrypted_data = file_content[16:-32] # Middle bytes
|
|
121
|
+
stored_hmac = file_content[-32:] # Last 32 bytes
|
|
122
|
+
|
|
123
|
+
# Derive the key and verify the HMAC
|
|
124
|
+
derived_key = self._derive_key(salt)
|
|
125
|
+
if not self._verify_hmac(salt + encrypted_data, derived_key, stored_hmac):
|
|
126
|
+
raise ValueError("HMAC verification failed: File may have been tampered with.")
|
|
127
|
+
|
|
128
|
+
# Decrypt the data
|
|
129
|
+
cipher_suite = Fernet(derived_key)
|
|
130
|
+
decrypted_data = cipher_suite.decrypt(encrypted_data)
|
|
131
|
+
return json.loads(decrypted_data.decode())
|
|
132
|
+
return {}
|
|
133
|
+
|
|
134
|
+
def _save_keystore(self, data):
|
|
135
|
+
"""
|
|
136
|
+
Args:
|
|
137
|
+
data: The data to be saved in the keystore, which will be encrypted and stored securely.
|
|
138
|
+
"""
|
|
139
|
+
# Generate a random 16-byte salt
|
|
140
|
+
salt = os.urandom(16)
|
|
141
|
+
|
|
142
|
+
# Derive the key
|
|
143
|
+
derived_key = self._derive_key(salt)
|
|
144
|
+
cipher_suite = Fernet(derived_key)
|
|
145
|
+
|
|
146
|
+
# Encrypt the data
|
|
147
|
+
encrypted_data = cipher_suite.encrypt(json.dumps(data).encode())
|
|
148
|
+
|
|
149
|
+
# Generate the HMAC
|
|
150
|
+
hmac = self._generate_hmac(salt + encrypted_data, derived_key)
|
|
151
|
+
|
|
152
|
+
# Write the salt, encrypted data, and HMAC to the file
|
|
153
|
+
with open(self.keystore_path, 'wb') as file:
|
|
154
|
+
file.write(salt)
|
|
155
|
+
file.write(encrypted_data)
|
|
156
|
+
file.write(hmac)
|
|
157
|
+
|
|
158
|
+
def set(self, key, value):
|
|
159
|
+
"""
|
|
160
|
+
Args:
|
|
161
|
+
key: The key to be stored in the keystore.
|
|
162
|
+
value: The value associated with the key to be stored.
|
|
163
|
+
"""
|
|
164
|
+
keystore = self._load_keystore()
|
|
165
|
+
keystore[key] = value
|
|
166
|
+
self._save_keystore(keystore)
|
|
167
|
+
logging.debug(f"Key '{key}' stored successfully.")
|
|
168
|
+
|
|
169
|
+
def get(self, key):
|
|
170
|
+
"""
|
|
171
|
+
Retrieves the value associated with the specified key from the keystore.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
key: The key whose associated value is to be returned.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The value corresponding to the specified key if it exists, otherwise None.
|
|
178
|
+
"""
|
|
179
|
+
keystore = self._load_keystore()
|
|
180
|
+
logging.debug(f"Retrieving Key '{key}'")
|
|
181
|
+
return keystore.get(key)
|
|
182
|
+
|
|
183
|
+
def delete(self, key):
|
|
184
|
+
"""
|
|
185
|
+
Deletes the specified key from the keystore if it exists.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
key: The key to be deleted from the keystore.
|
|
189
|
+
"""
|
|
190
|
+
keystore = self._load_keystore()
|
|
191
|
+
if key in keystore:
|
|
192
|
+
del keystore[key]
|
|
193
|
+
self._save_keystore(keystore)
|
|
194
|
+
logging.debug(f"Key '{key}' deleted successfully.")
|
|
195
|
+
else:
|
|
196
|
+
logging.debug(f"Key '{key}' not found in the keystore.")
|
|
@@ -8,7 +8,9 @@ import pybase64
|
|
|
8
8
|
from .secret_store import AbstractSecretStore, SecretsStoreException
|
|
9
9
|
from itertools import cycle
|
|
10
10
|
from ...misc import run_cmd
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
from .keystore import PasswordProtectedKeystoreWithHMAC
|
|
13
|
+
|
|
12
14
|
from base64 import urlsafe_b64encode
|
|
13
15
|
|
|
14
16
|
|
|
@@ -48,42 +50,23 @@ class LocalSecretStore(AbstractSecretStore):
|
|
|
48
50
|
password (str): Password for the local secret store (default: None).
|
|
49
51
|
"""
|
|
50
52
|
super().__init__(store_name, 'local', store_priority, application_settings)
|
|
51
|
-
|
|
53
|
+
if os.path.exists(os.path.join(root_store_path, f"{app_short_name}.keystore")):
|
|
54
|
+
logging.warning(f'Old Keystore file "{os.path.join(root_store_path, f"{app_short_name}.keystore")}" is no longer supported.')
|
|
55
|
+
self.store_path = os.path.join(root_store_path, f"{app_short_name}.v2keystore")
|
|
52
56
|
|
|
53
57
|
# If password is not provided, generate a unique password
|
|
54
58
|
if password is None:
|
|
55
59
|
password = self.__guid()
|
|
56
60
|
|
|
57
61
|
try:
|
|
58
|
-
# If the store does not exist, initialize it
|
|
59
|
-
if not os.path.exists(self.store_path):
|
|
60
|
-
self.store = self.__initialise_secrets_store(password)
|
|
61
|
-
|
|
62
62
|
# Try to load the existing store
|
|
63
|
-
self.store =
|
|
63
|
+
self.store = PasswordProtectedKeystoreWithHMAC(self.store_path, password)
|
|
64
64
|
self.store_available = True
|
|
65
65
|
self.store_read_only = not self.__is_writeable()
|
|
66
66
|
logging.info(f'Successfully opened Secrets Store: {self.store_path}')
|
|
67
67
|
except Exception as ex:
|
|
68
68
|
raise SecretsStoreException(f'Failed to open Secrets Store: {self.store_path}. Error: {str(ex)}')
|
|
69
69
|
|
|
70
|
-
def __initialise_secrets_store(self, password):
|
|
71
|
-
"""
|
|
72
|
-
Initialize the secrets store if it doesn't exist.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
password (str): Password for the local secret store.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
Initialized secrets store.
|
|
79
|
-
"""
|
|
80
|
-
try:
|
|
81
|
-
store = pykeystore.KeyStoreEx.create(self.store_path, password)
|
|
82
|
-
return store
|
|
83
|
-
except Exception as ex:
|
|
84
|
-
logging.error(f'Failed to create Secrets Store. Error: {str(ex)}')
|
|
85
|
-
raise ex
|
|
86
|
-
|
|
87
70
|
def __guid(self):
|
|
88
71
|
"""
|
|
89
72
|
Generate a unique identifier based on the machine and store path.
|
|
@@ -115,7 +98,7 @@ class LocalSecretStore(AbstractSecretStore):
|
|
|
115
98
|
base += self.store_path
|
|
116
99
|
key = re.sub("[^a-zA-Z]+", "", base)
|
|
117
100
|
xored = ''.join(chr(ord(x) ^ ord(y)) for (x, y) in zip(base, cycle(key)))
|
|
118
|
-
return urlsafe_b64encode(pybase64.b64encode_as_string(xored.encode())[:32].encode())
|
|
101
|
+
return urlsafe_b64encode(pybase64.b64encode_as_string(xored.encode())[:32].encode()).decode()
|
|
119
102
|
|
|
120
103
|
def get_secret(self, key, default_value=None):
|
|
121
104
|
"""
|
|
@@ -128,12 +111,20 @@ class LocalSecretStore(AbstractSecretStore):
|
|
|
128
111
|
Returns:
|
|
129
112
|
Secret value if found, else default_value.
|
|
130
113
|
"""
|
|
131
|
-
entry = self.store.
|
|
114
|
+
entry = self.store.get(key=key)
|
|
132
115
|
if not entry or entry == 'NONE':
|
|
133
116
|
return default_value
|
|
134
117
|
|
|
135
118
|
return entry
|
|
136
119
|
|
|
120
|
+
def set_persistent_setting(self, key, value):
|
|
121
|
+
if self.get_secret(key):
|
|
122
|
+
self.delete_secret(key)
|
|
123
|
+
|
|
124
|
+
self.store.set(key=key, value=value)
|
|
125
|
+
self.__save()
|
|
126
|
+
|
|
127
|
+
|
|
137
128
|
def set_secret(self, key, value):
|
|
138
129
|
"""
|
|
139
130
|
Set a secret in the local secret store.
|
|
@@ -145,7 +136,7 @@ class LocalSecretStore(AbstractSecretStore):
|
|
|
145
136
|
if self.get_secret(key):
|
|
146
137
|
self.delete_secret(key)
|
|
147
138
|
|
|
148
|
-
self.store.
|
|
139
|
+
self.store.set(key=key, value=value)
|
|
149
140
|
self.__save()
|
|
150
141
|
index = self.get_index()
|
|
151
142
|
if key not in index:
|
|
@@ -159,7 +150,7 @@ class LocalSecretStore(AbstractSecretStore):
|
|
|
159
150
|
Args:
|
|
160
151
|
key (str): Key of the secret.
|
|
161
152
|
"""
|
|
162
|
-
|
|
153
|
+
self.store.delete(key=key)
|
|
163
154
|
self.__save()
|
|
164
155
|
index = self.get_index()
|
|
165
156
|
while key in index:
|
|
@@ -168,7 +159,7 @@ class LocalSecretStore(AbstractSecretStore):
|
|
|
168
159
|
|
|
169
160
|
def __set_index(self, index: list):
|
|
170
161
|
logging.info(index)
|
|
171
|
-
self.store.
|
|
162
|
+
self.store.set(key=f'{self.store_name}.INDEX', value=json.dumps(index))
|
|
172
163
|
self.__save()
|
|
173
164
|
|
|
174
165
|
def get_index(self) -> list:
|
|
@@ -181,7 +172,8 @@ class LocalSecretStore(AbstractSecretStore):
|
|
|
181
172
|
|
|
182
173
|
def __save(self):
|
|
183
174
|
"""Save the changes made to the local secret store."""
|
|
184
|
-
self.store.
|
|
175
|
+
self.store.set('sstore_save', 'true')
|
|
176
|
+
self.store.delete('sstore_save')
|
|
185
177
|
|
|
186
178
|
def __is_writeable(self):
|
|
187
179
|
try:
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from watchdog.observers import Observer
|
|
2
|
+
from watchdog.events import FileSystemEventHandler, FileSystemEvent
|
|
3
|
+
import yaml
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import hashlib
|
|
7
|
+
|
|
8
|
+
class ConfigFileWatcher(FileSystemEventHandler):
|
|
9
|
+
|
|
10
|
+
def __init__(self, change_action, delete_action, watch_file, watch_folder):
|
|
11
|
+
self.change_action = change_action
|
|
12
|
+
self.delete_action = delete_action
|
|
13
|
+
self.watch_file = watch_file
|
|
14
|
+
self.watch_folder = watch_folder
|
|
15
|
+
self.watch_file_sha256 = self.calculate_sha256(os.path.join(self.watch_folder, self.watch_file))
|
|
16
|
+
|
|
17
|
+
def calculate_sha256(self, file_path):
|
|
18
|
+
sha256_hash = hashlib.sha256()
|
|
19
|
+
try:
|
|
20
|
+
with open(file_path, "rb") as f:
|
|
21
|
+
# Read and update hash in chunks of 4K
|
|
22
|
+
for byte_block in iter(lambda: f.read(4096), b""):
|
|
23
|
+
sha256_hash.update(byte_block)
|
|
24
|
+
return sha256_hash.hexdigest()
|
|
25
|
+
except FileNotFoundError:
|
|
26
|
+
return "File not found."
|
|
27
|
+
except Exception as e:
|
|
28
|
+
return f"An error occurred: {str(e)}"
|
|
29
|
+
|
|
30
|
+
def process(self, event: FileSystemEvent):
|
|
31
|
+
"""
|
|
32
|
+
event.event_type
|
|
33
|
+
'modified' | 'created' | 'moved' | 'deleted'
|
|
34
|
+
event.src_path
|
|
35
|
+
path to the modified file
|
|
36
|
+
"""
|
|
37
|
+
if event.src_path.endswith(self.watch_file):
|
|
38
|
+
if event.event_type == 'deleted':
|
|
39
|
+
logging.warning(f'Config Watch File Deleted: {event.src_path}')
|
|
40
|
+
self.delete_action()
|
|
41
|
+
elif event.event_type == 'modified':
|
|
42
|
+
if self.calculate_sha256(event.src_path) != self.watch_file_sha256:
|
|
43
|
+
logging.warning(f'Config Watch File Changed: {event.src_path}')
|
|
44
|
+
self.watch_file_sha256 = self.calculate_sha256(event.src_path)
|
|
45
|
+
self.change_action()
|
|
46
|
+
elif event.event_type == 'created':
|
|
47
|
+
logging.warning(f'Config Watch File Created: {event.src_path}')
|
|
48
|
+
self.watch_file_sha256 = self.calculate_sha256(event.src_path)
|
|
49
|
+
self.change_action()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def on_deleted(self, event: FileSystemEvent) -> None:
|
|
53
|
+
self.process(event)
|
|
54
|
+
|
|
55
|
+
def on_modified(self, event: FileSystemEvent):
|
|
56
|
+
self.process(event)
|
|
57
|
+
|
|
58
|
+
def on_created(self, event: FileSystemEvent):
|
|
59
|
+
self.process(event)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SettingsReader(dict):
|
|
64
|
+
"""
|
|
65
|
+
A class for reading settings from a YAML file and accessing them using dot notation.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
path (str): Path to the directory containing the settings YAML file.
|
|
69
|
+
priority (int): Priority of the settings reader.
|
|
70
|
+
settings_file (str): Full path to the settings YAML file.
|
|
71
|
+
"""
|
|
72
|
+
CONFIG_FILE = "config.yaml"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def __init__(self, path: str, priority: int) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Initialize the SettingsReader.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
path (str): Path to the directory containing the settings YAML file.
|
|
81
|
+
priority (int): Priority of the settings reader.
|
|
82
|
+
"""
|
|
83
|
+
self.priority = priority
|
|
84
|
+
self.settings_file = os.path.join(path, self.CONFIG_FILE)
|
|
85
|
+
|
|
86
|
+
self.load_yaml_file()
|
|
87
|
+
self.observer = Observer()
|
|
88
|
+
event_handler = ConfigFileWatcher(change_action=self.load_yaml_file, delete_action=super().clear,
|
|
89
|
+
watch_file=self.CONFIG_FILE, watch_folder=path)
|
|
90
|
+
self.observer.schedule(event_handler, path, recursive=False)
|
|
91
|
+
self.observer.start()
|
|
92
|
+
|
|
93
|
+
super().__init__()
|
|
94
|
+
|
|
95
|
+
def load_yaml_file(self):
|
|
96
|
+
"""
|
|
97
|
+
Load settings from the YAML file and update the dictionary.
|
|
98
|
+
"""
|
|
99
|
+
if os.path.exists(self.settings_file):
|
|
100
|
+
try:
|
|
101
|
+
with open(self.settings_file, 'r', encoding='UTF-8') as file:
|
|
102
|
+
super().clear()
|
|
103
|
+
self.update(yaml.safe_load(file))
|
|
104
|
+
logging.info(f'Loaded settings file {self.settings_file}.')
|
|
105
|
+
except Exception as ex:
|
|
106
|
+
logging.error(f'Error reading in settings file {self.settings_file}. {str(ex)}')
|
|
107
|
+
print(f'Error reading in settings file {self.settings_file}. {str(ex)}')
|
|
108
|
+
else:
|
|
109
|
+
logging.warning(f'Settings file "{self.settings_file}" does not exist.')
|
|
110
|
+
|
|
111
|
+
def clear(self) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Clear method not implemented.
|
|
114
|
+
"""
|
|
115
|
+
raise NotImplementedError
|
|
116
|
+
|
|
117
|
+
def popitem(self):
|
|
118
|
+
"""
|
|
119
|
+
popitem method not implemented.
|
|
120
|
+
"""
|
|
121
|
+
raise NotImplementedError
|
|
122
|
+
|
|
123
|
+
def __setitem__(self, k, v) -> None:
|
|
124
|
+
"""
|
|
125
|
+
__setitem__ method not implemented.
|
|
126
|
+
"""
|
|
127
|
+
raise NotImplementedError
|
|
128
|
+
|
|
129
|
+
def pop(self, __key):
|
|
130
|
+
"""
|
|
131
|
+
pop method not implemented.
|
|
132
|
+
"""
|
|
133
|
+
raise NotImplementedError
|
|
134
|
+
|
|
135
|
+
def __getitem__(self, key):
|
|
136
|
+
"""
|
|
137
|
+
Get item from the dictionary using dot notation.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
key (str): Key in dot notation.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The value associated with the key.
|
|
144
|
+
"""
|
|
145
|
+
keys = key.split('.')
|
|
146
|
+
if len(keys) == 1:
|
|
147
|
+
return dict.__getitem__(self, key)
|
|
148
|
+
else:
|
|
149
|
+
data = self.copy()
|
|
150
|
+
for key in keys:
|
|
151
|
+
if key in data:
|
|
152
|
+
data = data[key]
|
|
153
|
+
else:
|
|
154
|
+
return None
|
|
155
|
+
return data
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dtPyAppFramework
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0
|
|
4
4
|
Summary: A Python library for common features in application development.
|
|
5
5
|
Author-email: Digital-Thought <dev@digital-thought.org>
|
|
6
6
|
Maintainer-email: Digital-Thought <dev@digital-thought.org>
|
|
@@ -54,13 +54,13 @@ Requires-Dist: colorlog
|
|
|
54
54
|
Requires-Dist: psutil
|
|
55
55
|
Requires-Dist: pybase64
|
|
56
56
|
Requires-Dist: boto3
|
|
57
|
-
Requires-Dist: cryptography
|
|
57
|
+
Requires-Dist: cryptography
|
|
58
58
|
Requires-Dist: azure-identity
|
|
59
59
|
Requires-Dist: azure-keyvault-secrets
|
|
60
|
-
Requires-Dist: pykeystore
|
|
61
60
|
Requires-Dist: pytest-mock
|
|
62
61
|
Requires-Dist: pytest-watch
|
|
63
62
|
Requires-Dist: pytest
|
|
63
|
+
Requires-Dist: watchdog
|
|
64
64
|
|
|
65
65
|
# dtPyAppFramework
|
|
66
66
|
|
|
@@ -35,5 +35,8 @@ src/dtPyAppFramework/settings/settings_reader.py
|
|
|
35
35
|
src/dtPyAppFramework/settings/secrets/__init__.py
|
|
36
36
|
src/dtPyAppFramework/settings/secrets/aws_secret_store.py
|
|
37
37
|
src/dtPyAppFramework/settings/secrets/azure_secret_store.py
|
|
38
|
+
src/dtPyAppFramework/settings/secrets/keystore.py
|
|
38
39
|
src/dtPyAppFramework/settings/secrets/local_secret_store.py
|
|
39
|
-
src/dtPyAppFramework/settings/secrets/secret_store.py
|
|
40
|
+
src/dtPyAppFramework/settings/secrets/secret_store.py
|
|
41
|
+
tests/test_keystore.py
|
|
42
|
+
tests/test_settings_reader.py
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from cryptography.fernet import Fernet
|
|
8
|
+
from cryptography.hazmat.backends import default_backend
|
|
9
|
+
from cryptography.hazmat.primitives.hashes import SHA256
|
|
10
|
+
from cryptography.hazmat.primitives.hmac import HMAC
|
|
11
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
12
|
+
# Import the class to test
|
|
13
|
+
from dtPyAppFramework.settings.secrets.keystore import PasswordProtectedKeystoreWithHMAC
|
|
14
|
+
|
|
15
|
+
# The path to a temporary file for testing purposes
|
|
16
|
+
TEMP_FILE_PATH = "/tmp/test_keystore.db"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def teardown_function(function):
|
|
20
|
+
if os.path.exists(TEMP_FILE_PATH):
|
|
21
|
+
os.remove(TEMP_FILE_PATH)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def keystore():
|
|
26
|
+
return PasswordProtectedKeystoreWithHMAC(TEMP_FILE_PATH, 'test_password')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_init(keystore):
|
|
30
|
+
assert keystore.keystore_path == TEMP_FILE_PATH
|
|
31
|
+
assert keystore.password == 'test_password'.encode()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_derive_key(keystore):
|
|
35
|
+
salt = os.urandom(16)
|
|
36
|
+
key = keystore._derive_key(salt)
|
|
37
|
+
assert (isinstance(key, bytes))
|
|
38
|
+
assert (len(key) == 44) # Length of base64 encoded 256-bit key
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_generate_hmac(keystore):
|
|
42
|
+
salt = os.urandom(16)
|
|
43
|
+
key = keystore._derive_key(salt)
|
|
44
|
+
data = os.urandom(16)
|
|
45
|
+
hmac = keystore._generate_hmac(data, key)
|
|
46
|
+
assert (isinstance(hmac, bytes))
|
|
47
|
+
assert (len(hmac) == 32) # Length of SHA256 hash
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_verify_hmac(keystore):
|
|
51
|
+
salt = os.urandom(16)
|
|
52
|
+
key = keystore._derive_key(salt)
|
|
53
|
+
data = os.urandom(16)
|
|
54
|
+
hmac = keystore._generate_hmac(data, key)
|
|
55
|
+
assert keystore._verify_hmac(data, key, hmac) is True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_save_and_load_keystore(keystore):
|
|
59
|
+
test_data = {"test_key": "test_value"}
|
|
60
|
+
keystore._save_keystore(test_data)
|
|
61
|
+
loaded_data = keystore._load_keystore()
|
|
62
|
+
|
|
63
|
+
assert loaded_data == test_data
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_set_get_delete_methods(keystore):
|
|
67
|
+
keystore.set("test_key", "test_value")
|
|
68
|
+
assert keystore.get("test_key") == "test_value"
|
|
69
|
+
keystore.delete("test_key")
|
|
70
|
+
assert keystore.get("test_key") is None
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from dtPyAppFramework.settings.settings_reader import SettingsReader
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Test that __init__ sets the attributes correctly
|
|
9
|
+
def test_init():
|
|
10
|
+
settings_reader = SettingsReader('/path/to/settings', 5)
|
|
11
|
+
assert settings_reader.priority == 5
|
|
12
|
+
assert settings_reader.settings_file == '/path/to/settings/config.yaml'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Test that load_yaml_file loads a YAML file correctly
|
|
16
|
+
@mock.patch('yaml.safe_load', return_value={'key': 'value'})
|
|
17
|
+
def test_load_yaml_file(mock_safe_load):
|
|
18
|
+
settings_reader = SettingsReader('/path/to/settings', 5)
|
|
19
|
+
settings_reader.load_yaml_file()
|
|
20
|
+
assert settings_reader.get('key') == 'value'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Test that __getitem__ can retrieve data correctly using dot notation
|
|
24
|
+
def test_getitem():
|
|
25
|
+
settings_reader = SettingsReader('/path/to/settings', 5)
|
|
26
|
+
|
|
27
|
+
data = {
|
|
28
|
+
'key': 'value',
|
|
29
|
+
'nested': {
|
|
30
|
+
'key': 'nested value'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
settings_reader.update(data)
|
|
35
|
+
|
|
36
|
+
assert settings_reader.__getitem__('nested.key') == 'nested value'
|
|
37
|
+
assert settings_reader.__getitem__('key') == 'value'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Test that methods that are not implemented raise a NotImplementedError
|
|
41
|
+
def test_not_implemented_methods():
|
|
42
|
+
settings_reader = SettingsReader('/path/to/settings', 5)
|
|
43
|
+
|
|
44
|
+
with pytest.raises(NotImplementedError):
|
|
45
|
+
settings_reader.clear()
|
|
46
|
+
|
|
47
|
+
with pytest.raises(NotImplementedError):
|
|
48
|
+
settings_reader.popitem()
|
|
49
|
+
|
|
50
|
+
with pytest.raises(NotImplementedError):
|
|
51
|
+
settings_reader.__setitem__('key', 'value')
|
|
52
|
+
|
|
53
|
+
with pytest.raises(NotImplementedError):
|
|
54
|
+
settings_reader.pop('key')
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.5.2
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import yaml
|
|
3
|
-
import logging
|
|
4
|
-
import os
|
|
5
|
-
|
|
6
|
-
class SettingsReader(dict):
|
|
7
|
-
"""
|
|
8
|
-
A class for reading settings from a YAML file and accessing them using dot notation.
|
|
9
|
-
|
|
10
|
-
Attributes:
|
|
11
|
-
path (str): Path to the directory containing the settings YAML file.
|
|
12
|
-
priority (int): Priority of the settings reader.
|
|
13
|
-
settings_file (str): Full path to the settings YAML file.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
def __init__(self, path: str, priority: int) -> None:
|
|
17
|
-
"""
|
|
18
|
-
Initialize the SettingsReader.
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
path (str): Path to the directory containing the settings YAML file.
|
|
22
|
-
priority (int): Priority of the settings reader.
|
|
23
|
-
"""
|
|
24
|
-
self.priority = priority
|
|
25
|
-
self.settings_file = os.path.join(path, "config.yaml")
|
|
26
|
-
|
|
27
|
-
if os.path.exists(self.settings_file):
|
|
28
|
-
self.load_yaml_file()
|
|
29
|
-
|
|
30
|
-
super().__init__()
|
|
31
|
-
|
|
32
|
-
def load_yaml_file(self):
|
|
33
|
-
"""
|
|
34
|
-
Load settings from the YAML file and update the dictionary.
|
|
35
|
-
"""
|
|
36
|
-
try:
|
|
37
|
-
with open(self.settings_file, 'r', encoding='UTF-8') as file:
|
|
38
|
-
self.update(yaml.safe_load(file))
|
|
39
|
-
except Exception as ex:
|
|
40
|
-
logging.error(f'Error reading in settings file {self.settings_file}. {str(ex)}')
|
|
41
|
-
print(f'Error reading in settings file {self.settings_file}. {str(ex)}')
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def clear(self) -> None:
|
|
45
|
-
"""
|
|
46
|
-
Clear method not implemented.
|
|
47
|
-
"""
|
|
48
|
-
raise NotImplementedError
|
|
49
|
-
|
|
50
|
-
def popitem(self):
|
|
51
|
-
"""
|
|
52
|
-
popitem method not implemented.
|
|
53
|
-
"""
|
|
54
|
-
raise NotImplementedError
|
|
55
|
-
|
|
56
|
-
def __setitem__(self, k, v) -> None:
|
|
57
|
-
"""
|
|
58
|
-
__setitem__ method not implemented.
|
|
59
|
-
"""
|
|
60
|
-
raise NotImplementedError
|
|
61
|
-
|
|
62
|
-
def pop(self, __key):
|
|
63
|
-
"""
|
|
64
|
-
pop method not implemented.
|
|
65
|
-
"""
|
|
66
|
-
raise NotImplementedError
|
|
67
|
-
|
|
68
|
-
def __getitem__(self, key):
|
|
69
|
-
"""
|
|
70
|
-
Get item from the dictionary using dot notation.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
key (str): Key in dot notation.
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
The value associated with the key.
|
|
77
|
-
"""
|
|
78
|
-
keys = key.split('.')
|
|
79
|
-
if len(keys) == 1:
|
|
80
|
-
return dict.__getitem__(self, key)
|
|
81
|
-
else:
|
|
82
|
-
data = self.copy()
|
|
83
|
-
for key in keys:
|
|
84
|
-
if key in data:
|
|
85
|
-
data = data[key]
|
|
86
|
-
else:
|
|
87
|
-
return None
|
|
88
|
-
return data
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/logging/default_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
{dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/misc/packaging/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/process/multiprocessing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|