-
Notifications
You must be signed in to change notification settings - Fork 21
feat: rplt 828 bucket policy update #11553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
5bff2d8
0db9c82
66beb92
737e800
11ee41e
76fc57b
3d4cd88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { Construct } from 'constructs' | ||
import { Stack, aws_s3, aws_lambda, aws_iam, CustomResource, custom_resources, aws_logs, Duration } from 'aws-cdk-lib' | ||
import { BucketNames } from './create-S3-bucket' | ||
|
||
export class ResolveS3BucketPolicyConditionsCustomResource extends Construct { | ||
constructor( | ||
scope: Stack, | ||
id: string, | ||
{ | ||
buckets, | ||
iaasAccountId, | ||
paasAcountId, | ||
}: { buckets: Record<string, aws_s3.IBucket>; iaasAccountId: string; paasAcountId: string }, | ||
) { | ||
super(scope, id) | ||
|
||
const envStage = process.env.APP_STAGE === 'production' ? 'prod' : 'dev' | ||
|
||
const liveBucket = aws_s3.Bucket.fromBucketName(this, 'lookup-live-bucket', `${BucketNames.LIVE}-${envStage}`) | ||
const logBucket = aws_s3.Bucket.fromBucketName(this, 'lookup-log-bucket', `${BucketNames.LOG}-${envStage}`) | ||
const repoBucket = aws_s3.Bucket.fromBucketName(this, 'lookup-repo-bucket', `${BucketNames.REPO_CACHE}-${envStage}`) | ||
const versionBucket = aws_s3.Bucket.fromBucketName( | ||
this, | ||
'lookup-version-bucket', | ||
`${BucketNames.VERSION}-${envStage}`, | ||
) | ||
|
||
const resolveProductionS3Lambda = new aws_lambda.Function( | ||
scope, | ||
'resolve-S3-bucket-policy-conditions-custom-resource', | ||
{ | ||
handler: 'dist/resolve-s3-bucket-policy-conditions.resolveS3BucketPolicyConditions', | ||
code: aws_lambda.Code.fromAsset('bundle/resolve-s3-bucket-policy-conditions.zip'), | ||
memorySize: 1024, | ||
timeout: Duration.seconds(60), | ||
runtime: aws_lambda.Runtime.NODEJS_18_X, | ||
environment: { | ||
PAAS_ACCOUNT_ID: paasAcountId, | ||
IAAS_ACCOUNT_ID: iaasAccountId, | ||
BUCKETS: [liveBucket.bucketName, logBucket.bucketName, repoBucket.bucketName, versionBucket.bucketName].join( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it won't re-run if you change the env vars btw, don't know if you care about that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. naa all good +1 |
||
',', | ||
), | ||
}, | ||
}, | ||
) | ||
|
||
resolveProductionS3Lambda.addToRolePolicy( | ||
new aws_iam.PolicyStatement({ | ||
effect: aws_iam.Effect.ALLOW, | ||
resources: Object.values(buckets).map((bucket) => bucket.bucketArn), | ||
actions: [ | ||
's3:ListBucket', | ||
's3:PutObjectAcl', | ||
's3:GetBucketAcl', | ||
's3:GetObjectAcl', | ||
's3:GetBucketLocation', | ||
's3:GetObjectRetention', | ||
's3:GetObjectVersionAcl', | ||
'S3:PutBucketPolicy', | ||
'S3:GetBucketPolicy', | ||
's3:DeleteBucketPolicy', | ||
's3:PutBucketPolicy', | ||
's3:PutBucketOwnershipControls', | ||
's3:PutBucketACL', | ||
's3:PutBucketPublicAccessBlock', | ||
], | ||
}), | ||
) | ||
|
||
const resourceProvider = new custom_resources.Provider(scope, 'resolve-S3-bucket-policy-conditions', { | ||
onEventHandler: resolveProductionS3Lambda, | ||
logRetention: aws_logs.RetentionDays.TWO_WEEKS, | ||
}) | ||
|
||
new CustomResource(scope, 'resolve-s3-bucket-policy-conditions-custom-resource', { | ||
serviceToken: resourceProvider.serviceToken, | ||
properties: { | ||
fistonly: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✊ |
||
}, | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
import { | ||
DeletePublicAccessBlockCommand, | ||
GetBucketPolicyCommand, | ||
PutBucketPolicyCommand, | ||
PutPublicAccessBlockCommand, | ||
S3Client, | ||
} from '@aws-sdk/client-s3' | ||
import { OnEventHandler } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types' | ||
|
||
enum BucketPolicyConditions { | ||
StringEquals = 'StringEquals', | ||
} | ||
|
||
enum BucketPolicyConditionKey { | ||
'aws:SourceAccount' = 'aws:SourceAccount', | ||
'aws:PrincipalAccount' = 'aws:PrincipalAccount', | ||
} | ||
|
||
type BucketPolicyCondition = { | ||
[key in BucketPolicyConditions]: { | ||
[key in BucketPolicyConditionKey]?: string | string[] | ||
} | ||
} | ||
|
||
type BucketPolicyStatement = { | ||
Effect: 'Allow' | 'Deny' | ||
Action: string[] | ||
Condition?: BucketPolicyCondition | ||
Resource: string | ||
Principal: { | ||
[s: string]: string | ||
} | ||
} | ||
|
||
const resolveBucketPolicyConditions = | ||
(client: S3Client) => | ||
async ( | ||
bucketName: string, | ||
modifyPolicyStatement: (policyStatement: BucketPolicyStatement) => BucketPolicyStatement, | ||
) => { | ||
const policyResult = await client.send( | ||
new GetBucketPolicyCommand({ | ||
Bucket: bucketName, | ||
}), | ||
) | ||
|
||
if (!policyResult.Policy) throw new Error('Policy was not provided') | ||
|
||
const policy: { | ||
Version: string | ||
Statement: BucketPolicyStatement[] | ||
} = JSON.parse(policyResult?.Policy) | ||
|
||
await client.send( | ||
new PutBucketPolicyCommand({ | ||
Bucket: bucketName, | ||
Policy: JSON.stringify({ | ||
...policy, | ||
Statement: policy.Statement.map((statement) => modifyPolicyStatement(statement)), | ||
}), | ||
}), | ||
) | ||
} | ||
|
||
const migrateS3BucketPolicyConditions = async ({ | ||
bucketNames, | ||
iaasAccountId, | ||
paasAccountId, | ||
}: { | ||
iaasAccountId: string | ||
paasAccountId: string | ||
bucketNames: string[] | ||
}) => { | ||
const client = new S3Client() | ||
|
||
await Promise.all( | ||
bucketNames.map((bucketName) => | ||
client.send( | ||
new DeletePublicAccessBlockCommand({ | ||
Bucket: bucketName, | ||
}), | ||
), | ||
), | ||
) | ||
|
||
await Promise.all( | ||
bucketNames.map((bucketName) => | ||
resolveBucketPolicyConditions(client)(bucketName, (statement) => ({ | ||
...statement, | ||
Condition: statement?.Principal?.Service?.includes('cloudfront') | ||
? undefined | ||
: { | ||
[BucketPolicyConditions.StringEquals]: { | ||
[BucketPolicyConditionKey['aws:PrincipalAccount']]: [paasAccountId, iaasAccountId], | ||
}, | ||
}, | ||
})), | ||
), | ||
) | ||
|
||
await Promise.all( | ||
bucketNames.map((bucketName) => | ||
client.send( | ||
new PutPublicAccessBlockCommand({ | ||
Bucket: bucketName, | ||
PublicAccessBlockConfiguration: { | ||
BlockPublicPolicy: true, | ||
BlockPublicAcls: true, | ||
RestrictPublicBuckets: true, | ||
IgnorePublicAcls: true, | ||
}, | ||
}), | ||
), | ||
), | ||
) | ||
} | ||
|
||
const rollbackS3BucketPolicyConditions = async (bucketNames: string[]) => { | ||
const client = new S3Client() | ||
|
||
await Promise.all( | ||
bucketNames.map((bucketName) => | ||
client.send( | ||
new DeletePublicAccessBlockCommand({ | ||
Bucket: bucketName, | ||
}), | ||
), | ||
), | ||
) | ||
|
||
await Promise.all( | ||
bucketNames.map((bucketName) => | ||
resolveBucketPolicyConditions(client)(bucketName, (statement) => { | ||
delete statement.Condition | ||
|
||
return statement | ||
}), | ||
), | ||
) | ||
|
||
await Promise.all( | ||
bucketNames.map((bucketName) => | ||
client.send( | ||
new PutPublicAccessBlockCommand({ | ||
Bucket: bucketName, | ||
PublicAccessBlockConfiguration: { | ||
BlockPublicPolicy: true, | ||
BlockPublicAcls: true, | ||
RestrictPublicBuckets: true, | ||
IgnorePublicAcls: true, | ||
}, | ||
}), | ||
), | ||
), | ||
) | ||
} | ||
|
||
export const resolveS3BucketPolicyConditions: OnEventHandler = async (event) => { | ||
const bucketNames = process.env.BUCKETS ? process.env.BUCKETS?.split(',') : [] | ||
|
||
const iaasAccountId = process.env.IAAS_ACCOUNT_ID | ||
const paasAccountId = process.env.PAAS_ACCOUNT_ID | ||
|
||
if (typeof iaasAccountId !== 'string' || typeof paasAccountId !== 'string') | ||
throw new Error('envs for IAAS_ACCOUNT_ID or PAAS_ACCOUNT_ID was not found') | ||
|
||
if (event.RequestType === 'Create') { | ||
await migrateS3BucketPolicyConditions({ | ||
bucketNames, | ||
iaasAccountId, | ||
paasAccountId, | ||
}) | ||
} else if (event.RequestType === 'Delete') { | ||
await rollbackS3BucketPolicyConditions(bucketNames) | ||
} | ||
|
||
return { | ||
PhysicalResourceId: event.PhysicalResourceId, | ||
Data: {}, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could use Nodejsfunction here instead of bundling it then using the zip
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oooo really? I'll check that out!