diff --git a/backend/serdes/views.py b/backend/serdes/views.py index b15e6d6e5..8536c5978 100644 --- a/backend/serdes/views.py +++ b/backend/serdes/views.py @@ -2,6 +2,7 @@ import io import json import sys +import re from datetime import datetime from django.core import management @@ -12,9 +13,15 @@ from rest_framework.response import Response from rest_framework.views import APIView -from ciso_assistant.settings import VERSION +from ciso_assistant.settings import VERSION, SQLITE_FILE from serdes.serializers import LoadBackupSerializer +import structlog + +logger = structlog.get_logger(__name__) + +GZIP_MAGIC_NUMBER = b"\x1f\x8b" + class ExportBackupView(APIView): def get(self, request, *args, **kwargs): @@ -54,22 +61,14 @@ class LoadBackupView(APIView): parser_classes = (FileUploadParser,) serializer_class = LoadBackupSerializer - def post(self, request, *args, **kwargs): - if not request.user.has_backup_permission: - return Response(status=status.HTTP_403_FORBIDDEN) - if request.data: - backup_file = request.data["file"] - is_json = backup_file.name.split(".")[-1].lower() == "json" - data = backup_file.read() - decompressed_data = data if is_json else gzip.decompress(data) - # Performances could be improved (by avoiding the json.loads + json.dumps calls with a direct raw manipulation on the JSON body) - # But performances of the backup loading is not that much important. - decompressed_data = json.loads(decompressed_data)[1] - decompressed_data = json.dumps(decompressed_data) - - sys.stdin = io.StringIO(decompressed_data) - request.session.flush() - management.call_command("flush", interactive=False) + def load_backup(self, request, decompressed_data, backup_version, current_version): + with open(SQLITE_FILE, "rb") as database_file: + database_recover_data = database_file.read() + + sys.stdin = io.StringIO(decompressed_data) + request.session.flush() + management.call_command("flush", interactive=False) + try: # Here we load the data from stdin management.call_command( loaddata.Command(), @@ -84,5 +83,91 @@ def post(self, request, *args, **kwargs): "knox.authtoken", ], ) - return Response(status=status.HTTP_200_OK) - return Response(status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error("Error while loading backup", exc_info=e) + with open(SQLITE_FILE, "wb") as database_file: + database_file.write(database_recover_data) + + if backup_version != current_version: + logger.error("Backup version different than current version") + return Response( + {"error": "LowerBackupVersion"}, + status=status.HTTP_400_BAD_REQUEST, + ) + return Response({}, status=status.HTTP_400_BAD_REQUEST) + return Response({}, status=status.HTTP_200_OK) + + def post(self, request, *args, **kwargs): + if not request.user.has_backup_permission: + logger.error("Unauthorized user tried to load a backup", user=request.user) + return Response({}, status=status.HTTP_403_FORBIDDEN) + if not request.data: + logger.error("Request has no data") + + return Response( + {"error": "backupLoadNoData"}, status=status.HTTP_400_BAD_REQUEST + ) + backup_file = request.data["file"] + data = backup_file.read() + is_gzip = data.startswith(GZIP_MAGIC_NUMBER) + full_decompressed_data = gzip.decompress(data) if is_gzip else data + # Performances could be improved (by avoiding the json.loads + json.dumps calls with a direct raw manipulation on the JSON body) + # But performances of the backup loading is not that much important. + full_decompressed_data = json.loads(full_decompressed_data) + metadata, decompressed_data = full_decompressed_data + metadata = metadata["meta"] + + backup_version = None + for metadata_part in metadata: + backup_version = metadata_part.get("media_version") + if backup_version is not None: + break + + if backup_version is None: + logger.error("Backup malformed: no version found") + return Response( + {"erroe": "errorBackupNoVersion"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if backup_version.lower() == "dev": + backup_version = "v0.0.0" + + VERSION_REGEX = r"^v[0-9]+\.[0-9]+\.[0-9]+" + match = re.match(VERSION_REGEX, backup_version) + if match is None: + logger.error( + "Backup malformed: invalid version", + backup_version=backup_version, + current_version=VERSION, + ) + return Response( + {"error": "errorBackupInvalidVersion"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + backup_version = match.group() + current_version = VERSION.split("-")[0] + + if current_version.lower() == "dev": + current_version = "v0.0.0" + + backup_version = [int(num) for num in backup_version.lstrip("v").split(".")] + current_version = [int(num) for num in current_version.lstrip("v").split(".")] + # All versions are composed of 3 numbers (see git tag) + for i in range(3): + if backup_version[i] > current_version[i]: + logger.error( + "Backup version greater than current version", + version=backup_version, + ) + # Refuse to import the backup and ask to update the instance before importing the backup + return Response( + {"error": "GreaterBackupVersion"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + decompressed_data = json.dumps(decompressed_data) + return self.load_backup( + request, decompressed_data, backup_version, current_version + ) diff --git a/frontend/messages/ar.json b/frontend/messages/ar.json index 8739287b2..1250f7898 100644 --- a/frontend/messages/ar.json +++ b/frontend/messages/ar.json @@ -634,5 +634,8 @@ "integrity": "النزاهة", "availability": "التوفر", "authenticity": "الأصالة", - "waitingRiskAcceptances": "مرحبًا! لديك حاليًا {number} مخاطرة معلقة .يمكنك العثور عليها في علامة التبويب المخاطر." + "waitingRiskAcceptances": "مرحبًا! لديك حاليًا {number} مخاطرة معلقة .يمكنك العثور عليها في علامة التبويب المخاطر.", + "backupLoadingError": "حدث خطأ أثناء تحميل النسخة الاحتياطية.", + "backupGreaterVersionError": "لا يمكن تحميل النسخة الاحتياطية، إصدار النسخة الاحتياطية أعلى من الإصدار الحالي للتطبيق الخاص بك.", + "backupLowerVersionError": "حدث خطأ، قد تكون نسخة النسخ الاحتياطي قديمة جدًا، إذا كان الأمر كذلك فيجب تحديثها قبل إعادة المحاولة." } diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 9b1ddbc13..7777a980b 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -671,5 +671,8 @@ "observationSemiColon": "Beobachtung:", "tableMode": "Tabellenmodus", "owner": "Eigentümer", - "waitingRiskAcceptances": "Hallo! Sie haben derzeit {number} Risiko{s} zur Annahme ausstehend. Sie finden sie auf der Registerkarte „Risiken“." + "waitingRiskAcceptances": "Hallo! Sie haben derzeit {number} Risiko{s} zur Annahme ausstehend. Sie finden sie auf der Registerkarte „Risiken“.", + "backupLoadingError": "Beim Laden der Sicherung ist ein Fehler aufgetreten.", + "backupGreaterVersionError": "Das Backup kann nicht geladen werden, die Version des Backups ist höher als die aktuelle Version Ihrer Anwendung.", + "backupLowerVersionError": "Ein Fehler ist aufgetreten. Die Sicherungsversion ist möglicherweise zu alt. Wenn dies der Fall ist, muss sie vor einem erneuten Versuch aktualisiert werden." } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 48ce42c9c..8e3547a28 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -750,6 +750,9 @@ "theFollowingControlsWillBeAddedColon": "The following controls will be added:", "ShowAllNodesMessage": "Show all", "ShowOnlyAssessable": "Only assessable", + "backupLoadingError": "An error occurred while loading the backup.", + "backupGreaterVersionError": "Can't load the backup, the version of the backup is higher than the current version of your application.", + "backupLowerVersionError": "An error occurred, the backup version may be too old, if so it must be updated before retrying.", "NoPreviewMessage": "No preview available.", "errorLicenseSeatsExceeded": "The number of license seats is exceeded, you will not be able to grant editing rights to this user. Please contact your administrator.", "availableSeats": "Available seats" diff --git a/frontend/messages/es.json b/frontend/messages/es.json index af252c5a8..cabbdad0a 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -671,5 +671,8 @@ "observationSemiColon": "Observación:", "tableMode": "Modo de tabla", "owner": "Titular", - "waitingRiskAcceptances": "Hola! Actualmente tienes {number} riesgo{s} aceptación pendiente. Puedes encontrarlos en la pestaña de riesgo." + "waitingRiskAcceptances": "Hola! Actualmente tienes {number} riesgo{s} aceptación pendiente. Puedes encontrarlos en la pestaña de riesgo.", + "backupLoadingError": "Se produjo un error al cargar la copia de seguridad.", + "backupGreaterVersionError": "No se puede cargar la copia de seguridad, la versión de la copia de seguridad es superior a la versión actual de la aplicación.", + "backupLowerVersionError": "Se produjo un error, la versión de respaldo puede ser demasiado antigua, si es así debe actualizarse antes de volver a intentarlo." } diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json index 19942fb57..3140a2f6a 100644 --- a/frontend/messages/fr.json +++ b/frontend/messages/fr.json @@ -682,5 +682,8 @@ "createAppliedControlsFromSuggestionsConfirmMessage": "{count} mesures appliquées seront créées à partir des suggestions. Voulez-vous continuer ?", "theFollowingControlsWillBeAddedColon": "Les mesures suivantes seront appliquées :", "ShowAllNodesMessage": "Tout afficher", - "ShowOnlyAssessable": "Uniquement évaluables" + "ShowOnlyAssessable": "Uniquement évaluables", + "backupLoadingError": "Une erreur s'est produite lors du chargement de la sauvegarde.", + "backupGreaterVersionError": "Impossible de charger la sauvegarde, la version de la sauvegarde est supérieure à la version actuelle de votre application.", + "backupLowerVersionError": "Une erreur s'est produite, la version de sauvegarde est peut-être trop ancienne, si c'est le cas, elle doit être mise à jour avant de réessayer." } diff --git a/frontend/messages/hi.json b/frontend/messages/hi.json index 461e472a2..e4c337cca 100644 --- a/frontend/messages/hi.json +++ b/frontend/messages/hi.json @@ -680,5 +680,8 @@ "availability": "उपलब्धता", "authenticity": "प्रामाणिकता", "owner": "मालिक", - "waitingRiskAcceptances": "नमस्ते! आपके पास वर्तमान में {number} जोखिम स्वीकृति{s} लंबित हैं। आप उन्हें जोखिम टैब में पा सकते हैं।" + "waitingRiskAcceptances": "नमस्ते! आपके पास वर्तमान में {number} जोखिम स्वीकृति{s} लंबित हैं। आप उन्हें जोखिम टैब में पा सकते हैं।", + "backupLoadingError": "बैकअप लोड करते समय एक त्रुटि हुई.", + "backupGreaterVersionError": "बैकअप लोड नहीं किया जा सकता, बैकअप का संस्करण आपके एप्लिकेशन के वर्तमान संस्करण से अधिक है.", + "backupLowerVersionError": "कोई त्रुटि हुई, बैकअप संस्करण बहुत पुराना हो सकता है, यदि ऐसा है तो पुनः प्रयास करने से पहले उसे अद्यतन करना आवश्यक है।" } diff --git a/frontend/messages/it.json b/frontend/messages/it.json index 46ce4063a..270a386ee 100644 --- a/frontend/messages/it.json +++ b/frontend/messages/it.json @@ -671,5 +671,8 @@ "observationSemiColon": "Osservazione:", "tableMode": "Modalità tabella", "owner": "Proprietario", - "waitingRiskAcceptances": "Ciao! Al momento hai {number} rischi{s} in attesa di accettazione. Puoi trovarli nella scheda rischi." + "waitingRiskAcceptances": "Ciao! Al momento hai {number} rischi{s} in attesa di accettazione. Puoi trovarli nella scheda rischi.", + "backupLoadingError": "Si è verificato un errore durante il caricamento del backup.", + "backupGreaterVersionError": "Impossibile caricare il backup, la versione del backup è successiva alla versione corrente dell'applicazione.", + "backupLowerVersionError": "Si è verificato un errore, la versione di backup potrebbe essere troppo vecchia. In tal caso, è necessario aggiornarla prima di riprovare." } diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index b221feaf4..46ef03578 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -671,5 +671,8 @@ "observationSemiColon": "Observatie:", "tableMode": "Tabelmodus", "owner": "Eigenaar", - "waitingRiskAcceptances": "Hallo! U hebt momenteel {number} risico{s} acceptatie in behandeling. U kunt ze vinden in het tabblad risico." + "waitingRiskAcceptances": "Hallo! U hebt momenteel {number} risico{s} acceptatie in behandeling. U kunt ze vinden in het tabblad risico.", + "backupLoadingError": "Er is een fout opgetreden tijdens het laden van de back-up.", + "backupGreaterVersionError": "De back-up kan niet worden geladen. De versie van de back-up is hoger dan de huidige versie van uw applicatie.", + "backupLowerVersionError": "Er is een fout opgetreden. Mogelijk is de back-upversie te oud. Als dat zo is, moet u deze bijwerken voordat u het opnieuw probeert." } diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index 9ee3f15cd..e8bca8574 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -708,5 +708,8 @@ "observationSemiColon": "Obserwacja:", "tableMode": "Tryb tabeli", "owner": "Właściciel", - "waitingRiskAcceptances": "Cześć! Obecnie masz {number} ryzyko{s} oczekujące na akceptację. Możesz je znaleźć w zakładce ryzyko." + "waitingRiskAcceptances": "Cześć! Obecnie masz {number} ryzyko{s} oczekujące na akceptację. Możesz je znaleźć w zakładce ryzyko.", + "backupLoadingError": "Wystąpił błąd podczas ładowania kopii zapasowej.", + "backupGreaterVersionError": "Nie można załadować kopii zapasowej. Wersja kopii zapasowej jest nowsza niż bieżąca wersja aplikacji.", + "backupLowerVersionError": "Wystąpił błąd. Wersja kopii zapasowej może być za stara. Jeśli tak, należy ją zaktualizować przed ponowną próbą." } diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json index 745ca030a..72dacac56 100644 --- a/frontend/messages/pt.json +++ b/frontend/messages/pt.json @@ -671,5 +671,8 @@ "observationSemiColon": "Observação:", "tableMode": "Modo de tabela", "owner": "Proprietário", - "waitingRiskAcceptances": "Olá! No momento, você tem {number} aceitação de risco{s} pendente. Você pode encontrá-los na aba de risco." + "waitingRiskAcceptances": "Olá! No momento, você tem {number} aceitação de risco{s} pendente. Você pode encontrá-los na aba de risco.", + "backupLoadingError": "Ocorreu um erro ao carregar o backup.", + "backupGreaterVersionError": "Não é possível carregar o backup, a versão do backup é superior à versão atual do seu aplicativo.", + "backupLowerVersionError": "Ocorreu um erro, a versão de backup pode ser muito antiga. Se for o caso, ela deve ser atualizada antes de tentar novamente." } diff --git a/frontend/messages/ro.json b/frontend/messages/ro.json index 47d147b5e..b0fb01333 100644 --- a/frontend/messages/ro.json +++ b/frontend/messages/ro.json @@ -676,5 +676,8 @@ "integrity": "Integritate", "availability": "Accesibilitate", "authenticity": "Autenticitate", - "waitingRiskAcceptances": "Buna ziua! În prezent, aveți {number} riscul{s} în așteptare. Le puteți găsi în fila de riscuri." + "waitingRiskAcceptances": "Buna ziua! În prezent, aveți {number} riscul{s} în așteptare. Le puteți găsi în fila de riscuri.", + "backupLoadingError": "A apărut o eroare la încărcarea copiei de rezervă.", + "backupGreaterVersionError": "Nu se poate încărca copia de rezervă, versiunea copiei de rezervă este mai mare decât versiunea curentă a aplicației dvs.", + "backupLowerVersionError": "A apărut o eroare, versiunea de rezervă poate fi prea veche, dacă da, trebuie actualizată înainte de a reîncerca." } diff --git a/frontend/messages/ur.json b/frontend/messages/ur.json index 2151a9a50..db0620133 100644 --- a/frontend/messages/ur.json +++ b/frontend/messages/ur.json @@ -680,5 +680,8 @@ "availability": "دستیابی", "authenticity": "صداقت", "owner": "مالک", - "waitingRiskAcceptances": "ہیلو! آپ کے پاس فی الحال {number} خطرے کی منظوری {s} زیر التواء ہیں۔ آپ انہیں خطرے والے ٹیب میں پا سکتے ہیں۔" + "waitingRiskAcceptances": "ہیلو! آپ کے پاس فی الحال {number} خطرے کی منظوری {s} زیر التواء ہیں۔ آپ انہیں خطرے والے ٹیب میں پا سکتے ہیں۔", + "backupLoadingError": "بیک اپ لوڈ کرتے وقت ایک خرابی پیش آگئی۔", + "backupGreaterVersionError": "بیک اپ لوڈ نہیں ہو سکتا، بیک اپ کا ورژن آپ کی ایپلیکیشن کے موجودہ ورژن سے زیادہ ہے۔", + "backupLowerVersionError": "ایک خرابی پیش آ گئی، بیک اپ ورژن بہت پرانا ہو سکتا ہے، اگر ایسا ہے تو اسے دوبارہ کوشش کرنے سے پہلے اپ ڈیٹ کرنا ضروری ہے۔" } diff --git a/frontend/src/routes/(app)/(internal)/backup-restore/+page.server.ts b/frontend/src/routes/(app)/(internal)/backup-restore/+page.server.ts index 8646d8069..f3d29908b 100644 --- a/frontend/src/routes/(app)/(internal)/backup-restore/+page.server.ts +++ b/frontend/src/routes/(app)/(internal)/backup-restore/+page.server.ts @@ -1,9 +1,12 @@ import { BASE_API_URL } from '$lib/utils/constants'; import type { Actions } from '@sveltejs/kit'; import { fail } from 'assert'; +import { setFlash } from 'sveltekit-flash-message/server'; +import * as m from '$paraglide/messages'; export const actions: Actions = { - default: async ({ request, fetch }) => { + default: async (event) => { + const { request, fetch } = event; const formData = Object.fromEntries(await request.formData()); if (!(formData.file as File).name || (formData.file as File).name === 'undefined') { return fail(400, { @@ -23,10 +26,19 @@ export const actions: Actions = { }, body: file }); - const data = await response.text(); + const data = await response.json(); + + if (response.status >= 400 && !data.error) { + setFlash({ type: 'error', message: m.backupLoadingError() }, event); + } else if (data.error === 'GreaterBackupVersion') { + setFlash({ type: 'error', message: m.backupGreaterVersionError() }, event); + } else if (data.error === 'LowerBackupVersion') { + setFlash({ type: 'error', message: m.backupLowerVersionError() }, event); + } + return { status: response.status, - body: data + body: JSON.stringify(data) }; } };