From 9e7746da88c6422f313b10daf7f047b0aaea6f85 Mon Sep 17 00:00:00 2001 From: omenocal Date: Tue, 7 May 2024 15:08:47 -0600 Subject: [PATCH 1/7] Add CustomerManagedPolicy feature --- src/lib/index.ts | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 6f025ae..9698f10 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -53,6 +53,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' }, @@ -62,6 +63,8 @@ class ServerlessIamPerFunctionPlugin { } } + console.log('options', _options); + this.hooks = { 'before:package:finalize': this.createRolesPerFunction.bind(this), }; @@ -271,6 +274,28 @@ class ServerlessIamPerFunctionPlugin { return res; } + /** + * Create a Customer Managed Policy with the policy statement of the lambda function to attach to the role. + * @param {*} functionObject + * @return array of statements (possibly empty) + */ + createCustomerManagedPolicy(functionName: string, roleName: string, template: any, policyStatements: Statement[]) { + const managedPolicyName = `${functionName}-${this.serverless.service.provider.region}-policy`; + + const managedPolicy = { + 'Type': 'AWS::IAM::ManagedPolicy', + 'Properties': { + 'ManagedPolicyName': managedPolicyName, + 'PolicyDocument': policyStatements, + 'Roles': [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. @@ -296,7 +321,7 @@ class ServerlessIamPerFunctionPlugin { const functionIamRole = _.cloneDeep(globalIamRole); // remove the statements const policyStatements: Statement[] = []; - functionIamRole.Properties.Policies[0].PolicyDocument.Statement = policyStatements; + functionIamRole.Properties.Policies = []; // set log statements policyStatements[0] = { Effect: 'Allow', @@ -378,6 +403,20 @@ class ServerlessIamPerFunctionPlugin { functionIamRole.Properties.RoleName = functionObject.iamRoleStatementsName || this.getFunctionRoleName(functionName); + + console.log('functionObject', functionObject); + + if (functionObject.iamRoleCreateCustomerManagedPolicy) { + this.createCustomerManagedPolicy( + functionName, + functionIamRole.Properties.RoleName, + this.serverless.service.provider.compiledCloudFormationTemplate, + policyStatements, + ); + } else { + functionIamRole.Properties.Policies[0].PolicyDocument.Statement = policyStatements; + } + const roleResourceName = this.serverless.providers.aws.naming.getNormalizedFunctionName(functionName) + globalRoleName; this.serverless.service.provider.compiledCloudFormationTemplate.Resources[roleResourceName] = functionIamRole; From c8c323551e7d92d232e4edcc14b00afa51244bc7 Mon Sep 17 00:00:00 2001 From: omenocal Date: Tue, 7 May 2024 16:27:01 -0600 Subject: [PATCH 2/7] Fix policyDocument issue --- src/lib/index.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 9698f10..0eba4d2 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -63,8 +63,6 @@ class ServerlessIamPerFunctionPlugin { } } - console.log('options', _options); - this.hooks = { 'before:package:finalize': this.createRolesPerFunction.bind(this), }; @@ -286,8 +284,15 @@ class ServerlessIamPerFunctionPlugin { 'Type': 'AWS::IAM::ManagedPolicy', 'Properties': { 'ManagedPolicyName': managedPolicyName, - 'PolicyDocument': policyStatements, - 'Roles': [roleName], + 'PolicyDocument': { + 'Version': '2012-10-17', + 'Statement': policyStatements, + }, + 'Roles': [ + { + 'Ref': roleName, + } + ], }, }; @@ -404,24 +409,22 @@ class ServerlessIamPerFunctionPlugin { functionIamRole.Properties.RoleName = functionObject.iamRoleStatementsName || this.getFunctionRoleName(functionName); - console.log('functionObject', functionObject); + 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); if (functionObject.iamRoleCreateCustomerManagedPolicy) { this.createCustomerManagedPolicy( functionName, - functionIamRole.Properties.RoleName, + roleResourceName, this.serverless.service.provider.compiledCloudFormationTemplate, policyStatements, ); } else { functionIamRole.Properties.Policies[0].PolicyDocument.Statement = policyStatements; } - - 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); } /** From 883d0168d9b80fe159d0c8a08136013bf85be34a Mon Sep 17 00:00:00 2001 From: omenocal Date: Tue, 7 May 2024 17:27:06 -0600 Subject: [PATCH 3/7] Add unit test --- src/lib/index.ts | 5 ++++- src/test/funcs-with-iam.json | 17 +++++++++++++++++ src/test/index.test.ts | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 0eba4d2..e8eafa1 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -326,6 +326,8 @@ class ServerlessIamPerFunctionPlugin { const functionIamRole = _.cloneDeep(globalIamRole); // remove the statements const policyStatements: Statement[] = []; + const defaultRolePolicy = functionIamRole.Properties.Policies[0]; + // clean policies functionIamRole.Properties.Policies = []; // set log statements policyStatements[0] = { @@ -423,7 +425,8 @@ class ServerlessIamPerFunctionPlugin { policyStatements, ); } else { - functionIamRole.Properties.Policies[0].PolicyDocument.Statement = policyStatements; + defaultRolePolicy.PolicyDocument.Statement = policyStatements; + functionIamRole.Properties.Policies.push(defaultRolePolicy); } } diff --git a/src/test/funcs-with-iam.json b/src/test/funcs-with-iam.json index 24f2517..cdcdeef 100644 --- a/src/test/funcs-with-iam.json +++ b/src/test/funcs-with-iam.json @@ -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": { diff --git a/src/test/index.test.ts b/src/test/index.test.ts index b8660c9..fa89776 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -443,6 +443,25 @@ 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; + + console.log('helloCustomerManagedPolicyCustomerManagedPolicy', JSON.stringify(helloCustomerManagedPolicyCustomerManagedPolicy, null, 2)); + const managedPolicyName = helloCustomerManagedPolicyCustomerManagedPolicy.Properties.ManagedPolicyName; + assert.equal(managedPolicyName, 'helloCustomerManagedPolicy-us-east-1-policy'); + + const policyDocument = helloCustomerManagedPolicyCustomerManagedPolicy.Properties.PolicyDocument; + assert.equal(policyDocument.Version, '2012-10-17'); + assert.lengthOf(policyDocument.Statement, 3); + + const roles = helloCustomerManagedPolicyCustomerManagedPolicy.Properties.Roles; + assert.equal(roles[0].Ref, 'HelloCustomerManagedPolicyIamRoleLambdaExecution'); + }) }); }); From e8fc72e72802a98d05ff482980a6b6dc35746683 Mon Sep 17 00:00:00 2001 From: omenocal Date: Tue, 7 May 2024 17:54:16 -0600 Subject: [PATCH 4/7] Add stackName to policyName --- src/lib/index.ts | 3 ++- src/test/index.test.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index e8eafa1..680b4ea 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -278,7 +278,8 @@ class ServerlessIamPerFunctionPlugin { * @return array of statements (possibly empty) */ createCustomerManagedPolicy(functionName: string, roleName: string, template: any, policyStatements: Statement[]) { - const managedPolicyName = `${functionName}-${this.serverless.service.provider.region}-policy`; + const stackName = this.serverless.providers.aws.naming.getStackName(); + const managedPolicyName = `${stackName}-${functionName}-${this.serverless.service.provider.region}-policy`; const managedPolicy = { 'Type': 'AWS::IAM::ManagedPolicy', diff --git a/src/test/index.test.ts b/src/test/index.test.ts index fa89776..67b1063 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -453,7 +453,7 @@ describe('plugin tests', function(this: any) { console.log('helloCustomerManagedPolicyCustomerManagedPolicy', JSON.stringify(helloCustomerManagedPolicyCustomerManagedPolicy, null, 2)); const managedPolicyName = helloCustomerManagedPolicyCustomerManagedPolicy.Properties.ManagedPolicyName; - assert.equal(managedPolicyName, 'helloCustomerManagedPolicy-us-east-1-policy'); + assert.equal(managedPolicyName, 'test-service-dev-helloCustomerManagedPolicy-us-east-1-policy'); const policyDocument = helloCustomerManagedPolicyCustomerManagedPolicy.Properties.PolicyDocument; assert.equal(policyDocument.Version, '2012-10-17'); From d24b58d54cbd34f178b0c4c5e8c47c7b4b354b02 Mon Sep 17 00:00:00 2001 From: omenocal Date: Tue, 7 May 2024 20:50:39 -0600 Subject: [PATCH 5/7] Add global defaultCreateCustomerManagedPolicy --- src/lib/index.ts | 8 +++++++- src/test/index.test.ts | 26 +++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 680b4ea..52c4616 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -14,6 +14,7 @@ class ServerlessIamPerFunctionPlugin { hooks: {[i: string]: () => void}; serverless: any; awsPackagePlugin: any; + defaultCreateCustomerManagedPolicy: boolean; defaultInherit: boolean; readonly PROVIDER_AWS = 'aws'; @@ -39,6 +40,7 @@ class ServerlessIamPerFunctionPlugin { [PLUGIN_NAME]: { type: 'object', properties: { + defaultCreateCustomerManagedPolicy: { type: 'boolean' }, defaultInherit: { type: 'boolean' }, iamGlobalPermissionsBoundary: { $ref: '#/definitions/awsArn' }, }, @@ -66,6 +68,7 @@ class ServerlessIamPerFunctionPlugin { this.hooks = { 'before:package:finalize': this.createRolesPerFunction.bind(this), }; + this.defaultCreateCustomerManagedPolicy = _.get(this.serverless.service, `custom.${PLUGIN_NAME}.defaultCreateCustomerManagedPolicy`, false); this.defaultInherit = _.get(this.serverless.service, `custom.${PLUGIN_NAME}.defaultInherit`, false); } @@ -418,7 +421,10 @@ class ServerlessIamPerFunctionPlugin { const functionResourceName = this.updateFunctionResourceRole(functionName, roleResourceName, globalRoleName); functionToRoleMap.set(functionResourceName, roleResourceName); - if (functionObject.iamRoleCreateCustomerManagedPolicy) { + const isCustomerManagedPolicy = functionObject.iamRoleCreateCustomerManagedPolicy + || (this.defaultCreateCustomerManagedPolicy && functionObject.iamRoleCreateCustomerManagedPolicy !== false); + + if (isCustomerManagedPolicy) { this.createCustomerManagedPolicy( functionName, roleResourceName, diff --git a/src/test/index.test.ts b/src/test/index.test.ts index 67b1063..2d8b3b4 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -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); @@ -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', @@ -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'); @@ -451,16 +457,30 @@ describe('plugin tests', function(this: any) { const helloCustomerManagedPolicyCustomerManagedPolicy = compiledResources.HelloCustomerManagedPolicyCustomerManagedPolicy; - console.log('helloCustomerManagedPolicyCustomerManagedPolicy', JSON.stringify(helloCustomerManagedPolicyCustomerManagedPolicy, null, 2)); 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'); }) }); }); From 63556ca6ef24d86d2fee787d3e9b986038fc6cd5 Mon Sep 17 00:00:00 2001 From: omenocal Date: Wed, 8 May 2024 12:06:38 -0600 Subject: [PATCH 6/7] Validate when user defines top level role --- src/lib/index.ts | 45 +++++++++++++++++++++++++++++++++++++----- src/test/index.test.ts | 3 ++- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 52c4616..ec3b578 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -68,7 +68,9 @@ class ServerlessIamPerFunctionPlugin { this.hooks = { 'before:package:finalize': this.createRolesPerFunction.bind(this), }; - this.defaultCreateCustomerManagedPolicy = _.get(this.serverless.service, `custom.${PLUGIN_NAME}.defaultCreateCustomerManagedPolicy`, false); + + 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); } @@ -181,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) @@ -277,7 +288,10 @@ class ServerlessIamPerFunctionPlugin { /** * Create a Customer Managed Policy with the policy statement of the lambda function to attach to the role. - * @param {*} functionObject + * @param {*} functionName + * @param {*} roleName + * @param {*} template + * @param {*} policyStatements * @return array of statements (possibly empty) */ createCustomerManagedPolicy(functionName: string, roleName: string, template: any, policyStatements: Statement[]) { @@ -295,7 +309,7 @@ class ServerlessIamPerFunctionPlugin { 'Roles': [ { 'Ref': roleName, - } + }, ], }, }; @@ -327,7 +341,28 @@ 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[] = []; const defaultRolePolicy = functionIamRole.Properties.Policies[0]; @@ -431,7 +466,7 @@ class ServerlessIamPerFunctionPlugin { this.serverless.service.provider.compiledCloudFormationTemplate, policyStatements, ); - } else { + } else if (defaultRolePolicy) { defaultRolePolicy.PolicyDocument.Statement = policyStatements; functionIamRole.Properties.Policies.push(defaultRolePolicy); } diff --git a/src/test/index.test.ts b/src/test/index.test.ts index 2d8b3b4..b971808 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -455,7 +455,8 @@ describe('plugin tests', function(this: any) { plugin.createRolesPerFunction(); - const helloCustomerManagedPolicyCustomerManagedPolicy = compiledResources.HelloCustomerManagedPolicyCustomerManagedPolicy; + const helloCustomerManagedPolicyCustomerManagedPolicy = + compiledResources.HelloCustomerManagedPolicyCustomerManagedPolicy; const managedPolicyName = helloCustomerManagedPolicyCustomerManagedPolicy.Properties.ManagedPolicyName; assert.equal(managedPolicyName, 'test-service-dev-helloCustomerManagedPolicy-us-east-1-policy'); From 218e298cbf05ee4faeb693f7effaa13611cf1cfd Mon Sep 17 00:00:00 2001 From: omenocal Date: Sat, 1 Mar 2025 15:29:07 -0600 Subject: [PATCH 7/7] Updates function doc --- src/lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index ec3b578..e4f183e 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -292,7 +292,7 @@ class ServerlessIamPerFunctionPlugin { * @param {*} roleName * @param {*} template * @param {*} policyStatements - * @return array of statements (possibly empty) + * @returns void */ createCustomerManagedPolicy(functionName: string, roleName: string, template: any, policyStatements: Statement[]) { const stackName = this.serverless.providers.aws.naming.getStackName();