aws/deploy/createRole.js

/**
 * Exports an async function that creates one "kitchen sink" role for all AWS services to use. This module first creates an IAMClient, then creates a role, and finally adds permissions/policies (used as synonyms herein) with the help of a the helper function retry().
 * @module createRole
 */
const {
  IAMClient,
  CreateRoleCommand,
  AttachRolePolicyCommand,
} = require("@aws-sdk/client-iam");
const logger = require("../../utils/logger")("dev");
const retry = require("../../utils/retry");

/**
 * This constant is the general policy document. It is used by createRole.
 */
const policy = {
  Version: "2012-10-17",
  Statement: [
    {
      Effect: "Allow",
      Principal: {
        Service: [
          "s3.amazonaws.com",
          "sqs.amazonaws.com",
          "apigateway.amazonaws.com",
          "dynamodb.amazonaws.com",
          "lambda.amazonaws.com",
        ],
      },
      Action: "sts:AssumeRole",
    },
  ],
};

/**
 * This constant is an array containing all the specific policies our infrastructure uses. We are creating one role that will have all of these policies attached to it.
 */
const arnPermissions = [
  "arn:aws:iam::aws:policy/AmazonSQSFullAccess",
  "arn:aws:iam::aws:policy/AmazonS3FullAccess",
  "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs",
  "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess",
  "arn:aws:iam::aws:policy/AWSLambda_FullAccess",
];

/**
 * We must create a role first before we can add specific service-level policies to it.
 * @param {IAMClient} iam A IAM client: new IAMClient({ region })
 * @param {Object} policyDoc A constant defining the policy.
 * @param {String} roleName A constant made in deploy.js: `beekeeper-${PROFILE_NAME}-master-role`
 * @returns {String} data.Role.Arn This is an Amazon Resource Number uniquely identifying the role.
 */
const createRole = async (iam, policyDoc, roleName) => {
  const params = {
    RoleName: roleName,
    AssumeRolePolicyDocument: JSON.stringify(policyDoc),
  };
  const command = new CreateRoleCommand(params);

  try {
    let data = await iam.send(command);
    logger.debugSuccess(`Successfully created IAM role: ${data.Role.Arn}`);
    return data.Role.Arn;
  } catch (err) {
    logger.debugError("Error", err);
    throw new Error(err);
  }
};

/**
 * This function adds all the permissions from the arnPermissions array to the role we created. Adding multiple permissions in succession sometimes causes throttling by AWS, so we use a helper function retry(); if adding a permission fails, it waits more time and retries again. This function only iterates the arnPermissions array and uses the helper retry(); the concern of actually attaching the policy/permission to the role is left to attachPolicy(). Policy/permission are used interchangeably here.
 * @param {IAMClient} iam 
 * @param {String} roleName A constant made in deploy.js: `beekeeper-${PROFILE_NAME}-master-role`
 */
const addPermissions = async (iam, roleName) => {
  for (let arnPermission of arnPermissions) {
    await retry(() => attachPolicy(iam, arnPermission, roleName));
  }
};

/**
 * This function actually attaches a permission/policy to the role.
 * @param {IAMClient} iam A IAM client: new IAMClient({ region })
 * @param {String} arnPermission One of the permissions from the arnPermissions constant.
 * @param {String} roleName A constant made in deploy.js: `beekeeper-${PROFILE_NAME}-master-role`
 * @returns {Object} An object with properties the retry() helper function is expecting.
 */
const attachPolicy = async (iam, arnPermission, roleName) => {
  const command = new AttachRolePolicyCommand({
    PolicyArn: arnPermission,
    RoleName: roleName,
  });

  try {
    await iam.send(command);
    logger.debugSuccess(`Successfully added permission: ${arnPermission}`);
    return { status: "Success", response: ""}
  } catch (err) {
    logger.debugError("Error", err);
    return { status: err.Code, response: ""}
  }
}

/**
 * Exports createRole.
 * @param {String} region A constant destructured from the CLI user's answers in deploy.js. Like "us-east-2".
 * @param {String} roleName Made in deploy.js: `beekeeper-${PROFILE_NAME}-master-role` 
 * @returns {String} A constant Amazon Resource Number uniquely identifying the role. It is needed by many other modules called in deploy.js because AWS services often need to be associated with a role containing their permissions.
 */
module.exports = async (region, roleName) => {
  // Create an IAM client service object
  const iam = new IAMClient({ region });

  // Create Role
  const roleArn = await createRole(iam, policy, roleName);
  await addPermissions(iam, roleName);
  return roleArn;
};