Skip to content

Improve IAM role statements, policies and principals internal handling and resolution #8396

@medikoo

Description

@medikoo

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

  1. Before configuration of all functions is fully processed, IAM role resource is created at mergeIamTemplates function (assuming there's no provider.role set or all functions having role) where:
    • Policy statements needed for logs groups access are added to it
    • Eventual policy statements from provider.iamRoleStatements are added to it
    • Eventual managed policies from provider.iamManagedPolicies are added to it
    • Managed policies needed for VPC configuration are added to it

It is all happening here:

// 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();

  1. In next turn, events are compiled, and then eventual extra statements, polices are added and principals are registered. This is achieved by retrieving IamRoleLambdaExecution resource from serverless.service.provider.compiledCloudFormationTemplate.Resource and adding needed configuration on pre-created object literally, as e.g. it's done here:
    if (cfTemplate.Resources.IamRoleLambdaExecution) {
    const statement =
    cfTemplate.Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument
    .Statement;
    if (mskStatement.Resource.length) {
    statement.push(mskStatement);
    statement.push(ec2Statement);
    }
    }
    });

Proposed solution

Ideally would be to refactor above into following:

  1. At mergeIamTemplates:
    • Create IAM role resources , but with no statements, policies (setup collections, but leave them empty) - we still need to create it here, to not break things for plugins.
    • Create awsProvider.iamConfig and functions[].iamConfig, where each iamConfig is object as follows:
{
  principals: new Set(),
  policyStatements: [],
  managedPolicies: []
};
  • Add eventual policy statements from provider.iamRoleStatements to awsProvider.iamConfig.policyStatements
  • Add eventual managed policies from provider.iamManagedPolicies to awsProvider.iamConfig.managedPolicies
  • If provider.vpc add VPC related polices to awsProvider.iamConfig.managedPolicies
  1. In any place where we currently extend IamRoleLambdaExecution directly (look for getRoleLogicalId() and IamRoleLambdaExecution references) Refactor the code, to not extend IAM role resource directly, but instead add related policy statements, managed policies, principals to corresponding function's iamConfig (as those cases will be about specific function event configurations)

  2. In lib/plugins/aws/package/compile/functions/index.js:

    • Add related log group policy statements to function's iamConfig.policyStatements (at this point reflect exactly statements as were added at mergeIamTemplates function)
    • If function has vpc configuration add VPC related policies to its iamConfig.managedPolicies
  3. Create resolveIamRoles.js in lib/plugins/aws/package/lib which should export a method to be assigned at

    Object.assign(
    this,
    generateCoreTemplate,
    mergeIamTemplates,
    generateArtifactDirectoryName,
    mergeCustomProviderResources,
    saveServiceState,
    saveCompiledTemplate
    );
    invoked at
    BbPromise.bind(this).then(() =>
    and which should:

    • Register all awProvider.iamConfig.principals and functions[].iamConfig.principals on IamRoleLambdaExecution resource (ensuring no duplicates)
    • Add all awsProvider.iamConfig.managedPolicies and functions[].iamConfig.managedPolicies to IamRoleLambdaExecution resource (ensuring no duplicates)
    • Add all awsProvider.iamConfig.policyStatements and functions[].iamConfig.managedPolicies to IamRoleLambdaExecution resource (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 runServerless should keep same tests passing after given refactor is made.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions