diff --git a/lib/core/buckets/helpers/uploadValidation.ts b/lib/core/buckets/helpers/uploadValidation.ts new file mode 100644 index 00000000..0234c905 --- /dev/null +++ b/lib/core/buckets/helpers/uploadValidation.ts @@ -0,0 +1,19 @@ +import semver from "semver"; +import { getContext } from "../../../requestContext"; + +const EXEMPT_CLIENTS: Record = { + "internxt-cli": "1.6.3", + "drive-desktop-windows": "2.6.6", + // All versions + "drive-desktop-linux": '*', +}; + +export function shouldEnforceUploadValidation(): boolean { + const { clientId, version } = getContext(); + if (!clientId || !version) return true; + if (!(clientId in EXEMPT_CLIENTS)) return true; + + const maxExemptVersion = EXEMPT_CLIENTS[clientId]; + const coerced = semver.coerce(version) ?? version; + return !semver.satisfies(coerced, `<=${maxExemptVersion}`); +} diff --git a/lib/core/buckets/usecase.ts b/lib/core/buckets/usecase.ts index f69c0b88..34ba4640 100644 --- a/lib/core/buckets/usecase.ts +++ b/lib/core/buckets/usecase.ts @@ -28,6 +28,7 @@ import { ContactsRepository } from '../contacts/Repository'; import { StorageGateway } from '../storage/StorageGateway'; import { Contact } from '../contacts/Contact'; import { Upload } from '../uploads/Upload'; +import { shouldEnforceUploadValidation } from './helpers/uploadValidation'; export class BucketEntryNotFoundError extends Error { constructor(bucketEntryId?: string) { @@ -679,19 +680,19 @@ export class BucketsUsecase { if (contact.objectCheckNotRequired) { contactsThatStoreTheShard.push(contact); } else { - await this.validateObjectInStorage(contact, uuid, data_size).catch((error) => { + try { + await this.validateObjectInStorage(contact, uuid, data_size); + } catch (error) { if (error instanceof UploadSizeDoesNotMatchError) { log.error(`[finishUpload][SizeDoesNotMatchError] ${JSON.stringify({ uuid, expectedSize: data_size, contactId: contact.id, message: error.message, isMultipartUpload })}`); throw error; - } - - if (error instanceof UploadNotFoundInStorageError) { + } else if (error instanceof UploadNotFoundInStorageError) { log.error(`[finishUpload][UploadNotFoundInStorageError] ${JSON.stringify({ uuid, contactId: contact.id, error: error.message, stack: error.stack, isMultipartUpload, size: data_size })}`); - return; + } else { + log.error(`[finishUpload][unexpectedError] Error getting bucket meta ${JSON.stringify({ uuid, contactId: contact.id, error: (error as Error).message, stack: (error as Error).stack })}`); } - - log.error(`[finishUpload][unexpectedError] Error getting bucket meta ${JSON.stringify({ uuid, contactId: contact.id, error: error.message, stack: error.stack })}`); - }); + if (shouldEnforceUploadValidation()) throw error; + } contactsThatStoreTheShard.push(contact); } } diff --git a/package.json b/package.json index 75830ceb..51025a97 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@types/node": "^17.0.23", "@types/node-mongodb-fixtures": "^3.2.3", "@types/secp256k1": "^4.0.6", + "@types/semver": "^7.7.1", "@types/sinon": "^10.0.11", "@types/supertest": "^2.0.12", "@types/uuid": "^8.3.4", @@ -118,6 +119,7 @@ "rc": "^1.2.8", "redis": "^3.1.0", "secp256k1": "^4.0.2", + "semver": "^7.7.4", "storj-lib": "github:internxt/core#v8.7.3-beta", "storj-mongodb-adapter": "github:internxt/mongodb-adapter#10.1.0-beta", "storj-service-error-types": "github:internxt/service-error-types", diff --git a/tests/lib/core/buckets/helpers/uploadValidation.test.ts b/tests/lib/core/buckets/helpers/uploadValidation.test.ts new file mode 100644 index 00000000..1b824b99 --- /dev/null +++ b/tests/lib/core/buckets/helpers/uploadValidation.test.ts @@ -0,0 +1,92 @@ +import * as requestContext from '../../../../../lib/requestContext'; +import { shouldEnforceUploadValidation } from '../../../../../lib/core/buckets/helpers/uploadValidation'; + +describe('shouldEnforceUploadValidation()', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + const mockContext = (clientId: string | undefined, version: string | undefined) => { + jest.spyOn(requestContext, 'getContext').mockReturnValue({ clientId, version }); + }; + + describe('When there are no client headers', () => { + it('When clientId is missing, then it should enforce', () => { + mockContext(undefined, '1.0.0'); + expect(shouldEnforceUploadValidation()).toBe(true); + }); + + it('When version is missing, then it should enforce', () => { + mockContext('internxt-cli', undefined); + expect(shouldEnforceUploadValidation()).toBe(true); + }); + + it('When both are missing, then it should enforce', () => { + mockContext(undefined, undefined); + expect(shouldEnforceUploadValidation()).toBe(true); + }); + }); + + describe('When the client is not in the exempt list', () => { + it('When called with an unknown client, then it should enforce', () => { + mockContext('some-unknown-client', '9.9.9'); + expect(shouldEnforceUploadValidation()).toBe(true); + }); + }); + + describe('When the client is drive-desktop-linux (all versions exempt)', () => { + it('When called with any version, then it should not enforce', () => { + mockContext('drive-desktop-linux', '0.0.1'); + expect(shouldEnforceUploadValidation()).toBe(false); + }); + + it('When called with a high version, then it should not enforce', () => { + mockContext('drive-desktop-linux', '99.99.99'); + expect(shouldEnforceUploadValidation()).toBe(false); + }); + }); + + describe('When the client is internxt-cli (exempt up to 1.6.3)', () => { + it('When version is below the exempt threshold, then it should not enforce', () => { + mockContext('internxt-cli', '1.6.2'); + expect(shouldEnforceUploadValidation()).toBe(false); + }); + + it('When version equals the exempt threshold, then it should not enforce', () => { + mockContext('internxt-cli', '1.6.3'); + expect(shouldEnforceUploadValidation()).toBe(false); + }); + + it('When version is above the exempt threshold, then it should enforce', () => { + mockContext('internxt-cli', '1.6.4'); + expect(shouldEnforceUploadValidation()).toBe(true); + }); + + it('When minor version is above the exempt threshold, then it should enforce', () => { + mockContext('internxt-cli', '1.7.0'); + expect(shouldEnforceUploadValidation()).toBe(true); + }); + + it('When major version is above the exempt threshold, then it should enforce', () => { + mockContext('internxt-cli', '2.0.0'); + expect(shouldEnforceUploadValidation()).toBe(true); + }); + }); + + describe('When the client is drive-desktop-windows (exempt up to 2.6.6)', () => { + it('When version is below the exempt threshold, then it should not enforce', () => { + mockContext('drive-desktop-windows', '2.6.5'); + expect(shouldEnforceUploadValidation()).toBe(false); + }); + + it('When version equals the exempt threshold, then it should not enforce', () => { + mockContext('drive-desktop-windows', '2.6.6'); + expect(shouldEnforceUploadValidation()).toBe(false); + }); + + it('When version is above the exempt threshold, then it should enforce', () => { + mockContext('drive-desktop-windows', '2.6.7'); + expect(shouldEnforceUploadValidation()).toBe(true); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 6e7306a5..f986a65f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1808,6 +1808,11 @@ dependencies: "@types/node" "*" +"@types/semver@^7.7.1": + version "7.7.1" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528" + integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA== + "@types/send@*": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74" @@ -8149,7 +8154,7 @@ semver-store@^0.3.0: resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9" integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg== -semver@7.7.4, semver@^7.2.1, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.7.3: +semver@7.7.4, semver@^7.2.1, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.7.3, semver@^7.7.4: version "7.7.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==