From 11e14bc4f5582550b69a0dabfde6adb611b28707 Mon Sep 17 00:00:00 2001 From: Zero King Date: Sat, 11 Dec 2021 01:08:03 +0000 Subject: [PATCH] feat: Support IAM role authentication for S3 (#2830) closes #2829 --- package.json | 2 +- server/commands/accountProvisioner.test.ts | 1 + .../commands/documentPermanentDeleter.test.ts | 1 + server/commands/fileOperationDeleter.test.ts | 1 + server/commands/teamCreator.test.ts | 1 + server/commands/teamPermanentDeleter.test.ts | 1 + server/routes/api/attachments.test.ts | 1 + server/routes/api/attachments.ts | 17 ++--------- server/routes/api/fileOperations.test.ts | 1 + server/routes/api/utils.test.ts | 1 + server/utils/s3.ts | 28 +++++++++++++++++++ yarn.lock | 8 +++--- 12 files changed, 44 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 70835e3ef..d9594e947 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@tippy.js/react": "^2.2.2", "@tommoor/remove-markdown": "^0.3.2", "autotrack": "^2.4.1", - "aws-sdk": "^2.831.0", + "aws-sdk": "^2.1044.0", "babel-plugin-lodash": "^3.3.4", "babel-plugin-styled-components": "^1.11.1", "babel-plugin-transform-class-properties": "^6.24.1", diff --git a/server/commands/accountProvisioner.test.ts b/server/commands/accountProvisioner.test.ts index f15f9e4b5..3c2460406 100644 --- a/server/commands/accountProvisioner.test.ts +++ b/server/commands/accountProvisioner.test.ts @@ -7,6 +7,7 @@ import accountProvisioner from "./accountProvisioner"; jest.mock("../mailer"); jest.mock("aws-sdk", () => { const mS3 = { + createPresignedPost: jest.fn(), putObject: jest.fn().mockReturnThis(), promise: jest.fn(), }; diff --git a/server/commands/documentPermanentDeleter.test.ts b/server/commands/documentPermanentDeleter.test.ts index c2a025fdc..f0168ea87 100644 --- a/server/commands/documentPermanentDeleter.test.ts +++ b/server/commands/documentPermanentDeleter.test.ts @@ -6,6 +6,7 @@ import documentPermanentDeleter from "./documentPermanentDeleter"; jest.mock("aws-sdk", () => { const mS3 = { + createPresignedPost: jest.fn(), deleteObject: jest.fn().mockReturnThis(), promise: jest.fn(), }; diff --git a/server/commands/fileOperationDeleter.test.ts b/server/commands/fileOperationDeleter.test.ts index df886fc26..105270d40 100644 --- a/server/commands/fileOperationDeleter.test.ts +++ b/server/commands/fileOperationDeleter.test.ts @@ -5,6 +5,7 @@ import fileOperationDeleter from "./fileOperationDeleter"; jest.mock("aws-sdk", () => { const mS3 = { + createPresignedPost: jest.fn(), deleteObject: jest.fn().mockReturnThis(), promise: jest.fn(), }; diff --git a/server/commands/teamCreator.test.ts b/server/commands/teamCreator.test.ts index 38c8ef864..9bb0a61a1 100644 --- a/server/commands/teamCreator.test.ts +++ b/server/commands/teamCreator.test.ts @@ -4,6 +4,7 @@ import teamCreator from "./teamCreator"; jest.mock("aws-sdk", () => { const mS3 = { + createPresignedPost: jest.fn(), putObject: jest.fn().mockReturnThis(), promise: jest.fn(), }; diff --git a/server/commands/teamPermanentDeleter.test.ts b/server/commands/teamPermanentDeleter.test.ts index 2bd0ccd60..87c36955b 100644 --- a/server/commands/teamPermanentDeleter.test.ts +++ b/server/commands/teamPermanentDeleter.test.ts @@ -11,6 +11,7 @@ import teamPermanentDeleter from "./teamPermanentDeleter"; jest.mock("aws-sdk", () => { const mS3 = { + createPresignedPost: jest.fn(), deleteObject: jest.fn().mockReturnThis(), promise: jest.fn(), }; diff --git a/server/routes/api/attachments.test.ts b/server/routes/api/attachments.test.ts index 1de3ce330..e022da392 100644 --- a/server/routes/api/attachments.test.ts +++ b/server/routes/api/attachments.test.ts @@ -15,6 +15,7 @@ const app = webService(); const server = new TestServer(app.callback()); jest.mock("aws-sdk", () => { const mS3 = { + createPresignedPost: jest.fn(), deleteObject: jest.fn().mockReturnThis(), promise: jest.fn(), }; diff --git a/server/routes/api/attachments.ts b/server/routes/api/attachments.ts index e6b15f3a1..2530b830c 100644 --- a/server/routes/api/attachments.ts +++ b/server/routes/api/attachments.ts @@ -1,4 +1,3 @@ -import { format } from "date-fns"; import Router from "koa-router"; import { v4 as uuidv4 } from "uuid"; import { NotFoundError } from "@server/errors"; @@ -6,10 +5,8 @@ import auth from "@server/middlewares/authentication"; import { Attachment, Document, Event } from "@server/models"; import policy from "@server/policies"; import { - makePolicy, - getSignature, + getPresignedPost, publicS3Endpoint, - makeCredential, getSignedUrl, } from "@server/utils/s3"; import { assertPresent } from "@server/validation"; @@ -34,9 +31,7 @@ router.post("attachments.create", auth(), async (ctx) => { : "private"; const bucket = acl === "public-read" ? "public" : "uploads"; const key = `${bucket}/${user.id}/${s3Key}/${name}`; - const credential = makeCredential(); - const longDate = format(new Date(), "yyyyMMdd'T'HHmmss'Z'"); - const policy = makePolicy(credential, longDate, acl, contentType); + const presignedPost = await getPresignedPost(key, acl, contentType); const endpoint = publicS3Endpoint(); const url = `${endpoint}/${key}`; @@ -73,13 +68,7 @@ router.post("attachments.create", auth(), async (ctx) => { form: { "Cache-Control": "max-age=31557600", "Content-Type": contentType, - acl, - key, - policy, - "x-amz-algorithm": "AWS4-HMAC-SHA256", - "x-amz-credential": credential, - "x-amz-date": longDate, - "x-amz-signature": getSignature(policy), + ...presignedPost.fields, }, attachment: { documentId, diff --git a/server/routes/api/fileOperations.test.ts b/server/routes/api/fileOperations.test.ts index fff568e25..441750a82 100644 --- a/server/routes/api/fileOperations.test.ts +++ b/server/routes/api/fileOperations.test.ts @@ -15,6 +15,7 @@ const app = webService(); const server = new TestServer(app.callback()); jest.mock("aws-sdk", () => { const mS3 = { + createPresignedPost: jest.fn(), deleteObject: jest.fn().mockReturnThis(), promise: jest.fn(), }; diff --git a/server/routes/api/utils.test.ts b/server/routes/api/utils.test.ts index 0da2f709c..b2736a37f 100644 --- a/server/routes/api/utils.test.ts +++ b/server/routes/api/utils.test.ts @@ -11,6 +11,7 @@ const app = webService(); const server = new TestServer(app.callback()); jest.mock("aws-sdk", () => { const mS3 = { + createPresignedPost: jest.fn(), deleteObject: jest.fn().mockReturnThis(), promise: jest.fn(), }; diff --git a/server/utils/s3.ts b/server/utils/s3.ts index d2e109dd0..5a1ea2bbf 100644 --- a/server/utils/s3.ts +++ b/server/utils/s3.ts @@ -1,4 +1,5 @@ import crypto from "crypto"; +import util from "util"; import AWS from "aws-sdk"; import { addHours, format } from "date-fns"; import fetch from "fetch-with-proxy"; @@ -24,6 +25,10 @@ const s3 = new AWS.S3({ new AWS.Endpoint(process.env.AWS_S3_UPLOAD_BUCKET_URL), signatureVersion: "v4", }); +const createPresignedPost = util.promisify< + AWS.S3.PresignedPost.Params, + AWS.S3.PresignedPost +>(s3.createPresignedPost); const hmac = ( key: string | Buffer, @@ -93,6 +98,29 @@ export const getSignature = (policy: string) => { return signature; }; +export const getPresignedPost = ( + key: string, + acl: string, + contentType = "image" +) => { + const params = { + Bucket: process.env.AWS_S3_UPLOAD_BUCKET_NAME, + Conditions: [ + // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. + ["content-length-range", 0, +process.env.AWS_S3_UPLOAD_MAX_SIZE], + ["starts-with", "$Content-Type", contentType], + ["starts-with", "$Cache-Control", ""], + ], + Fields: { + key, + acl, + }, + Expires: 3600, + }; + + return createPresignedPost(params); +}; + export const publicS3Endpoint = (isServerUpload?: boolean) => { // lose trailing slash if there is one and convert fake-s3 url to localhost // for access outside of docker containers in local development diff --git a/yarn.lock b/yarn.lock index 6d5998b55..e50144846 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4060,10 +4060,10 @@ autotrack@^2.4.1: rollup-plugin-node-resolve "^3.0.0" source-map "^0.5.6" -aws-sdk@^2.831.0: - version "2.831.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.831.0.tgz#02607cc911a2136e5aabe624c1282e821830aef2" - integrity sha512-lrOjbGFpjk2xpESyUx2PGsTZgptCy5xycZazPeakNbFO19cOoxjHx3xyxOHsMCYb3pQwns35UvChQT60B4u6cw== +aws-sdk@^2.1044.0: + version "2.1044.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1044.0.tgz#0708eaf48daf8d961b414e698d84e8cd1f82c4ad" + integrity sha512-n55uGUONQGXteGGG1QlZ1rKx447KSuV/x6jUGNf2nOl41qMI8ZgLUhNUt0uOtw3qJrCTanzCyR/JKBq2PMiqEQ== dependencies: buffer "4.9.2" events "1.1.1"