feat: Support IAM role authentication for S3 (#2830)

closes #2829
This commit is contained in:
Zero King
2021-12-11 01:08:03 +00:00
committed by GitHub
parent 05b9ae3e63
commit 11e14bc4f5
12 changed files with 44 additions and 19 deletions

View File

@@ -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",

View File

@@ -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(),
};

View File

@@ -6,6 +6,7 @@ import documentPermanentDeleter from "./documentPermanentDeleter";
jest.mock("aws-sdk", () => {
const mS3 = {
createPresignedPost: jest.fn(),
deleteObject: jest.fn().mockReturnThis(),
promise: jest.fn(),
};

View File

@@ -5,6 +5,7 @@ import fileOperationDeleter from "./fileOperationDeleter";
jest.mock("aws-sdk", () => {
const mS3 = {
createPresignedPost: jest.fn(),
deleteObject: jest.fn().mockReturnThis(),
promise: jest.fn(),
};

View File

@@ -4,6 +4,7 @@ import teamCreator from "./teamCreator";
jest.mock("aws-sdk", () => {
const mS3 = {
createPresignedPost: jest.fn(),
putObject: jest.fn().mockReturnThis(),
promise: jest.fn(),
};

View File

@@ -11,6 +11,7 @@ import teamPermanentDeleter from "./teamPermanentDeleter";
jest.mock("aws-sdk", () => {
const mS3 = {
createPresignedPost: jest.fn(),
deleteObject: jest.fn().mockReturnThis(),
promise: jest.fn(),
};

View File

@@ -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(),
};

View File

@@ -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,

View File

@@ -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(),
};

View File

@@ -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(),
};

View File

@@ -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

View File

@@ -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"