diff --git a/backend/core/base_models.py b/backend/core/base_models.py index 82c7984a7..4f11da487 100644 --- a/backend/core/base_models.py +++ b/backend/core/base_models.py @@ -92,12 +92,7 @@ def clean(self) -> None: if not self.is_unique_in_scope(scope=scope, fields_to_check=_fields_to_check): for field in _fields_to_check: if not self.is_unique_in_scope(scope=scope, fields_to_check=[field]): - field_errors[field] = ValidationError( - _( - f"{getattr(self, field)} is already in use in this scope. Please choose another value." - ), - code="unique", - ) + field_errors[field] = f"{getattr(self, field)} is already used in this scope. Please choose another value." super().clean() if field_errors: raise ValidationError(field_errors) diff --git a/backend/core/migrations/0008_alter_complianceassessment_status_and_more.py b/backend/core/migrations/0008_alter_complianceassessment_status_and_more.py new file mode 100644 index 000000000..7d089b187 --- /dev/null +++ b/backend/core/migrations/0008_alter_complianceassessment_status_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.2 on 2024-03-13 08:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_alter_requirementlevel_framework_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='complianceassessment', + name='status', + field=models.CharField(blank=True, choices=[('planned', 'Planned'), ('in_progress', 'In progress'), ('in_review', 'In review'), ('done', 'Done'), ('deprecated', 'Deprecated')], default='planned', max_length=100, null=True, verbose_name='Status'), + ), + migrations.AlterField( + model_name='riskassessment', + name='status', + field=models.CharField(blank=True, choices=[('planned', 'Planned'), ('in_progress', 'In progress'), ('in_review', 'In review'), ('done', 'Done'), ('deprecated', 'Deprecated')], default='planned', max_length=100, null=True, verbose_name='Status'), + ), + ] diff --git a/backend/core/models.py b/backend/core/models.py index 8d54ea012..9b1a104f2 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -167,6 +167,8 @@ class Threat(ReferentialObjectMixin): Library, on_delete=models.CASCADE, null=True, blank=True, related_name="threats" ) + fields_to_check = ["ref_id", "name"] + class Meta: verbose_name = _("Threat") verbose_name_plural = _("Threats") @@ -215,6 +217,8 @@ class ReferenceControl(ReferentialObjectMixin): verbose_name=_("Typical evidence"), null=True, blank=True ) + fields_to_check = ["ref_id", "name"] + class Meta: verbose_name = _("Reference control") verbose_name_plural = _("Reference controls") @@ -548,6 +552,8 @@ class Evidence(NameDescriptionMixin, FolderMixin): verbose_name=_("Link"), ) + fields_to_check = ["name"] + class Meta: verbose_name = _("Evidence") verbose_name_plural = _("Evidences") @@ -636,7 +642,7 @@ class Status(models.TextChoices): verbose_name=_("Effort"), ) - fields_to_check = ["name", "category"] + fields_to_check = ["name"] class Meta: verbose_name = _("Applied control") @@ -750,6 +756,8 @@ class Status(models.TextChoices): choices=Status.choices, default=Status.PLANNED, verbose_name=_("Status"), + blank=True, + null=True ) authors = models.ManyToManyField( User, @@ -1155,6 +1163,8 @@ class RiskScenario(NameDescriptionMixin): max_length=500, blank=True, null=True, verbose_name=_("Justification") ) + fields_to_check = ["name"] + class Meta: verbose_name = _("Risk scenario") verbose_name_plural = _("Risk scenarios") @@ -1274,6 +1284,7 @@ class Result(models.TextChoices): verbose_name=_("Result"), ) + class Meta: verbose_name = _("Compliance assessment") verbose_name_plural = _("Compliance assessments") diff --git a/backend/core/serializers.py b/backend/core/serializers.py index ab6444bcc..92de37ec0 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -23,7 +23,12 @@ def update(self, instance: models.Model, validated_data: Any) -> models.Model: raise serializers.ValidationError( {"urn": "Imported objects cannot be modified"} ) - return super().update(instance, validated_data) + try: + object_updated = super().update(instance, validated_data) + return object_updated + except Exception as e: + logger.error(e) + raise serializers.ValidationError(e.args[0]) def create(self, validated_data: Any): logger.debug("validated data", **validated_data) @@ -42,7 +47,12 @@ def create(self, validated_data: Any): "folder": "You do not have permission to create objects in this folder" } ) - return super().create(validated_data) + try: + object_created = super().create(validated_data) + return object_created + except Exception as e: + logger.error(e) + raise serializers.ValidationError(e.args[0]) class Meta: model: models.Model diff --git a/frontend/src/lib/components/Forms/Form.svelte b/frontend/src/lib/components/Forms/Form.svelte index efffad084..d2d7b1a92 100644 --- a/frontend/src/lib/components/Forms/Form.svelte +++ b/frontend/src/lib/components/Forms/Form.svelte @@ -4,6 +4,11 @@ import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte'; import type { AnyZodObject } from 'zod'; + import type { ModalStore } from '@skeletonlabs/skeleton'; + import { getModalStore } from '@skeletonlabs/skeleton'; + + const modalStore: ModalStore = getModalStore(); + export let data: SuperValidated; export let dataType: 'form' | 'json'; export let invalidateAll = true; // set to false to keep form data using muliple forms on a page @@ -14,12 +19,25 @@ export let debug = false; // set to true to enable SuperDebug component + function handleFormUpdated({ + form, + closeModal + }: { + form: any; + closeModal: boolean; + }) { + if (closeModal && form.valid) { + $modalStore[0] ? modalStore.close() : null; + } + } + export const _form = superForm(data, { dataType: dataType, invalidateAll: invalidateAll, applyAction: applyAction, resetForm: resetForm, validators: validators, + onUpdated: ({ form }) => handleFormUpdated({ form, closeModal: true }), onSubmit: onSubmit }); diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte index aeb577f33..01a2ed047 100644 --- a/frontend/src/lib/components/Forms/ModelForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm.svelte @@ -17,8 +17,6 @@ import { browser } from '$app/environment'; import { page } from '$app/stores'; import * as m from '$paraglide/messages.js'; - import { localItems, toCamelCase } from '$lib/utils/locales'; - import { languageTag } from '$paraglide/runtime'; export let form: SuperValidated; export let model: ModelInfo; diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts index 3cf3ae210..c82e394fe 100644 --- a/frontend/src/lib/utils/schemas.ts +++ b/frontend/src/lib/utils/schemas.ts @@ -1,71 +1,71 @@ import { z, type AnyZodObject } from 'zod'; export const loginSchema = z - .object({ - username: z - .string({ - required_error: 'Email is required' - }) - .email(), - password: z.string({ - required_error: 'Password is required' - }) - }) - .required(); + .object({ + username: z + .string({ + required_error: 'Email is required' + }) + .email(), + password: z.string({ + required_error: 'Password is required' + }) + }) + .required(); export const emailSchema = z - .object({ - email: z.string({ - required_error: 'Email is required' - }) - }) - .required(); + .object({ + email: z.string({ + required_error: 'Email is required' + }) + }) + .required(); // Utility functions for commonly used schema structures const nameSchema = z - .string({ - required_error: 'Name is required' - }) - .min(1); + .string({ + required_error: 'Name is required' + }) + .min(1); const descriptionSchema = z.string().optional().nullable(); const baseNamedObject = (additionalFields: any) => - z.object({ - name: nameSchema, - description: descriptionSchema, - ...additionalFields - }); + z.object({ + name: nameSchema, + description: descriptionSchema, + ...additionalFields + }); export const FolderSchema = baseNamedObject({ - parent_folder: z.string().optional() + parent_folder: z.string().optional() }); export const ProjectSchema = baseNamedObject({ - folder: z.string(), - internal_reference: z.string().optional().nullable(), - lc_status: z.string().optional().default('in_design') + folder: z.string(), + internal_reference: z.string().optional().nullable(), + lc_status: z.string().optional().default('in_design') }); export const RiskMatrixSchema = baseNamedObject({ - folder: z.string(), - json_definition: z.string(), - is_enabled: z.boolean() + folder: z.string(), + json_definition: z.string(), + is_enabled: z.boolean() }); export const LibraryUploadSchema = z.object({ - file: z.string().optional() + file: z.string().optional() }); export const RiskAssessmentSchema = baseNamedObject({ - version: z.string().optional().default('0.1'), - project: z.string(), - status: z.string().optional(), - risk_matrix: z.string(), - eta: z.string().optional().nullable(), - due_date: z.string().optional().nullable(), - authors: z.array(z.string().optional()), - reviewers: z.array(z.string().optional()) + version: z.string().optional().default('0.1'), + project: z.string(), + status: z.string().optional().nullable(), + risk_matrix: z.string(), + eta: z.string().optional().nullable(), + due_date: z.string().optional().nullable(), + authors: z.array(z.string().optional()), + reviewers: z.array(z.string().optional()) }); export const ThreatSchema = baseNamedObject({ @@ -74,137 +74,137 @@ export const ThreatSchema = baseNamedObject({ }); export const RiskScenarioSchema = baseNamedObject({ - existing_controls: z.string().optional(), - applied_controls: z.string().uuid().optional().array(), - current_proba: z.number().optional(), - current_impact: z.number().optional(), - residual_proba: z.number().optional(), - residual_impact: z.number().optional(), - treatment: z.string().optional(), - strength_of_knowledge: z.number().default(-1).optional(), - justification: z.string().optional().nullable(), - risk_assessment: z.string(), - threats: z.string().uuid().optional().array(), - assets: z.string().uuid().optional().array() + existing_controls: z.string().optional(), + applied_controls: z.string().uuid().optional().array(), + current_proba: z.number().optional(), + current_impact: z.number().optional(), + residual_proba: z.number().optional(), + residual_impact: z.number().optional(), + treatment: z.string().optional(), + strength_of_knowledge: z.number().default(-1).optional(), + justification: z.string().optional().nullable(), + risk_assessment: z.string(), + threats: z.string().uuid().optional().array(), + assets: z.string().uuid().optional().array() }); export const AppliedControlSchema = baseNamedObject({ - category: z.string().optional().nullable(), - status: z.string().optional().nullable(), - evidences: z.string().optional().array().optional(), - eta: z.string().optional().nullable(), - expiry_date: z.string().optional().nullable(), - link: z.string().url().optional().nullable(), - effort: z.string().optional().nullable(), - folder: z.string(), - reference_control: z.string().optional() + category: z.string().optional().nullable(), + status: z.string().optional().nullable(), + evidences: z.string().optional().array().optional(), + eta: z.string().optional().nullable(), + expiry_date: z.string().optional().nullable(), + link: z.string().url().optional().nullable(), + effort: z.string().optional().nullable(), + folder: z.string(), + reference_control: z.string().optional() }); export const PolicySchema = baseNamedObject({ - status: z.string().optional(), - evidences: z.string().optional().array().optional(), - eta: z.string().optional().nullable(), - expiry_date: z.string().optional().nullable(), - link: z.string().url().optional().nullable(), - effort: z.string().optional(), - folder: z.string(), - reference_control: z.string().optional() + status: z.string().optional().nullable(), + evidences: z.string().optional().array().optional(), + eta: z.string().optional().nullable(), + expiry_date: z.string().optional().nullable(), + link: z.string().url().optional().nullable(), + effort: z.string().optional().nullable(), + folder: z.string(), + reference_control: z.string().optional() }); export const RiskAcceptanceSchema = baseNamedObject({ - folder: z.string(), - expiry_date: z.string().optional().nullable(), - justification: z.string().optional().nullable(), - approver: z.string(), - risk_scenarios: z.array(z.string()) + folder: z.string(), + expiry_date: z.string().optional().nullable(), + justification: z.string().optional().nullable(), + approver: z.string(), + risk_scenarios: z.array(z.string()) }); export const ReferenceControlSchema = baseNamedObject({ - provider: z.string().optional().nullable(), - category: z.string().optional().nullable(), - folder: z.string() + provider: z.string().optional().nullable(), + category: z.string().optional().nullable(), + folder: z.string() }); export const AssetSchema = baseNamedObject({ - business_value: z.string().optional(), - type: z.string(), - folder: z.string(), - parent_assets: z.array(z.string()).optional() + business_value: z.string().optional(), + type: z.string(), + folder: z.string(), + parent_assets: z.array(z.string()).optional() }); export const RequirementAssessmentSchema = z.object({ - status: z.string(), - comment: z.string().optional().nullable(), - folder: z.string(), - requirement: z.string(), - evidences: z.string().uuid().optional().array(), - compliance_assessment: z.string(), - applied_controls: z.string().uuid().optional().array() + status: z.string(), + comment: z.string().optional().nullable(), + folder: z.string(), + requirement: z.string(), + evidences: z.string().uuid().optional().array(), + compliance_assessment: z.string(), + applied_controls: z.string().uuid().optional().array() }); export const UserEditSchema = z.object({ - email: z.string().email(), - first_name: z.string().optional(), - last_name: z.string().optional(), - is_active: z.boolean().optional(), - user_groups: z.array(z.string().uuid().optional()).optional() + email: z.string().email(), + first_name: z.string().optional(), + last_name: z.string().optional(), + is_active: z.boolean().optional(), + user_groups: z.array(z.string().uuid().optional()).optional() }); export const UserCreateSchema = z.object({ email: z.string().email() }); export const ChangePasswordSchema = z.object({ - old_password: z.string(), - new_password: z.string(), - confirm_new_password: z.string() + old_password: z.string(), + new_password: z.string(), + confirm_new_password: z.string() }); export const ResetPasswordSchema = z.object({ - new_password: z.string(), - confirm_new_password: z.string() + new_password: z.string(), + confirm_new_password: z.string() }); export const SetPasswordSchema = z.object({ - user: z.string(), - new_password: z.string(), - confirm_new_password: z.string() + user: z.string(), + new_password: z.string(), + confirm_new_password: z.string() }); export const ComplianceAssessmentSchema = baseNamedObject({ - version: z.string().optional().default('0.1'), - project: z.string(), - status: z.string().optional(), - framework: z.string(), - eta: z.string().optional().nullable(), - due_date: z.string().optional().nullable(), - authors: z.array(z.string().optional()), - reviewers: z.array(z.string().optional()) + version: z.string().optional().default('0.1'), + project: z.string(), + status: z.string().optional().nullable(), + framework: z.string(), + eta: z.string().optional().nullable(), + due_date: z.string().optional().nullable(), + authors: z.array(z.string().optional()), + reviewers: z.array(z.string().optional()) }); export const EvidenceSchema = baseNamedObject({ - attachment: z.string().optional().nullable(), - folder: z.string(), - applied_controls: z.string().optional().array().optional(), - requirement_assessments: z.string().optional().array().optional(), - link: z.string().optional().nullable() + attachment: z.string().optional().nullable(), + folder: z.string(), + applied_controls: z.string().optional().array().optional(), + requirement_assessments: z.string().optional().array().optional(), + link: z.string().optional().nullable() }); const SCHEMA_MAP: Record = { - folders: FolderSchema, - projects: ProjectSchema, - 'risk-matrices': RiskMatrixSchema, - 'risk-assessments': RiskAssessmentSchema, - threats: ThreatSchema, - 'risk-scenarios': RiskScenarioSchema, - 'applied-controls': AppliedControlSchema, - policies: PolicySchema, - 'risk-acceptances': RiskAcceptanceSchema, - 'reference-controls': ReferenceControlSchema, - assets: AssetSchema, - 'requirement-assessments': RequirementAssessmentSchema, - 'compliance-assessments': ComplianceAssessmentSchema, - evidences: EvidenceSchema, - users: UserCreateSchema + folders: FolderSchema, + projects: ProjectSchema, + 'risk-matrices': RiskMatrixSchema, + 'risk-assessments': RiskAssessmentSchema, + threats: ThreatSchema, + 'risk-scenarios': RiskScenarioSchema, + 'applied-controls': AppliedControlSchema, + policies: PolicySchema, + 'risk-acceptances': RiskAcceptanceSchema, + 'reference-controls': ReferenceControlSchema, + assets: AssetSchema, + 'requirement-assessments': RequirementAssessmentSchema, + 'compliance-assessments': ComplianceAssessmentSchema, + evidences: EvidenceSchema, + users: UserCreateSchema }; export const modelSchema = (model: string) => { - return SCHEMA_MAP[model] || z.object({}); + return SCHEMA_MAP[model] || z.object({}); }; diff --git a/frontend/src/routes/(app)/[model=urlmodel]/+page.server.ts b/frontend/src/routes/(app)/[model=urlmodel]/+page.server.ts index 01a0a7f36..0b1147e9f 100644 --- a/frontend/src/routes/(app)/[model=urlmodel]/+page.server.ts +++ b/frontend/src/routes/(app)/[model=urlmodel]/+page.server.ts @@ -7,7 +7,7 @@ import { } from '$lib/utils/crud'; import { modelSchema } from '$lib/utils/schemas'; import { fail, type Actions } from '@sveltejs/kit'; -import { message, setError, superValidate } from 'sveltekit-superforms/server'; +import { setError, superValidate } from 'sveltekit-superforms/server'; import { localItems, toCamelCase } from '$lib/utils/locales'; import * as m from '$paraglide/messages'; import { languageTag } from '$paraglide/runtime'; @@ -125,22 +125,22 @@ export const actions: Actions = { } } - const model: string = urlParamModelVerboseName(event.params.model!); + const model: string = event.params.model ? urlParamModelVerboseName(event.params.model) : ''; // TODO: reference newly created object if (model === 'User') { - return message(createForm, m.successfullyCreatedObject({object: localItems(languageTag())[model.toLowerCase()].toLowerCase()})); + setFlash({ type: 'success', message: m.successfullyCreatedObject({object: localItems(languageTag())[toCamelCase(model)].toLowerCase()}) }, event); } - return message(createForm, m.successfullyCreatedObject({object: localItems(languageTag())[toCamelCase(model.toLowerCase())].toLowerCase()})); + setFlash({ type: 'success', message: m.successfullyCreatedObject({object: localItems(languageTag())[toCamelCase(model)].toLowerCase()}) }, event); } return { createForm }; }, - delete: async ({ request, fetch, params }) => { - const formData = await request.formData(); + delete: async (event) => { + const formData = await event.request.formData(); const schema = z.object({ id: z.string().uuid() }); const deleteForm = await superValidate(formData, schema); const id = deleteForm.data.id; - const endpoint = `${BASE_API_URL}/${params.model}/${id}/`; + const endpoint = `${BASE_API_URL}/${event.params.model}/${id}/`; if (!deleteForm.valid) { console.log(deleteForm.errors); @@ -151,7 +151,7 @@ export const actions: Actions = { const requestInitOptions: RequestInit = { method: 'DELETE' }; - const res = await fetch(endpoint, requestInitOptions); + const res = await event.fetch(endpoint, requestInitOptions); if (!res.ok) { const response = await res.json(); console.log(response); @@ -160,9 +160,9 @@ export const actions: Actions = { } return fail(400, { form: deleteForm }); } - const model: string = urlParamModelVerboseName(params.model!); + const model: string = urlParamModelVerboseName(event.params.model!); // TODO: reference object by name instead of id - return message(deleteForm, m.successfullyDeletedObject({object: localItems(languageTag())[toCamelCase(model.toLowerCase())].toLowerCase(), id: id})); + setFlash({ type: 'success', message: m.successfullyDeletedObject({object: localItems(languageTag())[toCamelCase(toCamelCase(model))].toLowerCase()}) }, event); } return { deleteForm }; } diff --git a/frontend/src/routes/(app)/[model=urlmodel]/+page.svelte b/frontend/src/routes/(app)/[model=urlmodel]/+page.svelte index cf46c91a0..21d2e4c71 100644 --- a/frontend/src/routes/(app)/[model=urlmodel]/+page.svelte +++ b/frontend/src/routes/(app)/[model=urlmodel]/+page.svelte @@ -1,15 +1,12 @@