Implementation of encryption for sensitive information that may be providet by the client, to ensure that all information important or not is safe

This commit is contained in:
2026-06-26 23:39:05 +02:00
parent 925f07e96f
commit 19a585b2ec
2 changed files with 151 additions and 79 deletions
+142 -72
View File
@@ -18,9 +18,11 @@ Collection Structure:
- Status fields: slots_used_by - Status fields: slots_used_by
""" """
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
import Web.modules.inventarsystem.data_protection as dp
from Web.modules.database.settings import MongoClient from Web.modules.database.settings import MongoClient
from bson.objectid import ObjectId from bson.objectid import ObjectId
import datetime import datetime
import ast
def _get_tenant_db(client): def _get_tenant_db(client):
@@ -37,8 +39,47 @@ def _active_record_query(extra_query=None):
base_query.update(extra_query) base_query.update(extra_query)
return base_query return base_query
def _decrypt_appointment(item):
"""Helper function to safely decrypt appointment fields back to their original types."""
if not item:
return item
try:
if 'user' in item and item['user']:
item['user'] = dp.decrypt_text(item['user'])
if 'note' in item and item['note']:
item['note'] = dp.decrypt_text(item['note'])
if 'title' in item and item['title']:
item['title'] = dp.decrypt_text(item['title'])
if 'mail' in item and item['mail']:
decrypted_mail = dp.decrypt_text(item['mail'])
try:
item['mail'] = ast.literal_eval(decrypted_mail)
except Exception:
item['mail'] = decrypted_mail
if 'custom_fields' in item and item['custom_fields']:
item['custom_fields'] = [dp.decrypt_text(field) for field in item['custom_fields']]
if 'slots_booked' in item and item['slots_booked']:
# If it's a string, it was encrypted during an update execution
if isinstance(item['slots_booked'], str):
decrypted_slots = dp.decrypt_text(item['slots_booked'])
try:
item['slots_booked'] = ast.literal_eval(decrypted_slots)
except Exception:
item['slots_booked'] = decrypted_slots
except Exception as e:
print(f"Error during decryption: {e}")
return item
def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False, title: str="", custom_fields: list = (), clients_p_slot: int=1): def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False, title: str="", custom_fields: list = (), clients_p_slot: int=1):
client = None
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
@@ -50,62 +91,53 @@ def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
'time_span': time_span, 'time_span': time_span,
'slots': slots, 'slots': slots,
'slot_lenght': slot_lenght, 'slot_lenght': slot_lenght,
'user': user, 'user': dp.encrypt_text(user.strip()),
'mail': mail, 'mail': dp.encrypt_text(str(mail)),
'note': note, 'note': dp.encrypt_text(note),
'title': title, 'title': dp.encrypt_text(title),
'custom_fields': custom_fields, 'custom_fields': [dp.encrypt_text(str(field)) for field in custom_fields],
'calendar_enabled': bool(calendar_enabled), 'calendar_enabled': bool(calendar_enabled),
'clients_per_slot': clients_p_slot, 'clients_per_slot': clients_p_slot,
'slots_booked': [], # -> [(start_time, (names),(custom1, custom2,...)), ...]the list gets there indexes as the slot 1-defined so is can be counted without an extra variable 'slots_booked': [], # -> [(start_time, (names),(custom1, custom2,...)), ...]the list gets there indexes as the slot 1-defined so is can be counted without an extra variable
'Created': datetime.datetime.now(), 'Created': datetime.datetime.now(),
'LastUpdated': datetime.datetime.now() 'LastUpdated': datetime.datetime.now()
} }
result = items.insert_one(item) result = items.insert_one(item)
return result.inserted_id return result.inserted_id
except Exception as e: except Exception as e:
print(f"Exception accured: {e}") print(f"Exception occurred in add: {e}")
return None
finally:
if client:
client.close()
def get_item(id): def get_item(id):
""" """Retrieve a specific appointment by its ID and decrypt it."""
Retrieve a specific appointment by its ID. client = None
Args:
id (str): ID of the appointsment to retrieve
Returns:
dict: The appointment document or None if not found
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
items = db['appointments'] items = db['appointments']
item = items.find_one(_active_record_query({'_id': ObjectId(id)})) item = items.find_one(_active_record_query({'_id': ObjectId(id)}))
client.close()
return item return _decrypt_appointment(item)
except Exception as e: except Exception as e:
print(f"Error retrieving item: {e}") print(f"Error retrieving item: {e}")
return None return None
finally:
if client:
client.close()
def update(id,slots_used: list): def update(id, slots_used: list):
""" """Update an existing appointment's booked slots securely."""
Update an existing appointment. client = None
Args:
id (str): ID of the item to update
Returns:
bool: True if successful, False otherwise
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
items = db['appointments'] items = db['appointments']
update_data = { update_data = {
'slots_booked': slots_used, 'slots_booked': dp.encrypt_text(str(slots_used)),
'LastUpdated': datetime.datetime.now() 'LastUpdated': datetime.datetime.now()
} }
@@ -114,53 +146,81 @@ def update(id,slots_used: list):
{'$set': update_data} {'$set': update_data}
) )
client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
print(f"Error updating item: {e}") print(f"Error updating item: {e}")
return False return False
finally:
if client:
client.close()
def remove_slot(id, date_start_time, name): def remove_slot(id, date_start_time, name):
""" """
Remove a booked slot from an appointment's `slots_booked`. Remove a booked slot from an appointment's encrypted `slots_booked` list.
Args: Because the array is stored as an encrypted string blob, we must decrypt,
id (str): Appointment ID modify it in Python, and re-encrypt it.
date_start_time: The start time value used when booking
name (str): Name associated with the booking
Returns:
bool: True if a slot was removed, False otherwise
""" """
client = None
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
items = db['appointments'] items = db['appointments']
# Attempt to pull the exact element (stored as an array/tuple) item = items.find_one({'_id': ObjectId(id)})
if not item or 'slots_booked' not in item or not item['slots_booked']:
return False
try:
decrypted_slots = dp.decrypt_text(item['slots_booked'])
slots_list = ast.literal_eval(decrypted_slots)
except Exception as e:
print(f"Failed to decrypt or parse slots: {e}")
return False
# Structure format note: [(start_time, (names), (custom1, custom2...)), ...]
updated_slots = []
removed_any = False
for slot in slots_list:
slot_start = slot[0]
slot_names = slot[1]
if slot_start == date_start_time and (slot_names == name or name in slot_names):
removed_any = True
continue
updated_slots.append(slot)
if not removed_any:
return False
result = items.update_one( result = items.update_one(
{'_id': ObjectId(id)}, {'_id': ObjectId(id)},
{'$pull': {'slots_booked': [date_start_time, name]}} {
'$set': {
'slots_booked': dp.encrypt_text(str(updated_slots)),
'LastUpdated': datetime.datetime.now()
}
}
) )
client.close()
return result.modified_count > 0 return result.modified_count > 0
except Exception as e: except Exception as e:
print(f"Error removing slot: {e}") print(f"Error removing slot: {e}")
return False return False
finally:
if client:
client.close()
def remove(id): def remove(id):
""" """
Soft-delete an appointment by setting its `Deleted` flag. Hard-delete an appointment plan by its ID.
(Note: If your docstring mentions a soft-delete 'Deleted' flag,
Args: change items.delete_one to items.update_one with {'$set': {'Deleted': True}})
id (str): Appointment ID
Returns:
bool: True if the appointment was marked deleted, False otherwise
""" """
client = None
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
@@ -168,54 +228,64 @@ def remove(id):
result = items.delete_one({'_id': ObjectId(id)}) result = items.delete_one({'_id': ObjectId(id)})
client.close()
return result.deleted_count > 0 return result.deleted_count > 0
except Exception as e: except Exception as e:
print(f"Error removing appointment: {e}") print(f"Error removing appointment: {e}")
return False return False
finally:
if client:
client.close()
def remove_done(): def remove_done():
"""removose already finisched appointments""" """Remove all expired appointments whose end date is prior to today in a single call."""
client = None
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
items = db['appointments'] items = db['appointments']
today = datetime.date.today().strftime('%Y-%m-%d') today = datetime.date.today().strftime('%Y-%m-%d')
removed_count = 0
cursor = items.find( result = items.delete_many(
_active_record_query( _active_record_query(
{ {
'date_end': {'$lt': today}, 'date_end': {'$lt': today},
} }
) )
).sort('date_start', 1) )
for item in cursor: return result.deleted_count > 0
item['_id'] = str(item.get('_id'))
result = items.delete_one({'_id': ObjectId(item['_id'])})
removed_count += result.deleted_count
client.close()
return removed_count > 0
except Exception as e: except Exception as e:
print(f"Error removing appointment: {e}") print(f"Error cleaning up finished appointments: {e}")
return False return False
finally:
if client:
client.close()
def get_upcoming_for_user(user: str, limit: int = 25): def get_upcoming_for_user(user: str, limit: int = 25):
"""Return upcoming appointment plans for a user ordered by start date.""" """Return upcoming appointment plans for a user, matching by encrypted username."""
remove_done() try:
if hasattr(globals(), 'remove_done'):
remove_done()
except Exception:
pass
client = None
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client) db = _get_tenant_db(client)
items = db['appointments'] items = db['appointments']
today = datetime.date.today().strftime('%Y-%m-%d') today = datetime.date.today().strftime('%Y-%m-%d')
encrypted_user = dp.encrypt_text(str(user or '').strip())
cursor = items.find( cursor = items.find(
_active_record_query( _active_record_query(
{ {
'user': str(user or '').strip(), 'user': encrypted_user,
'date_end': {'$gte': today}, 'date_end': {'$gte': today},
} }
) )
@@ -224,14 +294,14 @@ def get_upcoming_for_user(user: str, limit: int = 25):
results = [] results = []
for item in cursor: for item in cursor:
item['_id'] = str(item.get('_id')) item['_id'] = str(item.get('_id'))
results.append(item) results.append(_decrypt_appointment(item))
if len(results) >= max(1, int(limit)): if len(results) >= max(1, int(limit)):
break break
client.close()
return results return results
except Exception as e: except Exception as e:
print(f"Error retrieving upcoming appointments: {e}") print(f"Error retrieving upcoming appointments: {e}")
return [] return []
finally:
if client:
client.close()
+9 -7
View File
@@ -8,6 +8,7 @@ import Web.modules.emailservice.email as mail_service
import Web.modules.database.termine as termin import Web.modules.database.termine as termin
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
from Web.tenant import get_tenant_context from Web.tenant import get_tenant_context
import Web.modules.inventarsystem.data_protection as dp
def _resolve_public_base_url() -> str: def _resolve_public_base_url() -> str:
@@ -99,8 +100,11 @@ def build_calendar_ics(appointment_id: str) -> str | None:
return None return None
uid = f"terminplaner-{appointment_id}@invario.eu" uid = f"terminplaner-{appointment_id}@invario.eu"
created_at = datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')
summary = f"Terminplan für {creator}" created_at = datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
summary = titel if titel else f"Terminplan für {creator}"
description_lines = [ description_lines = [
f"Buchungslink: {link}", f"Buchungslink: {link}",
f"Zeitraum: {date_start} bis {date_end}", f"Zeitraum: {date_start} bis {date_end}",
@@ -109,7 +113,7 @@ def build_calendar_ics(appointment_id: str) -> str | None:
description_lines.append('Zeitfenster: ' + '; '.join(str(entry) for entry in time_span)) description_lines.append('Zeitfenster: ' + '; '.join(str(entry) for entry in time_span))
if note: if note:
description_lines.append('Notiz: ' + str(note)) description_lines.append('Notiz: ' + str(note))
if titel: if titel and not summary == titel:
description_lines.append('Titel: ' + str(titel)) description_lines.append('Titel: ' + str(titel))
ics_lines = [ ics_lines = [
@@ -125,8 +129,7 @@ def build_calendar_ics(appointment_id: str) -> str | None:
f'DESCRIPTION:{_escape_ics_text(chr(10).join(description_lines))}', f'DESCRIPTION:{_escape_ics_text(chr(10).join(description_lines))}',
f'URL:{_escape_ics_text(link)}', f'URL:{_escape_ics_text(link)}',
f'DTSTART;VALUE=DATE:{_format_ics_date(start_date)}', f'DTSTART;VALUE=DATE:{_format_ics_date(start_date)}',
f'DTEND;VALUE=DATE:{_format_ics_date(end_date + timedelta(days=1))}', f'DTEND;VALUE=DATE:{_format_ics_date(end_date + datetime.timedelta(days=1))}',
f'Titel:{_escape_ics_text(titel)}',
'END:VEVENT', 'END:VEVENT',
'END:VCALENDAR', 'END:VCALENDAR',
'', '',
@@ -172,7 +175,7 @@ def build_client_slot_ics(appointment_id: str, slot_start: str, client_name: str
] ]
uid = f"terminplaner-slot-{appointment_id}-{start_dt.strftime('%Y%m%d%H%M')}@invario.eu" uid = f"terminplaner-slot-{appointment_id}-{start_dt.strftime('%Y%m%d%H%M')}@invario.eu"
created_at = datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%SZ') created_at = datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
dt_start = start_dt.strftime('%Y%m%dT%H%M%S') dt_start = start_dt.strftime('%Y%m%dT%H%M%S')
dt_end = end_dt.strftime('%Y%m%dT%H%M%S') dt_end = end_dt.strftime('%Y%m%dT%H%M%S')
@@ -405,7 +408,6 @@ def get_available(id):
def get_available_user(id): def get_available_user(id):
return get_available(id) return get_available(id)
def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]: def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
user_name = str(user or '').strip() user_name = str(user or '').strip()
if not user_name: if not user_name: