To find the IAM actions your AWS SDK for JavaScript code needs, list every Command the code imports and map each one to service-prefix:OperationName, so GetItemCommand becomes dynamodb:GetItem. Then apply the exceptions: S3 list calls need s3:ListBucket, HeadObject needs s3:GetObject, multipart uploads need s3:PutObject, and KMS keys and iam:PassRole add permissions no Command names.
You have Node.js or TypeScript code that works on your laptop with an admin profile, and now it needs a role of its own. This guide shows how to find IAM actions in AWS SDK JavaScript code reliably: the naming rule that covers most calls, the exceptions where the Command and the action differ, and a small script that scans a project and prints a candidate list.
It’s the step before a policy review. Once you have the list, our checklist to review a generated IAM policy for least privilege takes over for resources, conditions and validation.
How do you find IAM actions from SDK v3 Commands?
In SDK v3 every API call is a Command class named after the API operation: ListTablesCommand calls ListTables. For most services the IAM action is the operation name with the service prefix in front. The prefix comes from the package, and it isn’t always the package name:
| Package | IAM prefix | Example Command → action |
|---|---|---|
@aws-sdk/client-s3 |
s3 |
PutObjectCommand → s3:PutObject |
@aws-sdk/client-dynamodb, @aws-sdk/lib-dynamodb |
dynamodb |
QueryCommand → dynamodb:Query |
@aws-sdk/client-sqs |
sqs |
SendMessageCommand → sqs:SendMessage |
@aws-sdk/client-cloudwatch-logs |
logs |
FilterLogEventsCommand → logs:FilterLogEvents |
@aws-sdk/client-secrets-manager |
secretsmanager |
GetSecretValueCommand → secretsmanager:GetSecretValue |
@aws-sdk/client-cost-explorer |
ce |
GetCostAndUsageCommand → ce:GetCostAndUsage |
@aws-sdk/client-sfn |
states |
StartExecutionCommand → states:StartExecution |
The document client in @aws-sdk/lib-dynamodb shortens names, so translate them back: GetCommand is GetItem, PutCommand is PutItem, UpdateCommand is UpdateItem, DeleteCommand is DeleteItem, and BatchGetCommand and BatchWriteCommand are BatchGetItem and BatchWriteItem. Amazon Bedrock has a similar mismatch: ConverseCommand needs bedrock:InvokeModel and ConverseStreamCommand needs bedrock:InvokeModelWithResponseStream, as the policy in the guide to call Bedrock models with AWS SDK v3 shows.
The authoritative list for any service is the Service Authorization Reference for AWS actions, resources and condition keys. When a name doesn’t match, look the operation up there before you guess.
Which Commands don’t match their IAM action?
S3 is where the naming rule breaks most often. Amazon S3’s page of required permissions for Amazon S3 API operations maps each operation to its action; these are the mismatches you’ll meet in application code:
| Command | IAM action | Resource |
|---|---|---|
ListObjectsV2Command, ListObjectsCommand |
s3:ListBucket |
Bucket ARN |
HeadBucketCommand |
s3:ListBucket |
Bucket ARN |
HeadObjectCommand, GetObjectAttributesCommand |
s3:GetObject |
Object ARN (bucket/*) |
ListBucketsCommand |
s3:ListAllMyBuckets |
* |
CreateMultipartUpload, UploadPart, CompleteMultipartUpload |
s3:PutObject |
Object ARN |
ListMultipartUploadsCommand |
s3:ListBucketMultipartUploads |
Bucket ARN |
ListPartsCommand |
s3:ListMultipartUploadParts |
Object ARN |
DeleteObjectsCommand |
s3:DeleteObject (or s3:DeleteObjectVersion with version IDs) |
Object ARN |
CopyObjectCommand |
s3:GetObject on the source, s3:PutObject on the destination |
Both object ARNs |
GetObjectCommand with VersionId |
s3:GetObjectVersion |
Object ARN |
Outside S3, three more are worth memorising. Lambda’s InvokeCommand needs lambda:InvokeFunction. DynamoDB’s TransactWriteItemsCommand has no action of its own: it needs the action for each item operation inside it (dynamodb:PutItem, UpdateItem, DeleteItem, and dynamodb:ConditionCheckItem for condition checks). And STS GetCallerIdentityCommand needs no permission at all; AWS documents that it works even under an explicit deny.
Tip: HeadObject on a missing key returns 404 only if the caller also has s3:ListBucket. Without it, S3 returns 403, which looks like a permissions bug in “does this file exist?” code. Our example to check if an S3 object exists in TypeScript handles both responses.
Permissions no Command names: KMS and PassRole
KMS for encrypted objects
If a bucket or object uses SSE-KMS with a customer managed key, S3 calls KMS on the caller’s behalf, so the caller needs the KMS action too: kms:GenerateDataKey to write (PutObject, CreateMultipartUpload, the destination of CopyObject) and kms:Decrypt to read (GetObject, the source of CopyObject) and to complete a multipart upload. Grant these on the key ARN in an IAM policy; S3 bucket policies can’t contain KMS actions, and the key policy must also allow the caller. Nothing in the code says “KMS”, which is why this is the most common surprise AccessDenied. Secrets Manager works the same way with customer managed keys, as the guide to get a Secrets Manager secret value with AWS SDK v3 shows.
iam:PassRole when code hands a role to a service
Any Command that takes a role ARN, such as Lambda’s CreateFunctionCommand with Role or EC2’s RunInstancesCommand with an instance profile, needs iam:PassRole on that role. PassRole is a permission, not an API call, so it never appears in CloudTrail and activity-based policy generators can’t see it. Scope it to the role ARN and, where possible, to the service with the iam:PassedToService condition key.
Paginators, waiters and helper libraries
- Paginators (
paginateListObjectsV2,paginateQuery) call the same operation repeatedly. They need the operation’s action and nothing more. - Waiters poll a describe or head call.
waitUntilObjectExistscallsHeadObject, so it needss3:GetObject;waitUntilTableExistsneedsdynamodb:DescribeTable;waitUntilInstanceRunningneedsec2:DescribeInstances. Uploadfrom@aws-sdk/lib-storageusesPutObjectfor small bodies and multipart calls for large ones, which all map tos3:PutObject. On failure it aborts the upload, so adds3:AbortMultipartUpload. See it in context in our example to upload a file to S3 with S3Client in TypeScript, and for streams of unknown length in the example to upload large files and streams to S3 with AWS SDK v3.- Presigned URLs are signed locally, with no API call. The permission is checked when someone uses the URL, against the credentials that signed it, so the signer needs
s3:GetObject(downloads) ors3:PutObject(uploads). Both directions are covered in creating a presigned S3 download URL with AWS SDK v3 and creating a presigned S3 upload URL with SDK v3.
Script: find IAM actions in AWS SDK JavaScript code automatically
- Save the scriptPut
find-iam-actions.mjsin the project root. It uses only Node.js built-ins. - Run it on your source folder
node find-iam-actions.mjs srcreads every.tsand.jsfile and parses the@aws-sdkimports. - Read the notesLines starting with
NOTE:flag KMS, PassRole and unknown waiters for a manual check. - Add resourcesThe script gives actions only. Scope each one to ARNs before it goes into a policy.
// List candidate IAM actions for AWS SDK for JavaScript v3 code.
// Usage: node find-iam-actions.mjs src
import { readdir, readFile } from "node:fs/promises";
import { join, extname } from "node:path";
const PREFIX = {
"client-s3": "s3",
"client-dynamodb": "dynamodb",
"lib-dynamodb": "dynamodb",
"client-lambda": "lambda",
"client-sqs": "sqs",
"client-sns": "sns",
"client-sts": "sts",
"client-kms": "kms",
"client-ec2": "ec2",
"client-cloudwatch": "cloudwatch",
"client-cloudwatch-logs": "logs",
"client-secrets-manager": "secretsmanager",
"client-cost-explorer": "ce",
"client-sfn": "states",
};
// Operations whose IAM action is not "prefix:OperationName".
const EXCEPTIONS = {
"s3:ListObjectsV2": ["s3:ListBucket"],
"s3:ListObjects": ["s3:ListBucket"],
"s3:HeadBucket": ["s3:ListBucket"],
"s3:HeadObject": ["s3:GetObject"],
"s3:GetObjectAttributes": ["s3:GetObject"],
"s3:ListBuckets": ["s3:ListAllMyBuckets"],
"s3:CreateMultipartUpload": ["s3:PutObject"],
"s3:UploadPart": ["s3:PutObject"],
"s3:CompleteMultipartUpload": ["s3:PutObject"],
"s3:ListMultipartUploads": ["s3:ListBucketMultipartUploads"],
"s3:ListParts": ["s3:ListMultipartUploadParts"],
"s3:DeleteObjects": ["s3:DeleteObject"],
"s3:CopyObject": ["s3:GetObject", "s3:PutObject"],
"lambda:Invoke": ["lambda:InvokeFunction"],
"dynamodb:TransactWriteItems": ["dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:ConditionCheckItem"],
"dynamodb:TransactGetItems": ["dynamodb:GetItem"],
"sts:GetCallerIdentity": [],
};
// Document-client names in @aws-sdk/lib-dynamodb and the operation each one calls.
const DOC_CLIENT = {
Get: "GetItem", Put: "PutItem", Update: "UpdateItem", Delete: "DeleteItem",
Query: "Query", Scan: "Scan", BatchGet: "BatchGetItem", BatchWrite: "BatchWriteItem",
TransactGet: "TransactGetItems", TransactWrite: "TransactWriteItems",
};
// Waiters and the operation they poll.
const WAITERS = {
ObjectExists: "HeadObject", ObjectNotExists: "HeadObject",
BucketExists: "HeadBucket", BucketNotExists: "HeadBucket",
TableExists: "DescribeTable", TableNotExists: "DescribeTable",
InstanceRunning: "DescribeInstances", InstanceStopped: "DescribeInstances",
};
const IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*["']@aws-sdk\/([a-z0-9-]+)["']/g;
const found = new Map();
const notes = new Set();
async function* sourceFiles(dir) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) yield* sourceFiles(path);
} else if ([".ts", ".tsx", ".js", ".mjs", ".cjs"].includes(extname(entry.name))) {
yield path;
}
}
}
function add(prefix, operation, where) {
const key = `${prefix}:${operation}`;
const actions = EXCEPTIONS[key] ?? [key];
if (actions.length === 0) notes.add(`${key} needs no IAM permission (${where}).`);
for (const action of actions) {
if (!found.has(action)) found.set(action, new Set());
found.get(action).add(where);
}
}
for await (const file of sourceFiles(process.argv[2] ?? "src")) {
const text = await readFile(file, "utf8");
const prefixes = new Set();
for (const [, names, pkg] of text.matchAll(IMPORT_RE)) {
if (pkg === "lib-storage") {
if (/\bUpload\b/.test(names)) {
add("s3", "PutObject", `${file}: Upload`);
add("s3", "AbortMultipartUpload", `${file}: Upload`);
}
continue;
}
const prefix = PREFIX[pkg];
if (!prefix) {
notes.add(`No prefix mapping for @aws-sdk/${pkg}; look it up in the Service Authorization Reference.`);
continue;
}
prefixes.add(prefix);
for (const raw of names.split(",")) {
const name = raw.trim().split(/\s+as\s+/)[0];
const docName = (n) => (pkg === "lib-dynamodb" ? DOC_CLIENT[n] ?? n : n);
let m;
if ((m = name.match(/^(\w+)Command$/))) {
add(prefix, docName(m[1]), `${file}: ${name}`);
} else if ((m = name.match(/^paginate(\w+)$/))) {
add(prefix, docName(m[1]), `${file}: ${name}`);
} else if ((m = name.match(/^waitUntil(\w+)$/))) {
if (WAITERS[m[1]]) add(prefix, WAITERS[m[1]], `${file}: ${name}`);
else notes.add(`${name} in ${file}: check which operation this waiter polls.`);
}
}
}
if (prefixes.has("s3") && /SSEKMSKeyId|ServerSideEncryption/.test(text)) {
notes.add(`${file} sets S3 encryption: with a customer managed KMS key, add kms:GenerateDataKey and kms:Decrypt on the key.`);
}
if (/\bRole\s*:|IamInstanceProfile|RoleArn\s*:/.test(text)) {
notes.add(`${file} passes a role ARN: the caller needs iam:PassRole on that role.`);
}
}
for (const action of [...found.keys()].sort()) {
console.log(action.padEnd(32), [...found.get(action)].join(", "));
}
for (const note of notes) console.log(`NOTE: ${note}`);
Run against a small reporting service, it prints something like this:
$ node find-iam-actions.mjs src
dynamodb:GetItem src/db.ts: GetCommand
dynamodb:Query src/db.ts: QueryCommand, src/db.ts: paginateQuery
s3:AbortMultipartUpload src/export.ts: Upload
s3:GetObject src/reports.ts: GetObjectCommand, src/reports.ts: waitUntilObjectExists
s3:ListBucket src/reports.ts: paginateListObjectsV2
s3:PutObject src/export.ts: Upload
NOTE: src/export.ts sets S3 encryption: with a customer managed KMS key, add kms:GenerateDataKey and kms:Decrypt on the key.
From that list, the policy scopes each action to its resource. s3:ListBucket goes on the bucket ARN and object actions on bucket/*, a split that trips up many first drafts:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListReportsBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::acme-reports"
},
{
"Sid": "ReadWriteReportObjects",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:AbortMultipartUpload"],
"Resource": "arn:aws:s3:::acme-reports/*"
},
{
"Sid": "UseReportsKey",
"Effect": "Allow",
"Action": ["kms:GenerateDataKey", "kms:Decrypt"],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
},
{
"Sid": "OrdersTable",
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/orders"
}
]
}
What static scanning can’t see
- Dynamic calls. The script reads imports. It misses v3’s aggregated clients (
s3.getObject()),require()calls and Commands built from variables. Code that still uses v2 needs converting first; the AWS SDK v2 to v3 converter drafts that, and the plan to migrate a Node.js app from AWS SDK v2 to v3 covers what it leaves to you. - Resources and conditions. An action list says what, not where. Bucket names, table ARNs and key ARNs come from your configuration.
- Resource policies. A bucket policy, key policy or SCP can still deny what your identity policy allows.
- Describe actions. Many
Describe*andList*actions, such asec2:DescribeInstances, don’t support resource-level permissions and need"Resource": "*".
Test the result the honest way: run the code under the new role. If something fails, the error names the action, and the method in troubleshooting AWS IAM access denied errors step by step finds which policy blocked it. To see what a role currently allows, the example to check the permissions of your currently assumed IAM role lists its attached and inline policies.
Where ChatWithCloud fits
The free IAM policy generator for TypeScript and JavaScript code drafts a policy from pasted code, including the resource split above. There are versions for boto3 Python code and AWS SDK for Go code too. If you’re porting a boto3 script to Node.js with SDK v3, the actions stay the same. It works only from the code you paste: it can’t see bucket encryption settings, key policies or roles your code passes at runtime, so apply the KMS and PassRole checks yourself and review every draft.
The ChatWithCloud CLI answers questions about an account, such as “which buckets use a KMS key?”, with your own AWS profile, as described in how ChatWithCloud runs AWS calls on your machine. It runs changes without asking first, so point it at a read-only profile; connecting ChatWithCloud to an AWS profile, SSO or role shows how.
Frequently asked questions
What IAM permission does ListObjectsV2 need?
s3:ListBucket on the bucket ARN (arn:aws:s3:::bucket-name), not on bucket-name/*. There is no s3:ListObjectsV2 action.
What permission does HeadObject need?
s3:GetObject on the object ARN. Add s3:ListBucket if your code needs a 404 rather than a 403 for missing keys.
Can I find IAM actions in AWS SDK JavaScript code from CloudTrail instead?
Partly. Activity-based generation only sees calls that actually ran and were logged. It can’t see iam:PassRole, which isn’t an API call, and S3 object-level calls are logged only if you enable data events.
Does SDK v2 code need different IAM actions than v3?
No. Both call the same API operations. Only the way you spot the operation changes: v2 uses method names like getObject, v3 uses GetObjectCommand.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud