Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add version check during backup import #907

Merged
merged 10 commits into from
Oct 11, 2024
123 changes: 104 additions & 19 deletions backend/serdes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import io
import json
import sys
import re
from datetime import datetime

from django.core import management
Expand All @@ -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):
Expand Down Expand Up @@ -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(),
Expand All @@ -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
)
5 changes: 4 additions & 1 deletion frontend/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -634,5 +634,8 @@
"integrity": "النزاهة",
"availability": "التوفر",
"authenticity": "الأصالة",
"waitingRiskAcceptances": "مرحبًا! لديك حاليًا {number} مخاطرة معلقة .يمكنك العثور عليها في علامة التبويب المخاطر."
"waitingRiskAcceptances": "مرحبًا! لديك حاليًا {number} مخاطرة معلقة .يمكنك العثور عليها في علامة التبويب المخاطر.",
"backupLoadingError": "حدث خطأ أثناء تحميل النسخة الاحتياطية.",
"backupGreaterVersionError": "لا يمكن تحميل النسخة الاحتياطية، إصدار النسخة الاحتياطية أعلى من الإصدار الحالي للتطبيق الخاص بك.",
"backupLowerVersionError": "حدث خطأ، قد تكون نسخة النسخ الاحتياطي قديمة جدًا، إذا كان الأمر كذلك فيجب تحديثها قبل إعادة المحاولة."
}
5 changes: 4 additions & 1 deletion frontend/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
3 changes: 3 additions & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
5 changes: 4 additions & 1 deletion frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
5 changes: 4 additions & 1 deletion frontend/messages/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -680,5 +680,8 @@
"availability": "उपलब्धता",
"authenticity": "प्रामाणिकता",
"owner": "मालिक",
"waitingRiskAcceptances": "नमस्ते! आपके पास वर्तमान में {number} जोखिम स्वीकृति{s} लंबित हैं। आप उन्हें जोखिम टैब में पा सकते हैं।"
"waitingRiskAcceptances": "नमस्ते! आपके पास वर्तमान में {number} जोखिम स्वीकृति{s} लंबित हैं। आप उन्हें जोखिम टैब में पा सकते हैं।",
"backupLoadingError": "बैकअप लोड करते समय एक त्रुटि हुई.",
"backupGreaterVersionError": "बैकअप लोड नहीं किया जा सकता, बैकअप का संस्करण आपके एप्लिकेशन के वर्तमान संस्करण से अधिक है.",
"backupLowerVersionError": "कोई त्रुटि हुई, बैकअप संस्करण बहुत पुराना हो सकता है, यदि ऐसा है तो पुनः प्रयास करने से पहले उसे अद्यतन करना आवश्यक है।"
}
5 changes: 4 additions & 1 deletion frontend/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
5 changes: 4 additions & 1 deletion frontend/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
5 changes: 4 additions & 1 deletion frontend/messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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ą."
}
5 changes: 4 additions & 1 deletion frontend/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
5 changes: 4 additions & 1 deletion frontend/messages/ro.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
5 changes: 4 additions & 1 deletion frontend/messages/ur.json
Original file line number Diff line number Diff line change
Expand Up @@ -680,5 +680,8 @@
"availability": "دستیابی",
"authenticity": "صداقت",
"owner": "مالک",
"waitingRiskAcceptances": "ہیلو! آپ کے پاس فی الحال {number} خطرے کی منظوری {s} زیر التواء ہیں۔ آپ انہیں خطرے والے ٹیب میں پا سکتے ہیں۔"
"waitingRiskAcceptances": "ہیلو! آپ کے پاس فی الحال {number} خطرے کی منظوری {s} زیر التواء ہیں۔ آپ انہیں خطرے والے ٹیب میں پا سکتے ہیں۔",
"backupLoadingError": "بیک اپ لوڈ کرتے وقت ایک خرابی پیش آگئی۔",
"backupGreaterVersionError": "بیک اپ لوڈ نہیں ہو سکتا، بیک اپ کا ورژن آپ کی ایپلیکیشن کے موجودہ ورژن سے زیادہ ہے۔",
"backupLowerVersionError": "ایک خرابی پیش آ گئی، بیک اپ ورژن بہت پرانا ہو سکتا ہے، اگر ایسا ہے تو اسے دوبارہ کوشش کرنے سے پہلے اپ ڈیٹ کرنا ضروری ہے۔"
}
Original file line number Diff line number Diff line change
@@ -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, {
Expand All @@ -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)
};
}
};
Loading