serverless/lib/plugins/aws/package/lib/mergeIamTemplates.js
2021-05-10 13:07:44 +02:00

234 lines
8.1 KiB
JavaScript

'use strict';
const _ = require('lodash');
const path = require('path');
module.exports = {
mergeIamTemplates() {
// resolve early if no functions are provided
if (!this.serverless.service.getAllFunctions().length) {
return;
}
// create log group resources
this.serverless.service
.getAllFunctions()
.filter((functionName) => !this.serverless.service.getFunction(functionName).disableLogs)
.forEach((functionName) => {
const functionObject = this.serverless.service.getFunction(functionName);
const logGroupLogicalId = this.provider.naming.getLogGroupLogicalId(functionName);
const newLogGroup = {
[logGroupLogicalId]: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: this.provider.naming.getLogGroupName(functionObject.name),
},
},
};
const logRetentionInDays = this.provider.getLogRetentionInDays();
if (logRetentionInDays) {
newLogGroup[logGroupLogicalId].Properties.RetentionInDays = logRetentionInDays;
}
_.merge(
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
newLogGroup
);
});
const iamRole = _.get(this.serverless.service.provider.iam, 'role', {});
// resolve early if provider level role is provided as a reference to existing role
if (this.provider.isExistingRoleProvided(iamRole)) {
return;
}
if ('role' in this.serverless.service.provider) {
return;
}
// resolve early if all functions contain a custom role
const customRoleProvided = this.serverless.service.getAllFunctions().every((functionName) => {
const functionObject = this.serverless.service.getFunction(functionName);
return 'role' in functionObject;
});
if (customRoleProvided) {
return;
}
// merge in the iamRoleLambdaTemplate
const iamRoleLambdaExecutionTemplate = this.serverless.utils.readFileSync(
path.join(
this.serverless.config.serverlessPath,
'plugins',
'aws',
'package',
'lib',
'iam-role-lambda-execution-template.json'
)
);
iamRoleLambdaExecutionTemplate.Properties.Path = this.provider.naming.getRolePath();
iamRoleLambdaExecutionTemplate.Properties.RoleName = this.provider.naming.getRoleName();
// set role tags
if (iamRole.tags) {
iamRoleLambdaExecutionTemplate.Properties.Tags = Object.keys(iamRole.tags).map((key) => ({
Key: key,
Value: iamRole.tags[key],
}));
}
// set permission boundary
if (iamRole.permissionsBoundary) {
iamRoleLambdaExecutionTemplate.Properties.PermissionsBoundary = iamRole.permissionsBoundary;
} else if (iamRole.permissionBoundary) {
iamRoleLambdaExecutionTemplate.Properties.PermissionsBoundary = iamRole.permissionBoundary;
} else if (this.serverless.service.provider.rolePermissionsBoundary) {
iamRoleLambdaExecutionTemplate.Properties.PermissionsBoundary =
this.serverless.service.provider.rolePermissionsBoundary;
}
iamRoleLambdaExecutionTemplate.Properties.Policies[0].PolicyName =
this.provider.naming.getPolicyName();
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[this.provider.naming.getRoleLogicalId()]: iamRoleLambdaExecutionTemplate,
});
const canonicalFunctionNamePrefix = `${
this.provider.serverless.service.service
}-${this.provider.getStage()}`;
const logGroupsPrefix = this.provider.naming.getLogGroupName(canonicalFunctionNamePrefix);
const policyDocumentStatements =
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
this.provider.naming.getRoleLogicalId()
].Properties.Policies[0].PolicyDocument.Statement;
let hasOneOrMoreCanonicallyNamedFunctions = false;
const disableLogsCanonicallyNamedFunctions = [];
// Ensure policies for functions with custom name resolution
this.serverless.service.getAllFunctions().forEach((functionName) => {
const { name: resolvedFunctionName, disableLogs: areLogsDisabled } =
this.serverless.service.getFunction(functionName);
if (!resolvedFunctionName || resolvedFunctionName.startsWith(canonicalFunctionNamePrefix)) {
if (areLogsDisabled) {
disableLogsCanonicallyNamedFunctions.push(resolvedFunctionName);
} else {
hasOneOrMoreCanonicallyNamedFunctions = true;
}
return;
}
if (!areLogsDisabled) {
const customFunctionNamelogGroupsPrefix =
this.provider.naming.getLogGroupName(resolvedFunctionName);
policyDocumentStatements[0].Resource.push({
'Fn::Sub':
'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
`:log-group:${customFunctionNamelogGroupsPrefix}:*`,
});
policyDocumentStatements[1].Resource.push({
'Fn::Sub':
'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
`:log-group:${customFunctionNamelogGroupsPrefix}:*:*`,
});
}
});
if (hasOneOrMoreCanonicallyNamedFunctions) {
// Ensure general policies for functions with default name resolution
policyDocumentStatements[0].Resource.push({
'Fn::Sub':
'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
`:log-group:${logGroupsPrefix}*:*`,
});
policyDocumentStatements[1].Resource.push({
'Fn::Sub':
'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
`:log-group:${logGroupsPrefix}*:*:*`,
});
if (disableLogsCanonicallyNamedFunctions.length > 0) {
// add deny rule for disabled logs function
const arnOfDisableLogGroups = disableLogsCanonicallyNamedFunctions.map((functionName) => {
return {
'Fn::Sub':
'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
`:log-group:${this.provider.naming.getLogGroupName(functionName)}:*`,
};
});
this.mergeStatements([
{
Effect: 'Deny',
Action: 'logs:PutLogEvents',
Resource: arnOfDisableLogGroups,
},
]);
}
}
// add custom iam role statements
if (iamRole.statements) {
this.mergeStatements(iamRole.statements);
} else if (this.serverless.service.provider.iamRoleStatements) {
this.mergeStatements(this.serverless.service.provider.iamRoleStatements);
}
// add iam managed policies
if (iamRole.managedPolicies) {
this.mergeManagedPolicies(iamRole.managedPolicies);
} else if (this.serverless.service.provider.iamManagedPolicies) {
this.mergeManagedPolicies(this.serverless.service.provider.iamManagedPolicies);
}
// check if one of the functions contains vpc configuration
const vpcConfigProvided = this.serverless.service.getAllFunctions().some((functionName) => {
const functionObject = this.serverless.service.getFunction(functionName);
return 'vpc' in functionObject;
});
if (vpcConfigProvided || this.serverless.service.provider.vpc) {
// add managed iam policy to allow ENI management
this.mergeManagedPolicies([
{
'Fn::Join': [
'',
[
'arn:',
{ Ref: 'AWS::Partition' },
':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
],
],
},
]);
}
return;
},
mergeManagedPolicies(managedPolicies) {
if (!managedPolicies.length) return;
const resource =
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
this.provider.naming.getRoleLogicalId()
].Properties;
if (!Array.isArray(resource.ManagedPolicyArns)) {
resource.ManagedPolicyArns = [];
}
resource.ManagedPolicyArns.push(...managedPolicies);
},
mergeStatements(statements) {
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
this.provider.naming.getRoleLogicalId()
].Properties.Policies[0].PolicyDocument.Statement.push(...statements);
},
};