Skip to content

Commit a45daba

Browse files
relativisticelectronKim Neunert
andauthored
Chore: Make data encrytion for services optional (#1918)
* Removing the Singleton * Updated diagram * fix tests * removing simplejsons references * adding tests for encrypted Storages * working that only the encrypted data is removed # Conflicts: # src/cryptoadvance/specter/managers/service_manager.py # src/cryptoadvance/specter/services/service.py # src/cryptoadvance/specter/services/swan/service.py # tests/test_services.py * remove classmethod * Revert "remove classmethod" This reverts commit 7b1859d. * better unlink text * adding check that the correct services are removed from the users Co-authored-by: Kim Neunert <[email protected]>
1 parent 0bd48ab commit a45daba

File tree

7 files changed

+175
-34
lines changed

7 files changed

+175
-34
lines changed

src/cryptoadvance/specter/managers/service_manager.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -328,22 +328,48 @@ def get_service(self, plugin_id: str) -> Service:
328328
raise ExtensionException(f"No such plugin: '{plugin_id}'")
329329
return self._services[plugin_id]
330330

331-
def remove_all_services_from_user(self, user: User):
332-
"""
333-
Clears User.services and `user_secret`; wipes the User's
334-
ServiceEncryptedStorage.
335-
"""
336-
# Don't show any Services on the sidebar for the admin user
337-
user.services.clear()
331+
def delete_service_from_user(self, user: User, service_id: str, autosave=True):
332+
"Removes the service for the user and deletes the stored data in the ServiceEncryptedStorage"
333+
# remove the service from the sidebar
334+
user.remove_service(service_id, autosave=autosave)
335+
# delete the data if it was encrypted
336+
if (
337+
self.user_has_encrypted_storage(user=user)
338+
and self.get_service(service_id).encrypt_data
339+
):
340+
self.specter.service_encrypted_storage_manager.remove_service_data(
341+
user, service_id, autosave=autosave
342+
)
343+
# here we do not need to delete the data if it was unencrypted
338344

339-
# Reset as if we never had any encrypted storage
340-
user.delete_user_secret(autosave=False)
341-
user.save_info()
345+
def delete_services_with_encrypted_storage(self, user: User):
346+
services_with_encrypted_storage = [
347+
service_id
348+
for service_id in self.services
349+
if self.get_service(service_id).encrypt_data
350+
]
351+
for service_id in services_with_encrypted_storage:
352+
self.delete_service_from_user(user, service_id, autosave=True)
353+
354+
user.delete_user_secret(autosave=True)
355+
# Encrypted Service data is now orphaned since there is no
356+
# password. So wipe it from the disk.
357+
self.specter.service_encrypted_storage_manager.delete_all_service_data(user)
358+
logger.debug(
359+
f"Deleted encrypted services {services_with_encrypted_storage} and user secret"
360+
)
361+
362+
def delete_services_with_unencrypted_storage(self, user: User):
363+
services_with_unencrypted_storage = [
364+
service_id
365+
for service_id in self.services
366+
if not self.get_service(service_id).encrypt_data
367+
]
368+
for service_id in services_with_unencrypted_storage:
369+
self.delete_service_from_user(user, service_id, autosave=True)
342370

343-
if self.user_has_encrypted_storage(user=user):
344-
# Encrypted Service data is now orphaned since there is no
345-
# password. So wipe it from the disk.
346-
app.specter.service_encrypted_storage_manager.delete_all_service_data(user)
371+
self.specter.service_unencrypted_storage_manager.delete_all_service_data(user)
372+
logger.debug(f"Deleted unencrypted services")
347373

348374
@classmethod
349375
def get_service_x_dirs(cls, x):

src/cryptoadvance/specter/server_endpoints/settings.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -570,12 +570,11 @@ def auth():
570570
users = None
571571
app.config["LOGIN_DISABLED"] = True
572572

573-
# Cannot support Services if there's no password (admin was already
574-
# warned about this in the UI). Remove User.services, clear the
575-
# `user_secret`, and wipe the ServiceEncryptedStorage.
576-
app.specter.service_manager.remove_all_services_from_user(
577-
current_user
578-
)
573+
# if there is no password, we have to delete the previously encrypted data from services
574+
for user in app.specter.user_manager.users:
575+
app.specter.service_manager.delete_services_with_encrypted_storage(
576+
user
577+
)
579578

580579
# Redirect if a URL was given via the next variable
581580
if request.form.get("next") and request.form.get("next") != "":

src/cryptoadvance/specter/services/service.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class Service:
3838
# If the blueprint gets a "/ext" prefix (isolated_client = True), the login cookie won't work for all specter core functionality
3939
isolated_client = True
4040
devstatus = devstatus_alpha
41+
encrypt_data = False
4142

4243
def __init__(self, active, specter):
4344
if not hasattr(self, "id"):
@@ -47,30 +48,34 @@ def __init__(self, active, specter):
4748
self.active = active
4849
self.specter = specter
4950

51+
@classmethod
52+
def _storage_manager(cls):
53+
return (
54+
app.specter.service_encrypted_storage_manager
55+
if cls.encrypt_data
56+
else app.specter.service_unencrypted_storage_manager
57+
)
58+
5059
def callback(self, callback_id, *argv, **kwargv):
5160
if callback_id == callbacks.after_serverpy_init_app:
5261
if hasattr(self, "callback_after_serverpy_init_app"):
5362
self.callback_after_serverpy_init_app(kwargv["scheduler"])
5463

5564
@classmethod
5665
def set_current_user_service_data(cls, service_data: dict):
57-
app.specter.service_encrypted_storage_manager.set_current_user_service_data(
66+
cls._storage_manager().set_current_user_service_data(
5867
service_id=cls.id, service_data=service_data
5968
)
6069

6170
@classmethod
6271
def update_current_user_service_data(cls, service_data: dict):
63-
app.specter.service_encrypted_storage_manager.update_current_user_service_data(
72+
cls._storage_manager().update_current_user_service_data(
6473
service_id=cls.id, service_data=service_data
6574
)
6675

6776
@classmethod
6877
def get_current_user_service_data(cls) -> dict:
69-
return (
70-
app.specter.service_encrypted_storage_manager.get_current_user_service_data(
71-
service_id=cls.id
72-
)
73-
)
78+
return cls._storage_manager().get_current_user_service_data(service_id=cls.id)
7479

7580
@classmethod
7681
def get_blueprint_name(cls):

src/cryptoadvance/specter/services/service_encrypted_storage.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ def get_service_data(self, service_id: str) -> dict:
9898
service_data = {}
9999
return service_data
100100

101+
def remove_service_data(self, service_id: str, autosave: bool = True):
102+
# Add or update fields; does not remove existing fields
103+
if service_id not in self.data:
104+
logger.warning(f"service_id {service_id} does not exist in self.data")
105+
return
106+
107+
del self.data[service_id]
108+
if autosave:
109+
self._save()
110+
101111

102112
class ServiceUnencryptedStorage(ServiceEncryptedStorage):
103113
"""In order to use ServiceEncryptedStorage but unencrypted, we derive from that class
@@ -178,6 +188,17 @@ def delete_all_service_data(self, user: User):
178188
encrypted_storage.data = {}
179189
encrypted_storage._save()
180190

191+
def remove_service_data(self, user: User, service_id: str, autosave: bool = True):
192+
if user.id in self.storage_by_user:
193+
self.storage_by_user[user].remove_service_data(
194+
service_id, autosave=autosave
195+
)
196+
logger.debug(f"Removed dervice_data from user {user.id}")
197+
else:
198+
logger.debug(
199+
f"Could not remove dervice_data from user {user.id}, because {user.id} was not found in storage_by_user"
200+
)
201+
181202

182203
class ServiceUnencryptedStorageManager(ServiceEncryptedStorageManager):
183204
def __init__(self, data_folder, user_manager: UserManager):

src/cryptoadvance/specter/services/swan/service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class SwanService(Service):
2828
has_blueprint = True
2929
isolated_client = False
3030
devstatus = devstatus_prod
31+
encrypt_data = True
3132

3233
# TODO: As more Services are integrated, we'll want more robust categorization and sorting logic
3334
sort_priority = 1

src/cryptoadvance/specter/templates/settings/auth_settings.jinja

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
<br><br>
2929
{% endif %}
3030
<div id="hasencryptedservicedata" style="display: none" class="hasencryptedservicedata note">
31-
<p>{{ _("Note: If you set Authentication to \"None\", Specter will unlink your Service integrations as a security precaution.") }}</p>
31+
<p>{{ _("Note: If you set Authentication to \"None\", Specter will unlink the Service integrations ") }}
32+
"{{ specter.service_manager.services_sorted | selectattr('encrypt_data') | map(attribute='name') | join('", "') }}"
33+
{{ _(" as a security precaution.") }}</p>
3234
</div>
3335
<div id="ratelimit" class="{% if method == 'none' or not current_user.is_admin %}hidden{% endif %}">
3436
{{ _("Rate Limiting (seconds between login/register attempts)") }}:<br><input id="rate_limit" type="number" name="rate_limit" min="0" max="3600" step="1" value="{{ rate_limit }}" required><br><br>

tests/test_services.py

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,20 @@
2323
from cryptoadvance.specter.user import User, hash_password
2424

2525

26+
class FakeServiceNoEncryption(Service):
27+
# A dummy Service just used by the test suite
28+
id = "test_service_no_encryption"
29+
name = "Test Service no encryption"
30+
has_blueprint = False
31+
encrypt_data = False
32+
33+
2634
class FakeService(Service):
2735
# A dummy Service just used by the test suite
2836
id = "test_service"
2937
name = "Test Service"
3038
has_blueprint = False
39+
encrypt_data = True
3140

3241

3342
# @patch("cryptoadvance.specter.services.service_manager.app")
@@ -158,7 +167,9 @@ def test_access_encrypted_storage_after_login(app_no_node: SpecterFlask):
158167
) == {"somekey": "green"}
159168

160169

161-
def test_remove_all_services_from_user(app_no_node: SpecterFlask, empty_data_folder):
170+
def test_remove_encrypted_services_from_user(
171+
app_no_node: SpecterFlask, empty_data_folder
172+
):
162173
"""ServiceEncryptedStorage should be accessible (decryptable) after user login"""
163174
# Create test users; automatically generates their `user_secret` and kept decrypted
164175
# in memory.
@@ -170,9 +181,8 @@ def test_remove_all_services_from_user(app_no_node: SpecterFlask, empty_data_fol
170181
config={},
171182
)
172183

173-
storage_manager = ServiceEncryptedStorageManager(
174-
user_manager.data_folder, user_manager
175-
)
184+
storage_manager = app_no_node.specter.service_encrypted_storage_manager
185+
service_manager = app_no_node.specter.service_manager
176186
storage_manager.storage_by_user = {}
177187

178188
# Need a simulated request context to enable `current_user` lookup
@@ -199,8 +209,22 @@ def test_remove_all_services_from_user(app_no_node: SpecterFlask, empty_data_fol
199209
# Can't test the actual values because they're encrypted, but the Service.id key is plaintext
200210
assert FakeService.id in data_on_disk
201211

202-
# Now remove all
203-
app_no_node.specter.service_manager.remove_all_services_from_user(user)
212+
# Remove all services that need encryption
213+
# we add the fakeservice to the service_manager.services otherwise delete_services_with_encrypted_storage doesn't know it exists
214+
# strictly speaking the important call is here user.delete_user_secret(autosave=True) which will execute regardless of adding fakeservice
215+
fake_service = FakeService(True, app_no_node.specter)
216+
service_manager.services[fake_service.id] = fake_service
217+
assert fake_service.id in service_manager.services
218+
219+
# also add it to the user, and check later it was remove from the user
220+
user.add_service(fake_service.id)
221+
assert user.has_service(fake_service.id)
222+
223+
app_no_node.specter.service_manager.delete_services_with_encrypted_storage(user)
224+
# the user should not have the fake_service activated any more
225+
assert not user.has_service(fake_service.id)
226+
# the service_manager on the other hand keeps all services, no matter what
227+
assert service_manager.services[fake_service.id]
204228

205229
# Verify data on disk; Bob's user should have his user_secret cleared.
206230
users_file = app_no_node.specter.user_manager.users_file
@@ -230,6 +254,69 @@ def test_remove_all_services_from_user(app_no_node: SpecterFlask, empty_data_fol
230254
assert data_on_disk == {}
231255

232256

257+
def test_check_differences_between_encrypted_and_non_encrypted_services(
258+
app_no_node: SpecterFlask, empty_data_folder
259+
):
260+
"""ServiceEncryptedStorage should be accessible (decryptable) after user login"""
261+
# Create test users; automatically generates their `user_secret` and kept decrypted
262+
# in memory.
263+
user_manager: UserManager = app_no_node.specter.user_manager
264+
user_manager.create_user(
265+
user_id="bob",
266+
username="bob",
267+
plaintext_password="plain_pass_bob",
268+
config={},
269+
)
270+
271+
service_manager = app_no_node.specter.service_manager
272+
user = user_manager.get_user("bob")
273+
274+
def setup_services():
275+
# Remove all services that need encryption
276+
# we add the fakeservice to the service_manager.services otherwise delete_services_with_encrypted_storage doesn't know it exists
277+
# strictly speaking the important call is here user.delete_user_secret(autosave=True) which will execute regardless of adding fakeservice
278+
fake_service = FakeService(True, app_no_node.specter)
279+
fake_service_no_encryption = FakeServiceNoEncryption(True, app_no_node.specter)
280+
service_manager.services[fake_service.id] = fake_service
281+
service_manager.services[
282+
fake_service_no_encryption.id
283+
] = fake_service_no_encryption
284+
assert fake_service.id in service_manager.services
285+
assert fake_service_no_encryption.id in service_manager.services
286+
287+
# also add it to the user, and check later it was remove from the user
288+
user.add_service(fake_service.id)
289+
user.add_service(fake_service_no_encryption.id)
290+
assert user.has_service(fake_service.id)
291+
assert user.has_service(fake_service_no_encryption.id)
292+
293+
return fake_service, fake_service_no_encryption
294+
295+
fake_service, fake_service_no_encryption = setup_services()
296+
# delete the encrypted ones
297+
app_no_node.specter.service_manager.delete_services_with_encrypted_storage(user)
298+
assert not user.has_service(fake_service.id)
299+
assert user.has_service(fake_service_no_encryption.id)
300+
# delete the unencrypted ones
301+
app_no_node.specter.service_manager.delete_services_with_unencrypted_storage(user)
302+
assert not user.has_service(fake_service_no_encryption.id)
303+
304+
# now setup again and check a different order of execution
305+
fake_service, fake_service_no_encryption = setup_services()
306+
# delete the unencrypted ones
307+
app_no_node.specter.service_manager.delete_services_with_unencrypted_storage(user)
308+
assert not user.has_service(fake_service_no_encryption.id)
309+
assert user.has_service(fake_service.id)
310+
# delete the encrypted ones
311+
app_no_node.specter.service_manager.delete_services_with_encrypted_storage(user)
312+
# the user should not have the fake_service activated any more
313+
assert not user.has_service(fake_service.id)
314+
315+
# the service_manager on the other hand keeps all services, no matter what
316+
assert service_manager.services[fake_service.id]
317+
assert service_manager.services[fake_service_no_encryption.id]
318+
319+
233320
def test_ServiceUnEncryptedStorage(empty_data_folder, user1, user2):
234321
user1._generate_user_secret("muh")
235322

0 commit comments

Comments
 (0)