diff --git a/packages/actions/test/unit/security.test.ts b/packages/actions/test/unit/security.test.ts index 613bb21b..9a1773ea 100644 --- a/packages/actions/test/unit/security.test.ts +++ b/packages/actions/test/unit/security.test.ts @@ -28,13 +28,13 @@ import { cleanUpMockParticipant, getStorageConfiguration, sleep, - deleteBucket + deleteBucket, + deleteObjectFromS3 } from "../utils" import { commonTerms, formatZkeyIndex, generateGetObjectPreSignedUrl, - genesisZkeyIndex, getBucketName, getCurrentFirebaseAuthUser, getZkeyStorageFilePath, @@ -272,6 +272,7 @@ describe("Security", () => { const ceremonyContributor = fakeCeremoniesData.fakeCeremonyOpenedDynamic const circuitsNotCurrentContributor = fakeCircuitsData.fakeCircuitSmallContributors const bucketName = getBucketName(ceremonyContributor.data.prefix!, ceremonyBucketPostfix) + let storagePath: string = "" beforeAll(async () => { // we need the pre conditions to meet await createMockCeremony(adminFirestore, ceremonyNotContributor, circuitsNotCurrentContributor) @@ -281,6 +282,10 @@ describe("Security", () => { await signInWithEmailAndPassword(userAuth, users[2].data.email, passwords[2]) await createS3Bucket(userFunctions, bucketName) await sleep(2000) + storagePath = getZkeyStorageFilePath( + circuitsCurrentContributor.data.prefix!, + `${circuitsCurrentContributor.data.prefix}_${formatZkeyIndex(1)}.zkey` + ) }) afterAll(async () => { @@ -289,6 +294,7 @@ describe("Security", () => { await cleanUpMockCeremony(adminFirestore, ceremonyContributor.uid, circuitsCurrentContributor.uid) await cleanUpMockParticipant(adminFirestore, ceremonyNotContributor.uid, users[0].uid) await cleanUpMockParticipant(adminFirestore, ceremonyContributor.uid, users[0].uid) + await deleteObjectFromS3(bucketName, storagePath) await deleteBucket(bucketName) }) @@ -309,17 +315,8 @@ describe("Security", () => { } }) - await expect( - openMultiPartUpload( - userFunctions, - getBucketName(ceremonyContributor.data.prefix!, ceremonyBucketPostfix), - getZkeyStorageFilePath( - circuitsCurrentContributor.data.prefix!, - `${circuitsCurrentContributor.data.prefix}_${formatZkeyIndex(1)}.zkey` - ), - ceremonyContributor.uid - ) - ).to.be.fulfilled + await expect(openMultiPartUpload(userFunctions, bucketName, storagePath, ceremonyContributor.uid)).to.be + .fulfilled }) it("should revert when the user is not a contributor for this ceremony circuit", async () => { await signInWithEmailAndPassword(userAuth, users[0].data.email, passwords[0]) @@ -327,22 +324,45 @@ describe("Security", () => { openMultiPartUpload( userFunctions, getBucketName(ceremonyNotContributor.data.prefix!, ceremonyBucketPostfix), - `${circuitsNotCurrentContributor.data.prefix}_${genesisZkeyIndex}.zkey`, + storagePath, ceremonyNotContributor.uid ) ).to.be.rejectedWith( "Unable to interact with a multi-part upload (start, create pre-signed urls or complete)." ) }) - it("should fail when the user is trying to upload a file to a bucket not part of a ceremony", async () => { - await signInWithEmailAndPassword(userAuth, users[0].data.email, passwords[0]) + it("should revert when the user is trying to upload a file with the wrong storage path", async () => { + await adminFirestore + .collection(getCircuitsCollectionPath(ceremonyContributor.uid)) + .doc(circuitsCurrentContributor.uid) + .set({ + prefix: circuitsCurrentContributor.data.prefix, + waitingQueue: { + completedContributions: 0, + contributors: [users[0].uid, users[1].uid], + currentContributor: users[0].uid, // fake user 1 + failedContributions: 0 + } + }) + await expect( openMultiPartUpload( userFunctions, - "not-a-ceremony-bucket", - `${circuitsNotCurrentContributor.data.prefix}_${formatZkeyIndex(1)}.zkey`, - ceremonyNotContributor.uid + getBucketName(ceremonyContributor.data.prefix!, ceremonyBucketPostfix), + getZkeyStorageFilePath( + circuitsCurrentContributor.data.prefix!, + `${circuitsCurrentContributor.data.prefix}_${formatZkeyIndex(2)}.zkey` + ), + ceremonyContributor.uid ) + ).to.be.rejectedWith( + "Unable to interact with a multi-part upload (start, create pre-signed urls or complete)." + ) + }) + it("should fail when the user is trying to upload a file to a bucket not part of a ceremony", async () => { + await signInWithEmailAndPassword(userAuth, users[0].data.email, passwords[0]) + await expect( + openMultiPartUpload(userFunctions, "not-a-ceremony-bucket", storagePath, ceremonyNotContributor.uid) // @todo discuss whether this error name should be changed to be more general? ).to.be.rejectedWith("Unable to generate a pre-signed url for the given object in the provided bucket.") }) diff --git a/packages/backend/src/functions/storage.ts b/packages/backend/src/functions/storage.ts index 8e254355..b0aec35b 100644 --- a/packages/backend/src/functions/storage.ts +++ b/packages/backend/src/functions/storage.ts @@ -63,35 +63,38 @@ const checkUploadingFileValidity = async (contributorId: string, ceremonyId: str // Get the circuits for the ceremony const circuits = await getCeremonyCircuits(ceremonyId) - // We need to have at least 1 circuit - if (circuits.length === 0) logAndThrowError(SPECIFIC_ERRORS.SE_CONTRIBUTE_NO_CEREMONY_CIRCUITS) - - // Loop through the circuits until we find the one we are contributing to - for (const circuit of circuits) { - // Extract the data we need - const { prefix, waitingQueue } = circuit.data()! - const { completedContributions, currentContributor } = waitingQueue - - // If we are not a contributor to this circuit, continue looping - if (currentContributor === contributorId) { - // Get the index of the zKey - const contributorZKeyIndex = formatZkeyIndex(completedContributions + 1) - // The uploaded file must be the expected one - const zkeyNameContributor = `${prefix}_${contributorZKeyIndex}.zkey` - const contributorZKeyStoragePath = getZkeyStorageFilePath(prefix, zkeyNameContributor) - - // If the object key is not one of the two zkeys, throw an error - if (objectKey !== contributorZKeyStoragePath) { - logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD) - } + // Get the participant document + const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), contributorId!) + const participantData = participantDoc.data() - // void return if we found a match and the contributor can upload the zkey - return + if (!participantData) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA) + + // The index of the circuit will be the contribution progress - 1 + const index = participantData?.contributionProgress + // If the index is zero the user is not the current contributor + if (index === 0) logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD) + // We can safely use index - 1 + const circuit = circuits.at(index - 1) + + // If the circuit is undefined, throw an error + if (circuit === undefined) logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD) + // Extract the data we need + const { prefix, waitingQueue } = circuit!.data() + const { completedContributions, currentContributor } = waitingQueue + + // If we are not a contributor to this circuit then we cannot upload files + if (currentContributor === contributorId) { + // Get the index of the zKey + const contributorZKeyIndex = formatZkeyIndex(completedContributions + 1) + // The uploaded file must be the expected one + const zkeyNameContributor = `${prefix}_${contributorZKeyIndex}.zkey` + const contributorZKeyStoragePath = getZkeyStorageFilePath(prefix, zkeyNameContributor) + + // If the object key is not one of the two zkeys, throw an error + if (objectKey !== contributorZKeyStoragePath) { + logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_WRONG_OBJECT_KEY) } - } - - // if there was no match for the current contributor, then throw an error - logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD) + } else logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD) } /** diff --git a/packages/backend/src/lib/errors.ts b/packages/backend/src/lib/errors.ts index 6574171d..11a3e479 100644 --- a/packages/backend/src/lib/errors.ts +++ b/packages/backend/src/lib/errors.ts @@ -85,6 +85,11 @@ export const SPECIFIC_ERRORS = { "Unable to generate a pre-signed url for the given object in the provided bucket.", "The bucket is not associated with any valid ceremony document on the Firestore database." ), + SE_STORAGE_WRONG_OBJECT_KEY: makeError( + "failed-precondition", + "Unable to interact with a multi-part upload (start, create pre-signed urls or complete).", + "The object key provided does not match the expected one." + ), SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD: makeError( "failed-precondition", "Unable to interact with a multi-part upload (start, create pre-signed urls or complete).",