Skip to content

Support for Customer Managed Policies #128

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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 89 additions & 2 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ServerlessIamPerFunctionPlugin {
hooks: {[i: string]: () => void};
serverless: any;
awsPackagePlugin: any;
defaultCreateCustomerManagedPolicy: boolean;
defaultInherit: boolean;

readonly PROVIDER_AWS = 'aws';
Expand All @@ -39,6 +40,7 @@ class ServerlessIamPerFunctionPlugin {
[PLUGIN_NAME]: {
type: 'object',
properties: {
defaultCreateCustomerManagedPolicy: { type: 'boolean' },
defaultInherit: { type: 'boolean' },
iamGlobalPermissionsBoundary: { $ref: '#/definitions/awsArn' },
},
Expand All @@ -53,6 +55,7 @@ class ServerlessIamPerFunctionPlugin {
if (this.serverless.configSchemaHandler.defineFunctionProperties) {
this.serverless.configSchemaHandler.defineFunctionProperties(this.PROVIDER_AWS, {
properties: {
iamRoleCreateCustomerManagedPolicy: { type: 'boolean' },
iamRoleStatementsInherit: { type: 'boolean' },
iamRoleStatementsName: { type: 'string' },
iamPermissionsBoundary: { $ref: '#/definitions/awsArn' },
Expand All @@ -65,6 +68,9 @@ class ServerlessIamPerFunctionPlugin {
this.hooks = {
'before:package:finalize': this.createRolesPerFunction.bind(this),
};

const policyKey = `custom.${PLUGIN_NAME}.defaultCreateCustomerManagedPolicy`;
this.defaultCreateCustomerManagedPolicy = _.get(this.serverless.service, policyKey, false);
this.defaultInherit = _.get(this.serverless.service, `custom.${PLUGIN_NAME}.defaultInherit`, false);
}

Expand Down Expand Up @@ -177,6 +183,15 @@ class ServerlessIamPerFunctionPlugin {
const functionResource = this.serverless.service.provider
.compiledCloudFormationTemplate.Resources[functionResourceName];

if (typeof functionResource.Properties.Role === 'string') {
functionResource.Properties.Role = {
'Fn::GetAtt': [
globalRoleName,
'Arn',
],
};
}

if (_.isEmpty(functionResource)
|| _.isEmpty(functionResource.Properties)
|| _.isEmpty(functionResource.Properties.Role)
Expand Down Expand Up @@ -271,6 +286,39 @@ class ServerlessIamPerFunctionPlugin {
return res;
}

/**
* Create a Customer Managed Policy with the policy statement of the lambda function to attach to the role.
* @param {*} functionName
* @param {*} roleName
* @param {*} template
* @param {*} policyStatements
* @returns void
*/
createCustomerManagedPolicy(functionName: string, roleName: string, template: any, policyStatements: Statement[]) {
const stackName = this.serverless.providers.aws.naming.getStackName();
const managedPolicyName = `${stackName}-${functionName}-${this.serverless.service.provider.region}-policy`;

const managedPolicy = {
'Type': 'AWS::IAM::ManagedPolicy',
'Properties': {
'ManagedPolicyName': managedPolicyName,
'PolicyDocument': {
'Version': '2012-10-17',
'Statement': policyStatements,
},
'Roles': [
{
'Ref': roleName,
},
],
},
};

const normalizedIdentifier = this.serverless.providers.aws.naming.getNormalizedFunctionName(functionName)

template['Resources'][normalizedIdentifier + 'CustomerManagedPolicy'] = managedPolicy;
}

/**
* Will check if function has a definition of iamRoleStatements.
* If so will create a new Role for the function based on these statements.
Expand All @@ -293,10 +341,33 @@ class ServerlessIamPerFunctionPlugin {
// we use the configured role as a template
const globalRoleName = this.serverless.providers.aws.naming.getRoleLogicalId();
const globalIamRole = this.serverless.service.provider.compiledCloudFormationTemplate.Resources[globalRoleName];
const functionIamRole = _.cloneDeep(globalIamRole);
const functionIamRole = _.cloneDeep(globalIamRole) || {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: [
'lambda.amazonaws.com',
],
},
Action: [
'sts:AssumeRole',
],
},
],
},
Policies: [],
},
};
// remove the statements
const policyStatements: Statement[] = [];
functionIamRole.Properties.Policies[0].PolicyDocument.Statement = policyStatements;
const defaultRolePolicy = functionIamRole.Properties.Policies[0];
// clean policies
functionIamRole.Properties.Policies = [];
// set log statements
policyStatements[0] = {
Effect: 'Allow',
Expand Down Expand Up @@ -378,11 +449,27 @@ class ServerlessIamPerFunctionPlugin {

functionIamRole.Properties.RoleName = functionObject.iamRoleStatementsName
|| this.getFunctionRoleName(functionName);

const roleResourceName = this.serverless.providers.aws.naming.getNormalizedFunctionName(functionName)
+ globalRoleName;
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[roleResourceName] = functionIamRole;
const functionResourceName = this.updateFunctionResourceRole(functionName, roleResourceName, globalRoleName);
functionToRoleMap.set(functionResourceName, roleResourceName);

const isCustomerManagedPolicy = functionObject.iamRoleCreateCustomerManagedPolicy
|| (this.defaultCreateCustomerManagedPolicy && functionObject.iamRoleCreateCustomerManagedPolicy !== false);

if (isCustomerManagedPolicy) {
this.createCustomerManagedPolicy(
functionName,
roleResourceName,
this.serverless.service.provider.compiledCloudFormationTemplate,
policyStatements,
);
} else if (defaultRolePolicy) {
defaultRolePolicy.PolicyDocument.Statement = policyStatements;
functionIamRole.Properties.Policies.push(defaultRolePolicy);
}
}

/**
Expand Down
17 changes: 17 additions & 0 deletions src/test/funcs-with-iam.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@
"events": [],
"name": "test-permissions-boundary-hello",
"package": {}
},
"helloCustomerManagedPolicy": {
"handler": "handler.hello",
"iamRoleCreateCustomerManagedPolicy": true,
"iamRoleStatements": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/test"
}
],
"events": [],
"name": "test-python-dev-hello-customer-managed-policy",
"package": {},
"vpc": {}
}
},
"resources": {
Expand Down
44 changes: 42 additions & 2 deletions src/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,8 @@ describe('plugin tests', function(this: any) {
beforeEach(() => {
// set defaultInherit
_.set(serverless.service, 'custom.serverless-iam-roles-per-function.defaultInherit', true);
// set defaultCreateCustomerManagedPolicy
_.set(serverless.service, 'custom.serverless-iam-roles-per-function.defaultCreateCustomerManagedPolicy', true);
// change helloInherit to false for testing
_.set(serverless.service, 'functions.helloInherit.iamRoleStatementsInherit', false);
plugin = new Plugin(serverless);
Expand Down Expand Up @@ -406,7 +408,9 @@ describe('plugin tests', function(this: any) {
'HelloIamRoleLambdaExecution',
'function resource role is set properly',
);
let statements: any[] = helloRole.Properties.Policies[0].PolicyDocument.Statement;

const helloCustomerManagedPolicy = compiledResources.HelloCustomerManagedPolicy;
let statements: any[] = helloCustomerManagedPolicy.Properties.PolicyDocument.Statement;
assert.isObject(
statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords'),
'global statements imported as defaultInherit is set',
Expand All @@ -417,7 +421,9 @@ describe('plugin tests', function(this: any) {
);
const helloInheritRole = compiledResources.HelloInheritIamRoleLambdaExecution;
assertFunctionRoleName('helloInherit', helloInheritRole.Properties.RoleName);
statements = helloInheritRole.Properties.Policies[0].PolicyDocument.Statement;

const helloInheritCustomerManagedPolicy = compiledResources.HelloInheritCustomerManagedPolicy;
statements = helloInheritCustomerManagedPolicy.Properties.PolicyDocument.Statement;
assert.isObject(statements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 'per function statements imported');
assert.isTrue(statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords') === undefined,
'global statements not imported as iamRoleStatementsInherit is false');
Expand All @@ -443,6 +449,40 @@ describe('plugin tests', function(this: any) {
const policyName = defaultIamRoleLambdaExecution.Properties.PermissionsBoundary['Fn::Sub'];
assert.equal(policyName, 'arn:aws:iam::xxxxx:policy/permissions_boundary');
})

it('should add policy document to a Customer Managed Policy', () => {
const compiledResources = serverless.service.provider.compiledCloudFormationTemplate.Resources;

plugin.createRolesPerFunction();

const helloCustomerManagedPolicyCustomerManagedPolicy =
compiledResources.HelloCustomerManagedPolicyCustomerManagedPolicy;

const managedPolicyName = helloCustomerManagedPolicyCustomerManagedPolicy.Properties.ManagedPolicyName;
assert.equal(managedPolicyName, 'test-service-dev-helloCustomerManagedPolicy-us-east-1-policy');

const policyDocument = helloCustomerManagedPolicyCustomerManagedPolicy.Properties.PolicyDocument;
assert.equal(policyDocument.Version, '2012-10-17');
assert.lengthOf(policyDocument.Statement, 3);
assert.equal(policyDocument.Statement[1].Action[0], 'xray:PutTelemetryRecords');
assert.equal(policyDocument.Statement[2].Action[0], 'dynamodb:GetItem');

const roles = helloCustomerManagedPolicyCustomerManagedPolicy.Properties.Roles;
assert.equal(roles[0].Ref, 'HelloCustomerManagedPolicyIamRoleLambdaExecution');

const helloInheritCustomerManagedPolicy = compiledResources.HelloInheritCustomerManagedPolicy;

const inheritPolicyName = helloInheritCustomerManagedPolicy.Properties.ManagedPolicyName;
assert.equal(inheritPolicyName, 'test-service-dev-helloInherit-us-east-1-policy');

const inheritPolicyDocument = helloInheritCustomerManagedPolicy.Properties.PolicyDocument;
assert.equal(inheritPolicyDocument.Version, '2012-10-17');
assert.lengthOf(inheritPolicyDocument.Statement, 2);
assert.equal(inheritPolicyDocument.Statement[1].Action[0], 'dynamodb:GetItem');

const inheritRoles = helloInheritCustomerManagedPolicy.Properties.Roles;
assert.equal(inheritRoles[0].Ref, 'HelloInheritIamRoleLambdaExecution');
})
});
});

Expand Down