From fea527c18a563496b660cff2656c3dd3d5e68e0a Mon Sep 17 00:00:00 2001 From: rstrahan Date: Tue, 5 Dec 2023 19:52:18 +0000 Subject: [PATCH] fix: hardcoded ARN in qbusiness policy --- bin/convert-cfn-template.js | 104 +++++++++++++++++++++++++----------- 1 file changed, 74 insertions(+), 30 deletions(-) diff --git a/bin/convert-cfn-template.js b/bin/convert-cfn-template.js index 97941d6..78eaa86 100644 --- a/bin/convert-cfn-template.js +++ b/bin/convert-cfn-template.js @@ -26,21 +26,30 @@ async function main() { console.log(`Reading template from ${templatePath}...`); const template = JSON.parse(fs.readFileSync(templatePath, 'utf8')); - // Identify all Lambda function assets in the template - console.log('Identifying Lambda function assets...'); - const assets = identifyLambdaAssets(template); - - // Zip and upload assets from cdk.out to new S3 location - for (let asset of assets) { - console.log(`Zipping and uploading asset: ${asset.key}`); - const zippedFilePath = await zipAsset(asset); - await uploadAsset(zippedFilePath, destinationBucket, destinationPrefix, asset.key); + // Identify all Lambda functions in the template + console.log('Identifying Lambda functions...'); + const lambdas = identifyLambdas(template); + + // Zip and upload code assets from cdk.out to new S3 location + for (let lambda of lambdas) { + console.log(`Zipping and uploading lambda code asset: ${lambda.codeAssetS3Key}`); + const zippedFilePath = await zipAsset(lambda); + await uploadAsset( + zippedFilePath, + destinationBucket, + destinationPrefix, + lambda.codeAssetS3Key + ); fs.unlinkSync(zippedFilePath); // Clean up local zipped file } - // Update template with new asset paths - console.log('Updating CloudFormation template with new asset paths...'); - updateTemplateLambdaAssetPaths(template, assets, destinationBucket, destinationPrefix); + // Update template with new code asset paths + console.log('Updating CloudFormation template with new code asset paths...'); + updateTemplateLambdaAssetPaths(template, lambdas, destinationBucket, destinationPrefix); + + // Modify Lambda roles to reference AppId parameter + console.log('Updating CloudFormation template with new resource ARNs...'); + updateTemplateLambdaRolePermissions(template, lambdas); // Remove remaining CDK vestiges delete template.Parameters.BootstrapVersion; @@ -48,7 +57,7 @@ async function main() { // Parameterize the environment variables console.log('Adding parameters to CloudFormation template...'); - parameterizeTemplate(template, assets); + parameterizeTemplate(template, lambdas); // Add slack app manifest to the Outputs console.log('Adding slack manifest to CloudFormation template...'); @@ -75,22 +84,23 @@ async function main() { } } -function identifyLambdaAssets(template) { - let assets = []; +function identifyLambdas(template) { + let lambdas = []; for (let [key, value] of Object.entries(template.Resources)) { if (value.Type === 'AWS::Lambda::Function' && value.Properties.Code?.S3Key) { - console.log(`Found asset in resource ${key}`); - assets.push({ - key: value.Properties.Code.S3Key, - resourceName: key + console.log(`Found lambda function: resource ${key}`); + lambdas.push({ + resourceName: key, + codeAssetS3Key: value.Properties.Code.S3Key, + roleResourceName: value.Properties.Role['Fn::GetAtt'][0] }); } } - return assets; + return lambdas; } -async function zipAsset(asset) { - const assetHash = asset.key.split('.')[0]; +async function zipAsset(lambda) { + const assetHash = lambda.codeAssetS3Key.split('.')[0]; const sourceDir = path.join('cdk.out', `asset.${assetHash}`); const outPath = `${path.join('cdk.out', assetHash)}.zip`; @@ -118,7 +128,7 @@ async function uploadAsset(zippedFilePath, destinationBucket, destinationPrefix, const fileStream = fs.createReadStream(zippedFilePath); const destinationKey = `${destinationPrefix}/${path.basename(originalKey)}`; - console.log(`Uploading zipped asset to s3://${destinationBucket}/${destinationKey}`); + console.log(`Uploading zipped code asset to s3://${destinationBucket}/${destinationKey}`); try { await s3Client.send( @@ -133,15 +143,49 @@ async function uploadAsset(zippedFilePath, destinationBucket, destinationPrefix, } } -function updateTemplateLambdaAssetPaths(template, assets, destinationBucket, destinationPrefix) { - for (let asset of assets) { - let lambdaResource = template.Resources[asset.resourceName]; +function updateTemplateLambdaAssetPaths(template, lambdas, destinationBucket, destinationPrefix) { + for (let lambda of lambdas) { + let lambdaResource = template.Resources[lambda.resourceName]; lambdaResource.Properties.Code.S3Bucket = destinationBucket; - lambdaResource.Properties.Code.S3Key = `${destinationPrefix}/${path.basename(asset.key)}`; + lambdaResource.Properties.Code.S3Key = `${destinationPrefix}/${path.basename( + lambda.codeAssetS3Key + )}`; + } +} + +function replaceQAppIdResourceArn(role, arn) { + const amazonQAppResourceArn = { + 'Fn::Sub': 'arn:aws:qbusiness:*:*:application/${AmazonQAppId}' + }; + if (typeof arn === 'string' && arn.startsWith('arn:aws:qbusiness:*:*:application/')) { + console.log( + `Role ${role}: updating Q application arn: ${arn} to ${JSON.stringify(amazonQAppResourceArn)}` + ); + arn = amazonQAppResourceArn; + } + return arn; +} + +function updateTemplateLambdaRolePermissions(template, lambdas) { + for (let lambda of lambdas) { + const roleResourceName = lambda.roleResourceName; + const roleResource = template.Resources[roleResourceName]; + for (let policy of roleResource.Properties.Policies) { + for (let statement of policy.PolicyDocument.Statement) { + const resource = statement.Resource; + if (Array.isArray(resource)) { + for (let i = 0; i < resource.length; i++) { + statement.Resource[i] = replaceQAppIdResourceArn(roleResourceName, resource[i]); + } + } else { + statement.Resource = replaceQAppIdResourceArn(roleResourceName, resource); + } + } + } } } -function parameterizeTemplate(template, assets) { +function parameterizeTemplate(template, lambdas) { template.Parameters = { AmazonQUserId: { Type: 'String', @@ -168,8 +212,8 @@ function parameterizeTemplate(template, assets) { Description: 'Number of days to keep conversation context' } }; - for (let asset of assets) { - let lambdaResource = template.Resources[asset.resourceName]; + for (let lambda of lambdas) { + let lambdaResource = template.Resources[lambda.resourceName]; lambdaResource.Properties.Environment.Variables.AMAZON_Q_ENDPOINT = ''; // use default endpoint lambdaResource.Properties.Environment.Variables.AMAZON_Q_USER_ID = { Ref: 'AmazonQUserId' }; lambdaResource.Properties.Environment.Variables.AMAZON_Q_APP_ID = { Ref: 'AmazonQAppId' };