From 579ff98a5a063583856e7ae5233ccc2c256330a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Rame=CC=81?= Date: Wed, 4 Mar 2026 23:56:09 +0100 Subject: [PATCH] wip encryption requirements --- src/backend/core/api/viewsets.py | 30 +++- .../components/ModalEncryptDoc.tsx | 146 +++++++++++++++--- 2 files changed, 153 insertions(+), 23 deletions(-) diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 0db70bc9..0b5b7608 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -2055,6 +2055,14 @@ class DocumentViewSet( encryptedSymmetricKeyPerUser = serializer.validated_data["encryptedSymmetricKeyPerUser"] attachment_key_mapping = serializer.validated_data.get("attachmentKeyMapping", {}) + # Prevent encryption if the document is not restricted (private) + if document.computed_link_reach != models.LinkReachChoices.RESTRICTED: + raise drf.exceptions.ValidationError({ + 'non_field_errors': + 'Cannot encrypt a document that is not private. ' + 'Please set the document access to "Restricted" before encrypting.' + }) + # Prevent encryption if there are pending invitations if document.invitations.exists(): raise drf.exceptions.ValidationError({ @@ -2063,9 +2071,25 @@ class DocumentViewSet( 'Please resolve all invitations before encrypting.' }) - # Validate that we have keys for all users with access to this document - # Get all user IDs that have access to this document - document_accesses = models.DocumentAccess.objects.filter(document=document, user__isnull=False) + # Validate that all users with access have an encryption public key + document_accesses = models.DocumentAccess.objects.filter( + document=document, user__isnull=False + ).select_related('user') + + users_without_public_key = [ + access.user.email or str(access.user_id) + for access in document_accesses + if not access.user.encryption_public_key + ] + if users_without_public_key: + raise drf.exceptions.ValidationError({ + 'non_field_errors': + 'Cannot encrypt a document when some members have not enabled ' + 'encryption: ' + ', '.join(users_without_public_key) + '. ' + 'All members must enable encryption in their account settings first.' + }) + + # Validate that we have encrypted symmetric keys for all users with access users_with_access = {str(access.user_id) for access in document_accesses} # Check that encryptedSymmetricKeyPerUser contains all required users diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalEncryptDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalEncryptDoc.tsx index e55d7298..a1199c4e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalEncryptDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalEncryptDoc.tsx @@ -6,13 +6,14 @@ import { VariantType, useToastProvider, } from '@gouvfr-lasuite/cunningham-react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import * as Y from 'yjs'; import { backendUrl } from '@/api'; import { useState } from 'react'; -import { Box, ButtonCloseModal, Text, TextErrors } from '@/components'; +import { Box, ButtonCloseModal, Icon, Text, TextErrors } from '@/components'; import { useUserUpdate } from '@/core/api/useUserUpdate'; import { encryptContent, @@ -28,10 +29,14 @@ import { Doc, KEY_DOC, KEY_LIST_DOC, + LinkReach, extractAttachmentKeysAndMetadata, + getDocLinkReach, useEncryptDoc, useProviderStore, } from '@/features/docs/doc-management'; +import { useDocAccesses } from '@/features/docs/doc-share/api/useDocAccesses'; +import { useDocInvitations } from '@/features/docs/doc-share/api/useDocInvitations'; import { useKeyboardAction } from '@/hooks'; import { Spinner } from '@gouvfr-lasuite/ui-kit'; @@ -147,8 +152,34 @@ export const ModalEncryptDoc = ({ listInvalidQueries: [KEY_DOC, KEY_LIST_DOC], }); + const { data: invitationsData } = useDocInvitations({ + docId: doc.id, + page: 1, + }); + + const { data: accesses } = useDocAccesses({ docId: doc.id }); + const keyboardAction = useKeyboardAction(); + const effectiveReach = getDocLinkReach(doc); + const isRestricted = effectiveReach === LinkReach.RESTRICTED; + const hasPendingInvitations = !!invitationsData && invitationsData.count > 0; + + const membersWithoutKey = useMemo(() => { + if (!accesses || !doc.accesses_public_keys_per_user) { + return []; + } + + const publicKeysMap = doc.accesses_public_keys_per_user; + + return accesses.filter( + (access) => access.user && !publicKeysMap[access.user.id], + ); + }, [accesses, doc.accesses_public_keys_per_user]); + + const canEncrypt = + isRestricted && !hasPendingInvitations && membersWithoutKey.length === 0; + const handleClose = () => { if (isPending) { return; @@ -157,7 +188,7 @@ export const ModalEncryptDoc = ({ }; const handleEncrypt = async () => { - if (!provider || !user || isPending) { + if (!provider || !user || isPending || !canEncrypt) { return; } @@ -315,7 +346,7 @@ export const ModalEncryptDoc = ({ fullWidth onClick={handleEncrypt} onKeyDown={handleEncryptKeyDown} - disabled={isPending} + disabled={isPending || !canEncrypt} icon={ isPending ? (
@@ -354,24 +385,99 @@ export const ModalEncryptDoc = ({ } > - + {!isError && ( - -
- TODO: warning about encryption -
- TODO: accesses for users without public key will be lost (list them) -
- TODO: if no public key for current user, provide an onboarding -
- TODO: if document public, tell it needs first to be private (add - backend check too) -
+ + + {t( + 'Encrypting a document ensures that only authorized members can read its content. Before proceeding, the following conditions must be met:', + )} + + + {/* TODO: warning about encryption */} + {/* TODO: if no public key for current user, provide an onboarding */} + + + + + + {isRestricted + ? t('Document access is private') + : t( + 'Document must be set to private (currently {{reach}})', + { + reach: + effectiveReach === LinkReach.PUBLIC + ? t('public') + : t('connected'), + }, + )} + + + + + + + {!hasPendingInvitations + ? t('No pending invitations') + : t('Pending invitations must be resolved first')} + + + + + + + + {membersWithoutKey.length === 0 + ? t('All members have encryption enabled') + : t( + '{{count}} member(s) have not enabled encryption yet', + { count: membersWithoutKey.length }, + )} + + + {membersWithoutKey.length > 0 && ( + + {membersWithoutKey.map((access) => ( + + {access.user.full_name || access.user.email} + + ))} + + )} + + + + {!canEncrypt && ( + + {t( + 'Please resolve the issues above before encrypting the document.', + )} + + )} + )} {isError && }