|
// resolve early if provider level role is provided |
|
if ('role' in this.serverless.service.provider) { |
|
return BbPromise.resolve(); |
|
} |
|
|
|
// resolve early if all functions contain a custom role |
|
const customRolesProvided = []; |
|
this.serverless.service.getAllFunctions().forEach(functionName => { |
|
const functionObject = this.serverless.service.getFunction(functionName); |
|
customRolesProvided.push('role' in functionObject); |
|
}); |
|
if (_.isEqual(_.uniq(customRolesProvided), [true])) { |
|
return BbPromise.resolve(); |
|
} |
|
|
|
// 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(); |
|
|
|
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; |
|
|
|
// Ensure policies for functions with custom name resolution |
|
this.serverless.service.getAllFunctions().forEach(functionName => { |
|
const { name: resolvedFunctionName } = this.serverless.service.getFunction(functionName); |
|
if (!resolvedFunctionName || resolvedFunctionName.startsWith(canonicalFunctionNamePrefix)) { |
|
hasOneOrMoreCanonicallyNamedFunctions = true; |
|
return; |
|
} |
|
|
|
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 (this.serverless.service.provider.iamRoleStatements) { |
|
// add custom iam role statements |
|
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[ |
|
this.provider.naming.getRoleLogicalId() |
|
].Properties.Policies[0].PolicyDocument.Statement = policyDocumentStatements.concat( |
|
this.serverless.service.provider.iamRoleStatements |
|
); |
|
} |
|
|
|
if (this.serverless.service.provider.iamManagedPolicies) { |
|
// add iam managed policies |
|
const iamManagedPolicies = this.serverless.service.provider.iamManagedPolicies; |
|
if (iamManagedPolicies.length > 0) { |
|
this.mergeManagedPolicies(iamManagedPolicies); |
|
} |
|
} |
|
|
|
// check if one of the functions contains vpc configuration |
|
const vpcConfigProvided = []; |
|
this.serverless.service.getAllFunctions().forEach(functionName => { |
|
const functionObject = this.serverless.service.getFunction(functionName); |
|
if ('vpc' in functionObject) { |
|
vpcConfigProvided.push(true); |
|
} |
|
}); |
|
|
|
if (vpcConfigProvided.includes(true) || 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 BbPromise.resolve(); |
Use case description
Goal is to pave path for #4313
Currently Framework creates one IAM role, in which all needed statements for all functions are located.
We need a clear understanding which statements, managed policies and eventual principal registrations are required for which function
Current handling of IAM roles
mergeIamTemplatesfunction (assuming there's noprovider.roleset or all functions havingrole) where:provider.iamRoleStatementsare added to itprovider.iamManagedPoliciesare added to itIt is all happening here:
serverless/lib/plugins/aws/package/lib/mergeIamTemplates.js
Lines 41 to 173 in 4e49473
IamRoleLambdaExecutionresource fromserverless.service.provider.compiledCloudFormationTemplate.Resourceand adding needed configuration on pre-created object literally, as e.g. it's done here:serverless/lib/plugins/aws/package/compile/events/msk/index.js
Lines 115 to 124 in 4e49473
Proposed solution
Ideally would be to refactor above into following:
mergeIamTemplates:awsProvider.iamConfigandfunctions[].iamConfig, where eachiamConfigis object as follows:provider.iamRoleStatementstoawsProvider.iamConfig.policyStatementsprovider.iamManagedPoliciestoawsProvider.iamConfig.managedPoliciesprovider.vpcadd VPC related polices toawsProvider.iamConfig.managedPoliciesIn any place where we currently extend
IamRoleLambdaExecutiondirectly (look forgetRoleLogicalId()andIamRoleLambdaExecutionreferences) Refactor the code, to not extend IAM role resource directly, but instead add related policy statements, managed policies, principals to corresponding function'siamConfig(as those cases will be about specific function event configurations)In lib/plugins/aws/package/compile/functions/index.js:
iamConfig.policyStatements(at this point reflect exactly statements as were added atmergeIamTemplatesfunction)vpcconfiguration add VPC related policies to itsiamConfig.managedPoliciesCreate
resolveIamRoles.jsin lib/plugins/aws/package/lib which should export a method to be assigned atserverless/lib/plugins/aws/package/index.js
Lines 23 to 31 in 4e49473
serverless/lib/plugins/aws/package/index.js
Line 69 in 4e49473
awProvider.iamConfig.principalsandfunctions[].iamConfig.principalsonIamRoleLambdaExecutionresource (ensuring no duplicates)awsProvider.iamConfig.managedPoliciesandfunctions[].iamConfig.managedPoliciestoIamRoleLambdaExecutionresource (ensuring no duplicates)awsProvider.iamConfig.policyStatementsandfunctions[].iamConfig.managedPoliciestoIamRoleLambdaExecutionresource (ensuring no duplicates)Testing
This change will likely break a lot of tests, why at the same time we should end with same result CF template.
This signals that again our tests are wrong, and we should not put any effort in updating them in current shape.
Instead, any test file that will host broken tests should be refactored to be based on
runServerless(https://github.com/serverless/serverless/tree/master/test#unit-tests), and PR's that refactor each file should be proposed separate (different PR per each test file).Having those tests migrated to
runServerlessshould keep same tests passing after given refactor is made.