Skip to content
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

feat: configure custom IAM roles with set permissions #298

Closed
wants to merge 3 commits into from
Closed
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
6 changes: 5 additions & 1 deletion package/googlePackage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const prepareDeployment = require('./lib/prepareDeployment');
const saveCreateTemplateFile = require('./lib/writeFilesToDisk');
const mergeServiceResources = require('./lib/mergeServiceResources');
const generateArtifactDirectoryName = require('./lib/generateArtifactDirectoryName');
const createIamRoles = require('./lib/createIamRoles');
const compileFunctions = require('./lib/compileFunctions');
const saveUpdateTemplateFile = require('./lib/writeFilesToDisk');

Expand Down Expand Up @@ -54,6 +55,7 @@ class GooglePackage {
prepareDeployment,
saveCreateTemplateFile,
generateArtifactDirectoryName,
createIamRoles,
compileFunctions,
mergeServiceResources,
saveUpdateTemplateFile
Expand All @@ -72,7 +74,9 @@ class GooglePackage {
.then(this.saveCreateTemplateFile),

'before:package:compileFunctions': () =>
BbPromise.bind(this).then(this.generateArtifactDirectoryName),
BbPromise.bind(this)
.then(this.generateArtifactDirectoryName)
.then(this.createIamRoles),

'package:compileFunctions': () => BbPromise.bind(this).then(this.compileFunctions),

Expand Down
153 changes: 153 additions & 0 deletions package/lib/createIamRoles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
'use strict';

const _ = require('lodash');

module.exports = {
createIamRoles() {
const provider = this.serverless.service.provider;
const iamConfig = provider.iam;
if (!iamConfig || !iamConfig.permissions) return;

if (provider.serviceAccountEmail) {
throw new Error('Cannot set both iam permissions and serviceAccountEmail on provider')
}

const projectId = provider.project;
const serviceName = `${this.serverless.service.service}-${this.options.stage}`;
const serviceAccountName = `sls-${serviceName}`;
const serviceAccountEmail = `${serviceAccountName}@${projectId}.iam.gserviceaccount.com`;

provider.serviceAccountEmail = serviceAccountEmail;

const deploymentResources =
this.serverless.service.provider.compiledConfigurationTemplate.resources;

// Create Cloud Function identity service account to assign custom IAM roles to
deploymentResources.push({
type: 'iam.v1.serviceAccount',
name: serviceAccountName,
properties: {
accountId: serviceAccountName,
displayName: serviceAccountName,
description: `Generated service account for Serverless project ${serviceName}`,
},
});

// Collect all permissions that don't apply to a specific resource
const [permissions, resourceSpecificRoles] = _.partition(
iamConfig.permissions,
(item) => typeof item === 'string'
);

// Create and assign custom role for permissions without a resource
if (permissions.length > 0) {
const iamObject = { permissions, projectId };
const role = getCustomRoleTemplate(projectId, serviceName, iamObject);
deploymentResources.push(role);
deploymentResources.push(
getIamMemberTemplate(projectId, serviceAccountEmail, role, iamObject)
);
}

// Create and assign custom role(s) for each specific resource
resourceSpecificRoles.forEach((iamObject) => {
const role = getCustomRoleTemplate(projectId, serviceName, iamObject);
deploymentResources.push(role);
deploymentResources.push(
getIamMemberTemplate(projectId, serviceAccountEmail, role, iamObject)
);
});
},
};

const ROLE_NAME_MAX_LENGTH = 64;
const getCustomRoleTemplate = (project, serviceName, config) => {
const namePrefix = serviceName.slice(0, 48).replaceAll('-', '_');
const nameSuffix = getResourceRoleSuffix(config)
.replace(/[^a-zA-Z_]/g, '')
.slice(0, ROLE_NAME_MAX_LENGTH - namePrefix.length);
const name = `${namePrefix}${nameSuffix}`;

return {
type: 'gcp-types/iam-v1:projects.roles',
name,
properties: {
parent: `projects/${project}`,
roleId: name,
role: {
title: name,
description: `Generated IAM role for Serverless project ${serviceName}`,
stage: 'GA',
includedPermissions: config.permissions,
},
},
};
};

const getIamMemberTemplate = (project, serviceAccountEmail, role, config) => {
const { type, resource } = getIamMembershipResourceType(config);

return {
type,
name: `${role.name}_members`,
properties: {
...resource,
role: `projects/${project}/roles/${role.properties.roleId}`,
member: `serviceAccount:${serviceAccountEmail}`,
},
};
};

const getResourceRoleSuffix = (config) => {
if (config.bucket) return `gcs_${config.bucket}`;
if (config.organizationId) return `org_${config.organizationId}`;
if (config.folderId) return `fol_${config.folderId}`;
if (config.projectId) return `pro_${config.projectId}`;
if (config.cloudFunction) return `gcf_${config.cloudFunction}`;
return '';
};

const getIamMembershipResourceType = (config) => {
if (config.bucket) {
return {
type: 'gcp-types/storage-v1:virtual.buckets.iamMemberBinding',
resource: {
bucket: config.bucket,
},
};
}
if (config.organizationId) {
return {
type: 'gcp-types/cloudresourcemanager-v1:virtual.organizations.iamMemberBinding',
resource: {
resource: config.organizationId,
},
};
}
if (config.folderId) {
return {
type: 'gcp-types/cloudresourcemanager-v2:virtual.folders.iamMemberBinding',
resource: {
resource: config.folderId,
},
};
}
if (config.projectId) {
return {
type: 'gcp-types/cloudresourcemanager-v1:virtual.projects.iamMemberBinding',
resource: {
resource: config.projectId,
},
};
}
if (config.cloudFunction) {
return {
type: 'gcp-types/cloudfunctions-v1:virtual.projects.locations.functions.iamMemberBinding',
resource: {
resource: config.cloudFunction,
},
};
}

throw new Error('IAM resource type not supported');
};
45 changes: 44 additions & 1 deletion provider/googleProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,50 @@ class GoogleProvider {
},
additionalProperties: false,
},
iamCustomRoles: {
type: 'object',
properties: {
permissions: {
type: 'array',
items: {
anyOf: [
{
$ref: '#/definitions/iamPermissionsOnResource',
},
{
type: 'string',
},
],
},
},
},
additionalProperties: false,
},
iamPermissionsOnResource: {
type: 'object',
properties: {
bucket: { type: 'string' },
organizationId: { type: 'string' },
folderId: { type: 'string' },
projectId: { type: 'string' },
cloudFunction: { type: 'string' },
permissions: {
type: 'array',
items: {
type: 'string',
},
},
},
additionalProperties: false,
oneOf: [
{ required: ['bucket', 'permissions'] },
{ required: ['organizationId', 'permissions'] },
{ required: ['folderId', 'permissions'] },
{ required: ['projectId', 'permissions'] },
{ required: ['cloudFunction', 'permissions'] },
]
},
},

provider: {
properties: {
credentials: { type: 'string' },
Expand All @@ -146,6 +188,7 @@ class GoogleProvider {
vpc: { type: 'string' }, // Can be overridden by function configuration
vpcEgress: { $ref: '#/definitions/cloudFunctionVpcEgress' }, // Can be overridden by function configuration
labels: { $ref: '#/definitions/resourceManagerLabels' }, // Can be overridden by function configuration
iam: { $ref: '#/definitions/iamCustomRoles' },
},
},
function: {
Expand Down