If you want to check locally that your IAM policies allow and deny what you think they do, you can
use the simulated IAM service in Yulin. Sim IAM stores simulated Roles, Users and Policies, and evaluates allow and deny decisions for them.
The other simulated services use it to authorise their own actions, so a simulated S3 call from a
caller without the right permissions is denied in the same way real S3 would deny it. Here’s a simulated S3 Bucket with two Objects in it, and a Role which is allowed to read only one of
them: import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
import {
CreateBucketCommand,
GetObjectCommand,
PutObjectCommand,
} from "@aws-sdk/client-s3";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws({ defaultAccountId: "111111111111" });
const simIam = simAws.iam();
const simS3 = simAws.region("eu-west-2").s3();
await simS3.createBucket(new CreateBucketCommand({ Bucket: "foo-bucket" }));
await simS3.putObject(
new PutObjectCommand({
Bucket: "foo-bucket",
Key: "public/foobar.txt",
Body: "foobar",
}),
);
await simS3.putObject(
new PutObjectCommand({
Bucket: "foo-bucket",
Key: "private/foobar.txt",
Body: "foobar",
}),
);
const createRoleOutput = await simIam.createRole(
new CreateRoleCommand({
RoleName: "FooReaderRole",
AssumeRolePolicyDocument: JSON.stringify({
Version: "2012-10-17",
Statement: {
Effect: "Allow",
Principal: { AWS: "arn:aws:iam::111111111111:root" },
Action: "sts:AssumeRole",
},
}),
}),
);
const fooReaderRoleArn = createRoleOutput.Role.Arn;
await simIam.putRolePolicy(
new PutRolePolicyCommand({
RoleName: "FooReaderRole",
PolicyName: "ReadPublicObjects",
PolicyDocument: JSON.stringify({
Version: "2012-10-17",
Statement: {
Effect: "Allow",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::foo-bucket/public/*",
},
}),
}),
);
Note that Yulin can use the real command classes from the AWS JS SDK, so the policy documents are
the same JSON you would deploy. Simulated service commands take an optional caller alongside the SDK command, saying who is making
the request. The Role policy allows s3:GetObject on arn:aws:s3:::foo-bucket/public/*, so reading
that Object as the Role works: const output = await simS3.getObject(
new GetObjectCommand({
Bucket: "foo-bucket",
Key: "public/foobar.txt",
}),
{
caller: { kind: "arn", arn: fooReaderRoleArn },
},
);
Reading the other Object as the same Role throws instead: await simS3.getObject(
new GetObjectCommand({
Bucket: "foo-bucket",
Key: "private/foobar.txt",
}),
{
caller: { kind: "arn", arn: fooReaderRoleArn },
},
);
AccessDenied: User: arn:aws:iam::111111111111:role/FooReaderRole
is not authorized to perform:
s3:GetObject on resource: arn:aws:s3:::foo-bucket/private/foobar.txt
That error has a 403 status code and carries the attempted action, resource and caller for
diagnostics. Authorisation happens before the Object lookup, so a denied read of a key that doesn’t
exist gets the same access denied error rather than revealing that the key is missing. Omitting the caller defaults to the Account root, which is allowed within its own Account. Tests
which don’t care about IAM keep working as they did. Passing a caller to each command works when a test is driving sim S3 directly, but application code
uses the AWS SDK and knows nothing about simulated callers. For that, Yulin can
intercept the SDK client and attach an ambient
caller for the duration of a function call: import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { SimAws } from "@kensio/yulin";
import { SimSdk } from "@kensio/yulin/sdk";
const simAws = new SimAws({ defaultAccountId: "111111111111" });
const simSdk = new SimSdk({ simAws });
simSdk.intercept(S3Client);
// Ordinary application code, with no knowledge of the simulator.
async function readObject(key: string): Promise<string> {
const s3Client = new S3Client({ region: "eu-west-2" });
const output = await s3Client.send(
new GetObjectCommand({ Bucket: "foo-bucket", Key: key }),
);
return (await output.Body?.transformToString()) ?? "";
}
await simAws.runAs(
{ kind: "arn", arn: "arn:aws:iam::111111111111:role/FooReaderRole" },
async () => {
console.log(await readObject("public/foobar.txt")); // "foobar"
await readObject("private/foobar.txt"); // AccessDenied
},
);
simSdk.restoreAll();
Commands sent inside runAs() are attributed to that caller and authorised against its simulated
IAM permissions. The readObject() function is ordinary SDK code, and nothing in it changes when it
runs against the simulator. Yulin v0.34.2 is still a beta version, so the exact API might change a bit in future releases. Sim IAM evaluates identity policies, resource policies such as S3 Bucket policies, and policy
conditions, with explicit deny taking precedence. It doesn’t cover the whole of IAM: permissions
boundaries, session policies and service control policies aren’t evaluated, and only some condition
operators are supported. An unsupported condition operator fails closed and doesn’t match, so a
policy Yulin can’t fully evaluate denies rather than allows. Both the allowed and the denied paths run in the same Node.js process as the rest of the test, so
the whole thing stays in one debugger session and one coverage run. View post:
Simulating AWS IAM authorization of S3 calls locally with Yulin |