Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function autoDeleteHandler(event: AWSLambda.CloudFormationCustomRes
const response = await onUpdate(event);
return { PhysicalResourceId: response.PhysicalResourceId };
case 'Delete':
return onDelete(event.ResourceProperties?.BucketName);
return onDelete(event.ResourceProperties?.BucketName, event.ServiceToken);
}
}

Expand All @@ -39,8 +39,11 @@ async function onUpdate(event: AWSLambda.CloudFormationCustomResourceEvent) {
* Set a write deny policy to prevent new object creation while we're emptying the bucket.
*
* @param bucketName the bucket name
* @param serviceToken this handler's own Lambda ARN, used to derive the partition for the deny policy
*/
async function denyWrites(bucketName: string) {
async function denyWrites(bucketName: string, serviceToken: string) {
// ServiceToken is this Lambda's ARN — same partition as the bucket.
const partition = serviceToken.split(':')[1];
try {
const prevPolicyJson = (await s3.getBucketPolicy({ Bucket: bucketName }))?.Policy ?? S3_POLICY_STUB;
const policy = JSON.parse(prevPolicyJson);
Expand All @@ -50,9 +53,7 @@ async function denyWrites(bucketName: string) {
Principal: '*',
Effect: 'Deny',
Action: ['s3:PutObject'],
// TODO: this is probably an error of some sort
// eslint-disable-next-line @cdklabs/no-literal-partition
Resource: [`arn:aws:s3:::${bucketName}/*`],
Resource: [`arn:${partition}:s3:::${bucketName}/*`],
},
);

Expand Down Expand Up @@ -89,7 +90,7 @@ async function emptyBucket(bucketName: string) {
} while (listedObjects?.IsTruncated);
}

async function onDelete(bucketName?: string) {
async function onDelete(bucketName: string | undefined, serviceToken: string) {
if (!bucketName) {
throw new Error('No BucketName was provided.');
}
Expand All @@ -98,7 +99,7 @@ async function onDelete(bucketName?: string) {
console.log(`Bucket does not have '${AUTO_DELETE_OBJECTS_TAG}' tag, skipping cleaning.`);
return;
}
await denyWrites(bucketName);
await denyWrites(bucketName, serviceToken);
await emptyBucket(bucketName);
} catch (error: any) {
// Bucket doesn't exist, all is well
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ beforeEach(() => {
});

const BUCKET_DENY_POLICY = '{"Version":"2012-10-17","Statement":[{"Principal":"arn:iam:stackowner","Effect":"Allow","Action":["s3:PutBucketPolicy","s3:GetBucket*","s3:List*","s3:DeleteObject*"],"Resource":["arn:aws:s3:::MyBucket/*"]},{"Principal":"*","Effect":"Deny","Action":["s3:PutObject"],"Resource":["arn:aws:s3:::MyBucket/*"]}]}';
const SERVICE_TOKEN_ARN = 'arn:aws:lambda:us-east-1:123456789012:function:my-fn';
const CN_SERVICE_TOKEN_ARN = 'arn:aws-cn:lambda:cn-north-1:123456789012:function:my-fn';

afterEach(() => {
jest.resetAllMocks();
Expand Down Expand Up @@ -206,6 +208,7 @@ test('deletes no objects on delete event when bucket has no objects', async () =
// WHEN
const event: Partial<AWSLambda.CloudFormationCustomResourceDeleteEvent> = {
RequestType: 'Delete',
ServiceToken: SERVICE_TOKEN_ARN,
ResourceProperties: {
ServiceToken: 'Foo',
BucketName: 'MyBucket',
Expand Down Expand Up @@ -237,6 +240,7 @@ test('deletes all objects on delete event', async () => {
// WHEN
const event: Partial<AWSLambda.CloudFormationCustomResourceDeleteEvent> = {
RequestType: 'Delete',
ServiceToken: SERVICE_TOKEN_ARN,
ResourceProperties: {
ServiceToken: 'Foo',
BucketName: 'MyBucket',
Expand Down Expand Up @@ -265,6 +269,30 @@ test('deletes all objects on delete event', async () => {
});
});

test('deny policy ARN uses the partition from the ServiceToken (aws-cn)', async () => {
// GIVEN
mockS3Client.listObjectVersions.mockResolvedValue({ Versions: [] });

// WHEN
const event: Partial<AWSLambda.CloudFormationCustomResourceDeleteEvent> = {
RequestType: 'Delete',
ServiceToken: CN_SERVICE_TOKEN_ARN,
ResourceProperties: {
ServiceToken: 'Foo',
BucketName: 'MyBucket',
},
};
await invokeHandler(event);

// THEN
const cnDenyPolicy = '{"Version":"2012-10-17","Statement":[{"Principal":"arn:iam:stackowner","Effect":"Allow","Action":["s3:PutBucketPolicy","s3:GetBucket*","s3:List*","s3:DeleteObject*"],"Resource":["arn:aws:s3:::MyBucket/*"]},{"Principal":"*","Effect":"Deny","Action":["s3:PutObject"],"Resource":["arn:aws-cn:s3:::MyBucket/*"]}]}';
expect(mockS3Client.putBucketPolicy).toHaveBeenCalledTimes(1);
expect(mockS3Client.putBucketPolicy).toHaveBeenCalledWith({
Bucket: 'MyBucket',
Policy: cnDenyPolicy,
});
});

test('deletes all objects on delete event when bucket has no existing policy', async () => {
// GIVEN
mockS3Client.getBucketPolicy.mockClear();
Expand All @@ -279,6 +307,7 @@ test('deletes all objects on delete event when bucket has no existing policy', a
// WHEN
const event: Partial<AWSLambda.CloudFormationCustomResourceDeleteEvent> = {
RequestType: 'Delete',
ServiceToken: SERVICE_TOKEN_ARN,
ResourceProperties: {
ServiceToken: 'Foo',
BucketName: 'MyBucket',
Expand Down Expand Up @@ -328,6 +357,7 @@ test('deletes all objects on delete event even when deny policy assignment fails
// WHEN
const event: Partial<AWSLambda.CloudFormationCustomResourceDeleteEvent> = {
RequestType: 'Delete',
ServiceToken: SERVICE_TOKEN_ARN,
ResourceProperties: {
ServiceToken: 'Foo',
BucketName: 'MyBucket',
Expand Down Expand Up @@ -373,6 +403,7 @@ test('deletes all objects on delete event even when bucket policy cannot be read
// WHEN
const event: Partial<AWSLambda.CloudFormationCustomResourceDeleteEvent> = {
RequestType: 'Delete',
ServiceToken: SERVICE_TOKEN_ARN,
ResourceProperties: {
ServiceToken: 'Foo',
BucketName: 'MyBucket',
Expand Down Expand Up @@ -410,6 +441,7 @@ test('does not empty bucket if it is not tagged', async () => {
// WHEN
const event: Partial<AWSLambda.CloudFormationCustomResourceDeleteEvent> = {
RequestType: 'Delete',
ServiceToken: SERVICE_TOKEN_ARN,
ResourceProperties: {
ServiceToken: 'Foo',
BucketName: 'MyBucket',
Expand Down Expand Up @@ -442,6 +474,7 @@ test('delete event where bucket has many objects does recurse appropriately', as
// WHEN
const event: Partial<AWSLambda.CloudFormationCustomResourceDeleteEvent> = {
RequestType: 'Delete',
ServiceToken: SERVICE_TOKEN_ARN,
ResourceProperties: {
ServiceToken: 'Foo',
BucketName: 'MyBucket',
Expand Down Expand Up @@ -492,6 +525,7 @@ test('does nothing when the bucket does not exist', async () => {
// WHEN
const event: Partial<AWSLambda.CloudFormationCustomResourceDeleteEvent> = {
RequestType: 'Delete',
ServiceToken: SERVICE_TOKEN_ARN,
ResourceProperties: {
ServiceToken: 'Foo',
BucketName: 'MyBucket',
Expand Down
Loading