Using iam-floyd / cdk-iam-floyd
IAM Floyd is a fluent, type-safe AWS IAM policy statement generator. Instead of writing error-prone JSON, you call strongly-typed methods and let the library build the correct policy structure.
Installation
# Standalone (no CDK)
npm install iam-floyd
# With AWS CDK
npm install cdk-iam-floyd
Pin the version in
package.json(e.g."iam-floyd": "0.762.0") — the package is pre-1.0.0 and the API may change. CDK v2 requirescdk-iam-floyd >= 0.300.0.
Basic import
// TypeScript / ESM
import { Statement } from 'iam-floyd';
import { Statement } from 'cdk-iam-floyd'; // CDK variant
// CommonJS
const { Statement } = require('iam-floyd');
Core vocabulary — the fluent chain
Every statement follows this readable pattern:
new Statement.<Service>() // pick the AWS service
.allow() | .deny() // effect (allow is the default)
.toXxx() // actions ("to do something")
.onXxx() // resources ("on a specific resource")
.ifXxx() // conditions ("if a condition is met")
Effect — allow() / deny()
The default effect is Allow, so .allow() is optional but improves readability.
new Statement.Ec2().allow().toStartInstances();
new Statement.Ec2().deny().toStopInstances();
Actions — to*()
Every documented IAM action has a typed to*() method:
new Statement.Ec2()
.allow()
.toStartInstances()
.toStopInstances()
.toDescribeInstances();
Bulk action helpers:
| Method | Effect |
|---|---|
allActions() |
Adds ec2:* |
allListActions() |
All actions with access level List |
allReadActions() |
All actions with access level Read |
allWriteActions() |
All actions with access level Write |
allPermissionManagementActions() |
All permission-management actions |
allTaggingActions() |
All tagging actions |
allMatchingActions('/pattern/flags') |
Regex match (JS literal style, as string) |
allMatchingActionsand access-level methods expand to an explicit list at call time — they won't auto-update if AWS adds new actions later.
Use .compact() after access-level methods to collapse the list into wildcard
patterns (reduces policy size, but future AWS actions may match unexpectedly):
new Statement.Ec2().allow().allReadActions().allListActions().compact();
// produces: ["ec2:Describe*", "ec2:Get*", ...]
Missing action? Use the generic to() escape hatch:
.to('missingAction') // becomes ec2:missingAction
Resources — on*()
Every resource type has an on*() method that builds the correct ARN:
new Statement.S3()
.allow()
.allActions()
.onBucket('example-bucket')
.onObject('example-bucket', 'some/prefix/*');
new Statement.Lambda().allow().toInvokeFunction().onFunction('my-function'); // optional: account, region, partition params
new Statement.Ec2()
.allow()
.toStartInstances()
.toStopInstances()
.onInstance('i-1234567890abcdef0');
In cdk-iam-floyd, omitting account/region defaults to the CDK stack's values.
In iam-floyd (standalone), they default to *.
Override defaults for multiple resources using in*() / in():
new Statement.Lambda()
.allow()
.toUpdateFunctionCode()
.inAccount('098765432109')
.inRegion('us-east-1')
.inPartition('aws')
.onFunction('my-function-1')
.onFunction('my-function-2')
// Shorthand:
.in('098765432109', 'us-east-1', 'aws')
.onFunction('my-function-1')
// Switch context mid-statement:
.in('111111111111', 'eu-west-1', 'aws')
.onFunction('cross-account-function');
in*()methods set defaults only for resources added after them — call them before theon*()calls they should affect.
Already have an ARN? Use the generic on():
.on('arn:aws:s3:::my-bucket', 'arn:aws:s3:::another-bucket')
Conditions — if*()
Every documented condition key has an if*() method. Global AWS conditions start
with ifAws*.
new Statement.Ec2()
.allow()
.toStartInstances()
.ifEncrypted() // Bool condition
.ifInstanceType(['t3.micro', 't3.nano']) // StringLike with multiple values
.ifAssociatePublicIpAddress(false) // Bool: false
.ifAwsRequestTag('Owner', 'alice'); // Tag condition
Multiple conditions on one statement are AND-ed together. For OR logic, use multiple statements.
Override the operator (last argument):
.ifAwsRequestTag('Env', '*John*', 'StringEquals')
// Or use the typed Operator class:
import { Operator } from 'iam-floyd';
.ifAwsRequestTag('Env', '*John*', Operator.stringEquals)
Deny + condition operator trap: When using
deny()with a tag condition, the operator direction matters a lot. The default operator on mostif*()methods isStringLike. So.deny().toStopInstances().ifResourceTag('Owner', 'alice')means "deny stop when the tag matches alice" — which denies the owner, not others. To deny everyone except the owner, flip it:.deny().toStopInstances().ifResourceTag('Owner', 'alice', Operator.stringNotEquals)
Complex operators (ForAllValues, ForAnyValue, IfExists):
import { Operator } from 'iam-floyd';
new Statement.Dynamodb()
.allow()
.toGetItem()
.onTable('Thread')
.ifAttributes(
['ID', 'Message'],
new Operator().stringEquals().forAllValues(),
);
new Statement.Ec2()
.allow()
.toStartInstances()
.ifAwsRequestTag(
'Environment',
['Prod', 'Dev'],
new Operator().stringEquals().ifExists(),
);
Missing condition? Use the generic if():
.if('ec2:missingCondition', 'some-value') // defaults to StringLike
Principals — for*()
In cdk-iam-floyd, avoid building assume-role policies with iam-floyd — use CDK's
iam.PolicyDocument/iam.ManagedPolicywith a principal instead. iam-floyd'sfor*()methods are mainly useful for resource-based policies (S3 bucket policies, etc.)
new Statement.Sts()
.allow()
.toAssumeRole()
.forAccount('123456789012') // Account root principal
.forUser('123456789012', 'Alice', 'Bob') // IAM users (same account)
.forRole('123456789012', 'my-role') // IAM role ARN
.forService('lambda.amazonaws.com') // AWS service
.forFederatedCognito() // Cognito identity pool
.forPublic() // Principal: *
.for('arn:foo:bar'); // Arbitrary ARN
// CDK only — accepts iam.IPrincipal objects:
new Statement.Sts()
.allow()
.toAssumeRole()
.forCdkPrincipal(
new iam.ServicePrincipal('sns.amazonaws.com'),
new iam.ServicePrincipal('lambda.amazonaws.com'),
);
NotAction / NotResource / NotPrincipal
Understand these well before using — they can be surprisingly permissive.
// Allow everything EXCEPT delete on this bucket:
new Statement.S3()
.allow()
.notAction()
.toDeleteBucket()
.onBucket('example-bucket');
// Deny all S3 actions NOT on this bucket:
new Statement.S3()
.allow()
.notResource()
.toDeleteBucket()
.onBucket('example-bucket');
// Deny all S3 actions for anyone who is NOT Bob:
new Statement.S3()
.deny()
.allActions()
.notPrincipal()
.forUser('1234567890', 'Bob')
.onObject('example-bucket', '*');
Statement SID
new Statement.Ec2('AllowEC2ReadOnly').allow().allReadActions();
Building a complete policy
Standalone (iam-floyd)
import { Statement, Operator } from 'iam-floyd';
const policy = {
Version: '2012-10-17',
Statement: [
new Statement.Ec2()
.allow()
.toStartInstances()
.ifAwsRequestTag('Owner', '${aws:username}'),
// deny stop for anyone who is NOT the owner (note: StringNotEquals, not default StringLike)
new Statement.Ec2()
.deny()
.toStopInstances()
.ifResourceTag('Owner', '${aws:username}', Operator.stringNotEquals),
new Statement.Ec2().allow().allListActions().allReadActions(),
],
};
CDK (cdk-iam-floyd)
import { Statement } from 'cdk-iam-floyd';
import * as iam from 'aws-cdk-lib/aws-iam';
const policy = new iam.ManagedPolicy(this, 'Policy', {
statements: [
new Statement.S3()
.allow()
.toGetObject()
.toPutObject()
.onObject('my-bucket', '*'),
new Statement.S3().allow().toListBucket().onBucket('my-bucket'),
],
});
// Or attach to a role:
role.addToPolicy(
new Statement.Lambda().allow().toInvokeFunction().onFunction('my-function'),
);
In CDK,
onFunction()/onBucket()etc. automatically use the stack's account and region when you omit those parameters.
Collections — reusable statement groups
Collections are pre-built groups of statements for common scenarios:
import { Collection } from 'iam-floyd'; // or 'cdk-iam-floyd'
// Spread into a policy:
const policy = {
Version: '2012-10-17',
Statement: [...new Collection().allowEc2InstanceDeleteByOwner()],
};
Currently available: allowEc2InstanceDeleteByOwner (allows start/stop EC2
instances tagged with the caller's username).
Advanced: Semantic policy classes
A powerful pattern to bundle related permissions under a meaningful name. This creates reusable, self-documenting policy building blocks.
import { Statement } from 'cdk-iam-floyd'; // or 'iam-floyd'
// Bundle all permissions needed to push to ECR under a readable name
class MyEcr extends Statement.Ecr {
constructor() {
super();
}
toPushImage(): this {
return this.toGetAuthorizationToken()
.toBatchCheckLayerAvailability()
.toInitiateLayerUpload()
.toUploadLayerPart()
.toCompleteLayerUpload()
.toPutImage();
}
}
// Usage — now reads like intent, not implementation:
new MyEcr().allow().toPushImage().onRepository('my-repo');
Why this matters:
- The method name expresses business intent (
toPushImage), not raw AWS actions - Reusable across stacks / projects without copy-pasting action lists
- The chain still works — all
on*(),if*()etc. methods are still available
You can do this for any grouping that makes sense in your domain:
class AppBucket extends Statement.S3 {
toReadApp(): this {
return this.toGetObject().toListBucket();
}
toWriteApp(): this {
return this.toPutObject().toDeleteObject();
}
}
Real-world CDK example: CFN deployment role
import { Statement } from 'cdk-iam-floyd';
const policy = {
Version: '2012-10-17',
Statement: [
new Statement.Cloudformation() // allow all CFN actions
.allow()
.allActions(),
new Statement.All() // allow everything triggered via CFN
.allow()
.allActions()
.ifAwsCalledVia('cloudformation.amazonaws.com'),
new Statement.S3() // allow CDK staging bucket
.allow()
.allActions()
.on('arn:aws:s3:::cdktoolkit-stagingbucket-*'),
new Statement.Account() // block account-level changes
.deny()
.allPermissionManagementActions()
.allWriteActions(),
new Statement.Organizations() // block org-level changes
.deny()
.allPermissionManagementActions()
.allWriteActions(),
],
};
Tips & gotchas
- Pin the version — pre-1.0.0, breaking changes can happen in minor releases. Actions can be deleted or renamed by AWS; your code will break if you were using them.
- Multiple conditions = AND — to express OR, use multiple statements.
allMatchingActions/ access-level methods compile at call time — if AWS adds new actions later, re-run your code to pick them up.in*()must come beforeon*()—inAccount()/inRegion()set defaults for subsequently added resources only.- CDK assume-role policies: don't use
for*()methods for this — CDK'siam.PolicyDocumentwith a proper principal type works better with CDK grants. - Policy size limits:
allWriteActions()on services like EC2 can produce very large action lists. Use.compact()to compress them to wildcard patterns, or be more specific withto*()calls. Statement.Allis a special class that producesAction: "*"— useful for "allow anything called via CloudFormation" style statements.- Policy Converter: if you have an existing JSON policy, use the online converter at https://iam-floyd.readthedocs.io/en/latest/policy-converter.html to get the equivalent iam-floyd code.
- Non-chaining style is supported if you prefer it:
const s = new Statement.Ec2(); s.allow(); s.toStartInstances();
API reference
Full documentation: https://iam-floyd.readthedocs.io/en/latest/