DEV Community

Koby Bass
Koby Bass

Posted on

TypeScript AWS Policies

For more details and CDK libraries, check out the @cdklib project readme.
This post is also available on my blog

After working with AWS for a while, I've found myself writing the same IAM policy patterns over and over. Whether you're using CDK, Terraform, or just the AWS console, policy creation often involves copying JSON snippets and tweaking them for your specific resources.

I wanted a more TypeScript-friendly way to handle this common task, so I built @cdklib/aws-policy - a simple library that brings type safety to AWS IAM policies.

It's designed to work with any TypeScript project, whether you're using infrastructure as code tools or creating resources dynamically (tenant provisioning, etc).

Life As We Know It

If you've worked with AWS IAM policies in TypeScript, you're probably familiar with awkward patterns like these:

// Approach 1: JSON.stringify a raw object
const bucketPolicy = JSON.stringify({
  Version: "2012-10-17",
  Statement: [
    {
      Effect: "Allow",
      Action: ["s3:GetObject", "s3:ListBucket"],
      Resource: ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
    },
  ],
});

// Approach 2: Template literals
const bucketName = "app-assets";
const policyJson = `{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::${bucketName}",
        "arn:aws:s3:::${bucketName}/*"
      ]
    }
  ]
}`;

// Approach 3: CDK policies with duplicated statement wrappers
new iam.PolicyDocument({
  statements: [
    new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ["s3:GetObject", "s3:ListBucket"],
      resources: [`arn:aws:s3:::${bucketName}`, `arn:aws:s3:::${bucketName}/*`],
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

These approaches have several drawbacks:

  • No TypeScript intellisense for action names or effect types
  • Duplication of Version and Statement wrapper boilerplate (that never changes)
  • Error-prone when you need to modify for multiple resources
  • Inconsistent approaches across your codebase

Life with @cdklib/aws-policy

At its core, @cdklib/aws-policy lets you:

  1. Create policies with TypeScript instead of JSON
  2. Get intellisense and type checking for your policy statements
  3. Build reusable policy templates with parameters
  4. Easily import AWS examples into your codebase

Let's look at how it works.

Basic Usage

First, install the package:

npm install @cdklib/aws-policy
Enter fullscreen mode Exit fullscreen mode

Here's a simple example of creating a policy:

import { AwsPolicy } from "@cdklib/aws-policy";

// Create a policy with multiple statements
const bucketPolicy = AwsPolicy.from(
  {
    Effect: "Allow",
    Action: ["s3:GetObject", "s3:ListBucket"],
    Resource: ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
  },
  {
    Effect: "Deny",
    Action: "s3:DeleteObject",
    Resource: "arn:aws:s3:::my-bucket/*",
  }
);

// Get JSON output - the Version is automatically added
const policyJson = bucketPolicy.toJson();
Enter fullscreen mode Exit fullscreen mode

This gives you the same JSON policy you'd write by hand, but with TypeScript's help along the way. If you try to use an invalid effect type or forget a required field, your editor will let you know immediately.

Importing AWS Examples

Many times you just want to copy an example from the AWS docs and use it in your code.

The library makes it extremely easy - just copy paste the statements, and format-on-save will do the rest:

// Example straight from AWS docs:
// {
//   "Effect": "Allow",
//   "Action": "s3:ListBucket",
//   "Resource": "arn:aws:s3:::example_bucket"
// }

// Import into TypeScript
const policy = AwsPolicy.from({
  Effect: "Allow",
  Action: "s3:ListBucket",
  Resource: "arn:aws:s3:::example_bucket",
});
Enter fullscreen mode Exit fullscreen mode

You can also import existing policy JSON from files or APIs:

const rawStatement = JSON.parse(fs.readFileSync("policy.json", "utf8"));
const importedPolicy = AwsPolicy.fromRaw(rawStatement);
Enter fullscreen mode Exit fullscreen mode

Reusable Policy Templates

As you build more AWS resources, you'll find yourself creating similar policies with slight variations. For example, you might need S3 bucket policies with different bucket names. That's where prepared policies become useful:

import { AwsPreparedPolicy } from "@cdklib/aws-policy";

// Define a reusable policy template
const s3BucketPolicy = new AwsPreparedPolicy<{
  bucketName: string;
}>(({bucketName}) => ({
  Effect: "Allow",
  Action: ["s3:GetObject", "s3:ListBucket"],
  Resource: [
    `arn:aws:s3:::${bucketName}`,
    `arn:aws:s3:::${bucketName}/*`,
  ],
}));

// Use it for different buckets
const userDataPolicy = s3BucketPolicy.fill({
  bucketName: "user-data",
});

const appAssetsPolicy = s3BucketPolicy.fill({
  bucketName: "app-assets",
});

// You can also partially fill templates with .fillPartial() for progressive parameter filling
const partialPolicy = s3BucketPolicy.fillPartial({ bucketName: "user-data" });
const fullPolicy = partialPolicy.fill({ otherParam: "value" });
Enter fullscreen mode Exit fullscreen mode

This approach helps eliminate duplicate code while keeping your policies type-safe.

Integration with CdkConfig

If you're using the @cdklib/config library I mentioned in my previous post, you can create policies that use the CDK scope to access configuration:

import { AwsPreparedPolicy } from "@cdklib/aws-policy";
import { awsConfig } from "./config/aws";

// Define a policy that includes scope as a parameter
const s3BucketPolicy = new AwsPreparedPolicy<{
  scope: Construct;
  bucketName: string;
}>(({ scope, bucketName }) => {
  // Get config values from scope
  const { accountId } = awsConfig.get(scope);

  return {
    Effect: "Allow",
    Action: ["s3:GetObject", "s3:ListBucket"],
    Resource: [`arn:aws:s3:::${bucketName}`, `arn:aws:s3:::${bucketName}/*`],
    Principal: {
      AWS: `arn:aws:iam::${accountId}:root`,
    },
  };
});

// Provide scope and parameters
const policy = s3BucketPolicy.fill({
  scope: myApp,
  bucketName: "app-assets",
});
Enter fullscreen mode Exit fullscreen mode

Combining Policies

You can combine multiple policies together, for example granting S3 read access and Lambda invoke access.

The policy statements are combined - the library does not attempt to merge policies logically.

// Define individual policies
const s3ReadPolicy = new AwsPreparedPolicy<{ bucketName: string }>(
  ({bucketName}) => ({
    Effect: "Allow",
    Action: ["s3:GetObject", "s3:ListBucket"],
    Resource: [
      `arn:aws:s3:::${bucketName}`,
      `arn:aws:s3:::${bucketName}/*`,
    ],
  })
);

const lambdaInvokePolicy = new AwsPreparedPolicy<{ functionName: string }>(
  ({functionName}) => ({
    Effect: "Allow",
    Action: "lambda:InvokeFunction",
    Resource: `arn:aws:lambda:*:*:function:${functionName}`,
  })
);

// Combine policies - parameters are combined
const combinedPolicy = AwsPreparedPolicy.combine(
  s3ReadPolicy,
  lambdaInvokePolicy
);

// Fill with all required parameters
const policy = combinedPolicy.fill({
  bucketName: "my-bucket",
  functionName: "my-function",
});
Enter fullscreen mode Exit fullscreen mode

Closing Thoughts

The @cdklib/aws-policy library is a small utility that makes working with AWS IAM policies a bit nicer in TypeScript projects.

The library is open source and available on GitHub, where you can find more examples and documentation. Feel free to use it, modify it, or build on it to fit your needs.

Top comments (0)