Find the IAM Actions Your AWS SDK for JavaScript Code Needs

Close-up of a server rack with rows of small green and blue status lights

Photo by Tyler on Unsplash

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. waitUntilObjectExists calls HeadObject, so it needs s3:GetObject; waitUntilTableExists needs dynamodb:DescribeTable; waitUntilInstanceRunning needs ec2:DescribeInstances.
  • Upload from @aws-sdk/lib-storage uses PutObject for small bodies and multipart calls for large ones, which all map to s3:PutObject. On failure it aborts the upload, so add s3: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) or s3: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

  1. Save the scriptPut find-iam-actions.mjs in the project root. It uses only Node.js built-ins.
  2. Run it on your source foldernode find-iam-actions.mjs src reads every .ts and .js file and parses the @aws-sdk imports.
  3. Read the notesLines starting with NOTE: flag KMS, PassRole and unknown waiters for a manual check.
  4. Add resourcesThe script gives actions only. Scope each one to ARNs before it goes into a policy.
find-iam-actions.mjs

// 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:

Terminal

$ 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:

reports-service-policy.json

{
  "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* and List* actions, such as ec2: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