1import * as AWS from "@/AWS";
2import { Policy } from "@/AWS/IAM";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as IAM from "@distilled.cloud/aws/iam";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11test.provider(
12 "create, update, and delete managed policy",
13 (stack) =>
14 Effect.gen(function* () {
15 yield* stack.destroy();
16
17 const policy = yield* stack.deploy(
18 Effect.gen(function* () {
19 return yield* Policy("IamPolicy", {
20 policyDocument: {
21 Version: "2012-10-17",
22 Statement: [
23 {
24 Effect: "Allow",
25 Action: ["s3:ListBucket"],
26 Resource: ["*"],
27 },
28 ],
29 },
30 tags: {
31 env: "test",
32 },
33 });
34 }),
35 );
36
37 const created = yield* IAM.getPolicy({
38 PolicyArn: policy.policyArn,
39 });
40 expect(created.Policy?.PolicyName).toBe(policy.policyName);
41
42 yield* stack.deploy(
43 Effect.gen(function* () {
44 return yield* Policy("IamPolicy", {
45 policyDocument: {
46 Version: "2012-10-17",
47 Statement: [
48 {
49 Effect: "Allow",
50 Action: ["s3:GetObject"],
51 Resource: ["*"],
52 },
53 ],
54 },
55 tags: {
56 env: "prod",
57 },
58 });
59 }),
60 );
61
62 const updatedTags = yield* IAM.listPolicyTags({
63 PolicyArn: policy.policyArn,
64 });
65 expect(
66 Object.fromEntries(
67 (updatedTags.Tags ?? []).map((tag) => [tag.Key, tag.Value]),
68 ),
69 ).toMatchObject({
70 env: "prod",
71 });
72
73 yield* stack.destroy();
74
75 const deleted = yield* IAM.getPolicy({
76 PolicyArn: policy.policyArn,
77 }).pipe(Effect.option);
78 expect(deleted._tag).toBe("None");
79 }),
80 { tags: ["provider:aws", "provider:aws:iam", "live"] },
81);
82
83test.provider(
84 "rotates policy versions past the 5-version cap",
85 (stack) =>
86 Effect.gen(function* () {
87 yield* stack.destroy();
88
89 const deployWith = (actions: [string, ...string[]]) =>
90 stack.deploy(
91 Effect.gen(function* () {
92 return yield* Policy("VersionCapPolicy", {
93 policyDocument: {
94 Version: "2012-10-17",
95 Statement: [
96 {
97 Effect: "Allow",
98 Action: actions,
99 Resource: ["*"],
100 },
101 ],
102 },
103 });
104 }),
105 );
106
107 // 1 create + 6 document updates = 7 versions requested; IAM caps
108 // managed policies at 5 versions, so the provider must prune the
109 // oldest non-default version before each rotation past the cap.
110 const documents: [string, ...string[]][] = [
111 ["s3:ListBucket"],
112 ["s3:GetObject"],
113 ["s3:PutObject"],
114 ["s3:DeleteObject"],
115 ["s3:GetObjectTagging"],
116 ["s3:PutObjectTagging"],
117 ["s3:GetBucketLocation"],
118 ];
119 let policy: { policyArn: string } | undefined;
120 for (const actions of documents) {
121 policy = yield* deployWith(actions);
122 }
123
124 const versions = yield* IAM.listPolicyVersions({
125 PolicyArn: policy!.policyArn,
126 });
127 expect(versions.Versions?.length ?? 0).toBeLessThanOrEqual(5);
128
129 // The default version must carry the last document.
130 const defaultVersion = versions.Versions?.find(
131 (version) => version.IsDefaultVersion,
132 );
133 expect(defaultVersion?.VersionId).toBeDefined();
134 const document = yield* IAM.getPolicyVersion({
135 PolicyArn: policy!.policyArn,
136 VersionId: defaultVersion!.VersionId!,
137 });
138 const decoded = JSON.parse(
139 decodeURIComponent(document.PolicyVersion?.Document ?? ""),
140 ) as { Statement: [{ Action: string[] }] };
141 expect(decoded.Statement[0].Action).toEqual(["s3:GetBucketLocation"]);
142
143 yield* stack.destroy();
144
145 const deleted = yield* IAM.getPolicy({
146 PolicyArn: policy!.policyArn,
147 }).pipe(Effect.option);
148 expect(deleted._tag).toBe("None");
149 }),
150 { tags: ["provider:aws", "provider:aws:iam", "live"], timeout: 120_000 },
151);
152
153test.provider(
154 "list enumerates the deployed policy",
155 (stack) =>
156 Effect.gen(function* () {
157 yield* stack.destroy();
158
159 const deployed = yield* stack.deploy(
160 Effect.gen(function* () {
161 return yield* Policy("ListPolicy", {
162 policyDocument: {
163 Version: "2012-10-17",
164 Statement: [
165 {
166 Effect: "Allow",
167 Action: ["s3:ListBucket"],
168 Resource: ["*"],
169 },
170 ],
171 },
172 });
173 }),
174 );
175
176 const provider = yield* Provider.findProvider(Policy);
177 const all = yield* provider.list();
178
179 expect(all.some((p) => p.policyArn === deployed.policyArn)).toBe(true);
180
181 yield* stack.destroy();
182 }),
183 { tags: ["provider:aws", "provider:aws:iam", "live"] },
184);