Skip to content

Commit

Permalink
fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Arnaud AMBROSELLI committed Dec 5, 2023
1 parent fc77a2c commit 9e2faf3
Show file tree
Hide file tree
Showing 17 changed files with 208 additions and 30 deletions.
56 changes: 41 additions & 15 deletions dashboard/src/components/DataMigrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ const duplicateDecryptedData = async ({
personIdsMapped[person._id] = newPersonId;
const newPerson = {
...person,
user: userIdsMapped[person.user],
documents: await recryptPersonRelatedDocuments(person, person._id, newPersonId),
assignedTeams: person.assignedTeams?.map((t) => teamIdsMapped[t]).filter(Boolean) ?? [],
organisation: nextOrganisationId,
Expand All @@ -230,6 +231,7 @@ const duplicateDecryptedData = async ({
consultationIdsMapped[consultation._id] = newConsultationId;
const newConsultation = {
...consultation,
user: userIdsMapped[consultation.user],
documents: await recryptPersonRelatedDocuments(consultation, consultation.person, personIdsMapped[consultation.person]),
person: personIdsMapped[consultation.person],
teams: consultation.teams?.map((t) => teamIdsMapped[t]).filter(Boolean) ?? [],
Expand All @@ -248,6 +250,7 @@ const duplicateDecryptedData = async ({
treatmentIdsMapped[treatment._id] = newTreatmentId;
const newTreatment = {
...treatment,
user: userIdsMapped[treatment.user],
documents: await recryptPersonRelatedDocuments(treatment, treatment.person, personIdsMapped[treatment.person]),
person: personIdsMapped[treatment.person],
organisation: nextOrganisationId,
Expand All @@ -266,6 +269,7 @@ const duplicateDecryptedData = async ({
medicalFileIdsMapped[medicalFile._id] = newMedicalFileId;
const newMedicalFile = {
...medicalFile,
user: userIdsMapped[medicalFile.user],
documents: await recryptPersonRelatedDocuments(medicalFile, medicalFile.person, personIdsMapped[medicalFile.person]),
person: personIdsMapped[medicalFile.person],
organisation: nextOrganisationId,
Expand All @@ -284,6 +288,7 @@ const duplicateDecryptedData = async ({
actionIdsMapped[action._id] = newActionId;
const newAction = {
...action,
user: userIdsMapped[action.user],
person: personIdsMapped[action.person],
teams: action.teams?.map((t) => teamIdsMapped[t]).filter(Boolean) ?? [],
organisation: nextOrganisationId,
Expand Down Expand Up @@ -325,6 +330,7 @@ const duplicateDecryptedData = async ({
commentIdsMapped[comment._id] = newCommentId;
const newComment = {
...comment,
user: userIdsMapped[comment.user],
team: teamIdsMapped[comment.team],
organisation: nextOrganisationId,
_id: newCommentId,
Expand All @@ -345,6 +351,7 @@ const duplicateDecryptedData = async ({
passageIdsMapped[passage._id] = newPassageId;
const newPassage = {
...passage,
user: userIdsMapped[passage.user],
team: teamIdsMapped[passage.team],
person: personIdsMapped[passage.person],
organisation: nextOrganisationId,
Expand All @@ -360,6 +367,7 @@ const duplicateDecryptedData = async ({
rencontreIdsMapped[rencontre._id] = newRencontreId;
const newRencontre = {
...rencontre,
user: userIdsMapped[rencontre.user],
team: teamIdsMapped[rencontre.team],
person: personIdsMapped[rencontre.person],
organisation: nextOrganisationId,
Expand All @@ -375,6 +383,7 @@ const duplicateDecryptedData = async ({
territoryIdsMapped[territory._id] = newTerritoryId;
const newTerritory = {
...territory,
user: userIdsMapped[territory.user],
organisation: nextOrganisationId,
_id: newTerritoryId,
};
Expand All @@ -389,10 +398,10 @@ const duplicateDecryptedData = async ({
territoryObservationIdsMapped[territoryObservation._id] = newTerritoryObservationId;
const newTerritoryObservation = {
...territoryObservation,
user: userIdsMapped[territoryObservation.user],
territory: territoryIdsMapped[territoryObservation.territory],
team: teamIdsMapped[territoryObservation.team],
organisation: nextOrganisationId,
observedAt: territoryObservation.deletedAt ? territoryObservation.observedAt : territoryObservation.createdAt,
_id: newTerritoryObservationId,
};
newObs.push(newTerritoryObservation);
Expand All @@ -405,6 +414,7 @@ const duplicateDecryptedData = async ({
placeIdsMapped[place._id] = newPlaceId;
const newPlace = {
...place,
user: userIdsMapped[place.user],
organisation: nextOrganisationId,
_id: newPlaceId,
};
Expand All @@ -418,6 +428,7 @@ const duplicateDecryptedData = async ({
relPersonPlaceIdsMapped[relPersonPlace._id] = newRelPersonPlaceId;
const newRelPersonPlace = {
...relPersonPlace,
user: userIdsMapped[relPersonPlace.user],
person: personIdsMapped[relPersonPlace.person],
place: placeIdsMapped[relPersonPlace.place],
organisation: nextOrganisationId,
Expand All @@ -433,6 +444,7 @@ const duplicateDecryptedData = async ({
reportIdsMapped[report._id] = newReportId;
const newReport = {
...report,
user: userIdsMapped[report.user],
team: teamIdsMapped[report.team],
organisation: nextOrganisationId,
_id: newReportId,
Expand All @@ -447,20 +459,34 @@ const duplicateDecryptedData = async ({
teams: newTeams,
users: newUsers,
relUserTeams: newRelUserTeams,
persons: await Promise.all(newPersons.map(preparePersonForEncryption).map(encryptItem)),
consultations: await Promise.all(newConsultations.map(prepareConsultationForEncryption(organisation.consultations)).map(encryptItem)),
treatments: await Promise.all(newTreatments.map(prepareTreatmentForEncryption).map(encryptItem)),
medicalFiles: await Promise.all(newMedicalFiles.map(prepareMedicalFileForEncryption(organisation.customFieldsMedicalFile)).map(encryptItem)),
actions: await Promise.all(newActions.map(prepareActionForEncryption).map(encryptItem)),
groups: await Promise.all(newGroups.map(prepareGroupForEncryption).map(encryptItem)),
comments: await Promise.all(newComments.map(prepareCommentForEncryption).map(encryptItem)),
passages: await Promise.all(newPassages.map(preparePassageForEncryption).map(encryptItem)),
rencontres: await Promise.all(newRencontres.map(prepareRencontreForEncryption).map(encryptItem)),
territories: await Promise.all(newTerritories.map(prepareTerritoryForEncryption).map(encryptItem)),
observations: await Promise.all(newObs.map(prepareObsForEncryption(organisation.customFieldsObs)).map(encryptItem)),
places: await Promise.all(newPlaces.map(preparePlaceForEncryption).map(encryptItem)),
relsPersonPlace: await Promise.all(newRelPersonPlaces.map(prepareRelPersonPlaceForEncryption).map(encryptItem)),
reports: await Promise.all(newReports.map(prepareReportForEncryption).map(encryptItem)),
persons: await Promise.all(newPersons.map((item) => preparePersonForEncryption(item, { checkRequiredFields: false })).map(encryptItem)),
consultations: await Promise.all(
newConsultations
.map((item) => prepareConsultationForEncryption(organisation.consultations)(item, { checkRequiredFields: false }))
.map(encryptItem)
),
treatments: await Promise.all(newTreatments.map((item) => prepareTreatmentForEncryption(item, { checkRequiredFields: false })).map(encryptItem)),
medicalFiles: await Promise.all(
newMedicalFiles
.map((item) => prepareMedicalFileForEncryption(organisation.customFieldsMedicalFile)(item, { checkRequiredFields: false }))
.map(encryptItem)
),
actions: await Promise.all(newActions.map((item) => prepareActionForEncryption(item, { checkRequiredFields: false })).map(encryptItem)),
groups: await Promise.all(newGroups.map((item) => prepareGroupForEncryption(item, { checkRequiredFields: false })).map(encryptItem)),
comments: await Promise.all(newComments.map((item) => prepareCommentForEncryption(item, { checkRequiredFields: false })).map(encryptItem)),
passages: await Promise.all(newPassages.map((item) => preparePassageForEncryption(item, { checkRequiredFields: false })).map(encryptItem)),
rencontres: await Promise.all(newRencontres.map((item) => prepareRencontreForEncryption(item, { checkRequiredFields: false })).map(encryptItem)),
territories: await Promise.all(
newTerritories.map((item) => prepareTerritoryForEncryption(item, { checkRequiredFields: false })).map(encryptItem)
),
observations: await Promise.all(
newObs.map((item) => prepareObsForEncryption(organisation.customFieldsObs)(item, { checkRequiredFields: false })).map(encryptItem)
),
places: await Promise.all(newPlaces.map((item) => preparePlaceForEncryption(item, { checkRequiredFields: false })).map(encryptItem)),
relsPersonPlace: await Promise.all(
newRelPersonPlaces.map((item) => prepareRelPersonPlaceForEncryption(item, { checkRequiredFields: false })).map(encryptItem)
),
reports: await Promise.all(newReports.map((item) => prepareReportForEncryption(item, { checkRequiredFields: false })).map(encryptItem)),
};
};

Expand Down
154 changes: 154 additions & 0 deletions dashboard/src/components/duplicateOrganisation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { encryptVerificationKey } from '../services/encryption';
import { capture } from '../services/sentry';
import API, { getHashedOrgEncryptionKey, decryptAndEncryptItem } from '../services/api';

export default async function duplicate({ organisation }) {
const hashedOrgEncryptionKey = getHashedOrgEncryptionKey();
const encryptedVerificationKey = await encryptVerificationKey(hashedOrgEncryptionKey);

async function recrypt(path, callback = null) {
const cryptedItems = await API.get({
skipDecrypt: true,
path,
query: {
organisation: organisation._id,
limit: String(Number.MAX_SAFE_INTEGER),
page: String(0),
after: String(0),
withDeleted: true,
},
});
const encryptedItems = [];
for (const item of cryptedItems.data) {
try {
const recrypted = await decryptAndEncryptItem(item, previousKey.current, hashedOrgEncryptionKey, callback);
if (recrypted) encryptedItems.push(recrypted);
} catch (e) {
capture(e);
throw new Error(
`Impossible de déchiffrer et rechiffrer l'élément suivant: ${path} ${item._id}. Notez le numéro affiché et fournissez le à l'équipe de support.`
);
}
}
return encryptedItems;
}

const encryptedPersons = await recrypt('/person', async (decryptedData, item) =>
recryptPersonRelatedDocuments(decryptedData, item._id, previousKey.current, hashedOrgEncryptionKey)
);
const encryptedConsultations = await recrypt('/consultation', async (decryptedData) =>
recryptPersonRelatedDocuments(decryptedData, decryptedData.person, previousKey.current, hashedOrgEncryptionKey)
);
const encryptedTreatments = await recrypt('/treatment', async (decryptedData) =>
recryptPersonRelatedDocuments(decryptedData, decryptedData.person, previousKey.current, hashedOrgEncryptionKey)
);
const encryptedMedicalFiles = await recrypt('/medical-file', async (decryptedData) =>
recryptPersonRelatedDocuments(decryptedData, decryptedData.person, previousKey.current, hashedOrgEncryptionKey)
);
const encryptedGroups = await recrypt('/group');
const encryptedActions = await recrypt('/action');
const encryptedComments = await recrypt('/comment');
const encryptedPassages = await recrypt('/passage');
const encryptedRencontres = await recrypt('/rencontre');
const encryptedTerritories = await recrypt('/territory');
const encryptedTerritoryObservations = await recrypt('/territory-observation');
const encryptedPlaces = await recrypt('/place');
const encryptedRelsPersonPlace = await recrypt('/relPersonPlace');
const encryptedReports = await recrypt('/report');

const totalToEncrypt =
encryptedPersons.length +
encryptedGroups.length +
encryptedActions.length +
encryptedConsultations.length +
encryptedTreatments.length +
encryptedMedicalFiles.length +
encryptedComments.length +
encryptedPassages.length +
encryptedRencontres.length +
encryptedTerritories.length +
encryptedTerritoryObservations.length +
encryptedRelsPersonPlace.length +
encryptedPlaces.length +
encryptedReports.length;

totalDurationOnServer.current = totalToEncrypt * 0.005; // average 5 ms in server

const res = await API.post({
path: '/encrypt',
body: {
persons: encryptedPersons,
groups: encryptedGroups,
actions: encryptedActions,
consultations: encryptedConsultations,
treatments: encryptedTreatments,
medicalFiles: encryptedMedicalFiles,
comments: encryptedComments,
passages: encryptedPassages,
rencontres: encryptedRencontres,
territories: encryptedTerritories,
observations: encryptedTerritoryObservations,
places: encryptedPlaces,
relsPersonPlace: encryptedRelsPersonPlace,
reports: encryptedReports,
encryptedVerificationKey,
},
query: {
encryptionLastUpdateAt: organisation.encryptionLastUpdateAt,
encryptionEnabled: true,
changeMasterKey: true,
},
});

if (res.ok) {
}
}

const recryptDocument = async (doc, personId, { fromKey, toKey }) => {
const content = await API.download(
{
path: doc.downloadPath ?? `/person/${personId}/document/${doc.file.filename}`,
encryptedEntityKey: doc.encryptedEntityKey,
},
fromKey
);
const docResult = await API.upload(
{
path: `/person/${personId}/document`,
file: new File([content], doc.file.originalname, { type: doc.file.mimetype }),
},
toKey
);
const { data: file, encryptedEntityKey } = docResult;
return {
_id: file.filename,
name: doc.file.originalname,
encryptedEntityKey,
createdAt: doc.createdAt,
createdBy: doc.createdBy,
downloadPath: `/person/${personId}/document/${file.filename}`,
file,
};
};

const recryptPersonRelatedDocuments = async (item, id, oldKey, newKey) => {
if (!item.documents || !item.documents.length) return item;
const updatedDocuments = [];
for (const doc of item.documents) {
try {
const recryptedDocument = await recryptDocument(doc, id, { fromKey: oldKey, toKey: newKey });
updatedDocuments.push(recryptedDocument);
} catch (e) {
console.error(e);
// we need a temporary hack, for the organisations which already changed their encryption key
// but not all the documents were recrypted
// we told them to change back from `newKey` to `oldKey` to retrieve the old documents
// and then change back to `newKey` to recrypt them in the new key
// SO
// if the recryption failed, we assume the document might have been encrypted with the newKey already
// so we push it
updatedDocuments.push(doc);
}
}
return { ...item, documents: updatedDocuments };
};
2 changes: 1 addition & 1 deletion dashboard/src/recoil/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const allowedActionFieldsInHistory = [
];

export const prepareActionForEncryption = (action, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields && !action.deletedAt) {
if (!!checkRequiredFields) {
try {
if (!looseUuidRegex.test(action.person)) {
throw new Error('Action is missing person');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const commentsState = atom({
const encryptedFields = ['comment', 'person', 'action', 'group', 'team', 'user', 'date', 'urgent'];

export const prepareCommentForEncryption = (comment, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields && !comment.deletedAt) {
if (!!checkRequiredFields) {
try {
if (!looseUuidRegex.test(comment.person) && !looseUuidRegex.test(comment.action)) {
throw new Error('Comment is missing person or action');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/consultations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const consultationsFieldsIncludingCustomFieldsSelector = selector({
export const prepareConsultationForEncryption =
(customFieldsConsultations: CustomFieldsGroup[]) =>
(consultation: ConsultationInstance, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields && !consultation.deletedAt) {
if (!!checkRequiredFields) {
try {
if (!looseUuidRegex.test(consultation.person)) {
throw new Error('Consultation is missing person');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/medicalFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const encryptedFields = ['person', 'documents', 'comments', 'history'];
export const prepareMedicalFileForEncryption =
(customFieldsMedicalFile: CustomField[]) =>
(medicalFile: MedicalFileInstance | NewMedicalFileInstance, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields && !medicalFile.deletedAt) {
if (!!checkRequiredFields) {
try {
if (!looseUuidRegex.test(medicalFile.person)) {
throw new Error('MedicalFile is missing person');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/passages.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const passagesState = atom({
const encryptedFields = ['person', 'team', 'user', 'date', 'comment'];

export const preparePassageForEncryption = (passage, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields && !passage.deletedAt) {
if (!!checkRequiredFields) {
try {
// we don't check the presence of a person because passage can be anonymous
if (!looseUuidRegex.test(passage.team)) {
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/persons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const usePreparePersonForEncryption = () => {
const fieldsPersonsCustomizableOptions = useRecoilValue(fieldsPersonsCustomizableOptionsSelector);
const personFields = useRecoilValue(personFieldsSelector) as PredefinedField[];
const preparePersonForEncryption = (person: PersonInstance, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields && !person.deletedAt) {
if (!!checkRequiredFields) {
try {
if (!person.name) {
throw new Error('Person is missing name');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/places.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const placesState = atom({
const encryptedFields = ['user', 'name'];

export const preparePlaceForEncryption = (place, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields && !place.deletedAt) {
if (!!checkRequiredFields) {
try {
if (!place.name) {
throw new Error('Place is missing name');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/relPersonPlace.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const relsPersonPlaceState = atom({
const encryptedFields = ['place', 'person', 'user'];

export const prepareRelPersonPlaceForEncryption = (relPersonPlace, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields && !relPersonPlace.deletedAt) {
if (!!checkRequiredFields) {
try {
if (!looseUuidRegex.test(relPersonPlace.person)) {
throw new Error('RelPersonPlace is missing person');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/rencontres.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const rencontresState = atom({
const encryptedFields = ['person', 'team', 'user', 'date', 'comment'];

export const prepareRencontreForEncryption = (rencontre, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields && !rencontre.deletedAt) {
if (!!checkRequiredFields) {
try {
if (!looseUuidRegex.test(rencontre.person)) {
throw new Error('Rencontre is missing person');
Expand Down
Loading

0 comments on commit 9e2faf3

Please sign in to comment.