| 1 | import * as Lambda from "@/AWS/Lambda"; |
| 2 | import * as RAM from "@/AWS/RAM"; |
| 3 | import * as Duration from "effect/Duration"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 7 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 8 | import path from "pathe"; |
| 9 | |
| 10 | const main = path.resolve(import.meta.dirname, "handler.ts"); |
| 11 | |
| 12 | /** |
| 13 | * Fabricate a well-formed same-account invitation ARN that cannot exist — |
| 14 | * drives the typed not-found path for the invitation bindings, proving the |
| 15 | * grant reaches the API (an IAM gap would surface AccessDeniedException |
| 16 | * instead). |
| 17 | */ |
| 18 | const fakeInvitationArn = (shareArn: string) => { |
| 19 | const [, , , region, accountId] = shareArn.split(":"); |
| 20 | return `arn:aws:ram:${region}:${accountId}:resource-share-invitation/00000000-0000-4000-8000-000000000000`; |
| 21 | }; |
| 22 | |
| 23 | /** A well-formed same-account subnet ARN that cannot exist. */ |
| 24 | const fakeSubnetArn = (shareArn: string) => { |
| 25 | const [, , , region, accountId] = shareArn.split(":"); |
| 26 | return `arn:aws:ec2:${region}:${accountId}:subnet/subnet-00000000000000000`; |
| 27 | }; |
| 28 | |
| 29 | export class RAMTestFunction extends Lambda.Function<Lambda.Function>()( |
| 30 | "RAMTestFunction", |
| 31 | ) {} |
| 32 | |
| 33 | export default RAMTestFunction.make( |
| 34 | { |
| 35 | main, |
| 36 | functionUrl: true, |
| 37 | timeout: Duration.seconds(30), |
| 38 | }, |
| 39 | Effect.gen(function* () { |
| 40 | // A resource share owned by the fixture — guarantees that |
| 41 | // GetResourceShares(SELF) is non-empty and provides the region/account |
| 42 | // for the fabricated not-found ARNs. |
| 43 | yield* RAM.ResourceShare("BindingsShare", { |
| 44 | allowExternalPrincipals: true, |
| 45 | }); |
| 46 | |
| 47 | const acceptInvitation = yield* RAM.AcceptResourceShareInvitation(); |
| 48 | const rejectInvitation = yield* RAM.RejectResourceShareInvitation(); |
| 49 | const getInvitations = yield* RAM.GetResourceShareInvitations(); |
| 50 | const listPendingResources = yield* RAM.ListPendingInvitationResources(); |
| 51 | const getResourceShares = yield* RAM.GetResourceShares(); |
| 52 | const getAssociations = yield* RAM.GetResourceShareAssociations(); |
| 53 | const listResources = yield* RAM.ListResources(); |
| 54 | const listPrincipals = yield* RAM.ListPrincipals(); |
| 55 | const getResourcePolicies = yield* RAM.GetResourcePolicies(); |
| 56 | const getPermission = yield* RAM.GetPermission(); |
| 57 | const listPermissions = yield* RAM.ListPermissions(); |
| 58 | |
| 59 | const bound = { |
| 60 | acceptInvitation, |
| 61 | rejectInvitation, |
| 62 | getInvitations, |
| 63 | listPendingResources, |
| 64 | getResourceShares, |
| 65 | getAssociations, |
| 66 | listResources, |
| 67 | listPrincipals, |
| 68 | getResourcePolicies, |
| 69 | getPermission, |
| 70 | listPermissions, |
| 71 | }; |
| 72 | |
| 73 | // The fixture's own share doubles as the region/account source for |
| 74 | // fabricated ARNs. |
| 75 | const firstShareArn = getResourceShares({ resourceOwner: "SELF" }).pipe( |
| 76 | Effect.map((r) => r.resourceShares?.[0]?.resourceShareArn), |
| 77 | ); |
| 78 | |
| 79 | return { |
| 80 | fetch: Effect.gen(function* () { |
| 81 | const request = yield* HttpServerRequest; |
| 82 | const url = new URL(request.originalUrl); |
| 83 | const pathname = url.pathname; |
| 84 | |
| 85 | if (request.method === "GET" && pathname === "/bindings") { |
| 86 | return yield* HttpServerResponse.json({ |
| 87 | bound: Object.keys(bound), |
| 88 | }); |
| 89 | } |
| 90 | |
| 91 | if (request.method === "GET" && pathname === "/shares") { |
| 92 | const result = yield* getResourceShares({ resourceOwner: "SELF" }); |
| 93 | return yield* HttpServerResponse.json({ |
| 94 | count: (result.resourceShares ?? []).length, |
| 95 | }); |
| 96 | } |
| 97 | |
| 98 | if (request.method === "GET" && pathname === "/associations") { |
| 99 | const shareArn = yield* firstShareArn; |
| 100 | if (shareArn === undefined) { |
| 101 | return yield* HttpServerResponse.json({ tag: "NoShares" }); |
| 102 | } |
| 103 | const result = yield* getAssociations({ |
| 104 | associationType: "PRINCIPAL", |
| 105 | resourceShareArns: [shareArn], |
| 106 | }); |
| 107 | return yield* HttpServerResponse.json({ |
| 108 | tag: "Ok", |
| 109 | count: (result.resourceShareAssociations ?? []).length, |
| 110 | }); |
| 111 | } |
| 112 | |
| 113 | if (request.method === "GET" && pathname === "/invitations") { |
| 114 | const result = yield* getInvitations(); |
| 115 | return yield* HttpServerResponse.json({ |
| 116 | count: (result.resourceShareInvitations ?? []).length, |
| 117 | }); |
| 118 | } |
| 119 | |
| 120 | if (request.method === "GET" && pathname === "/accept-nonexistent") { |
| 121 | const shareArn = yield* firstShareArn; |
| 122 | if (shareArn === undefined) { |
| 123 | return yield* HttpServerResponse.json({ tag: "NoShares" }); |
| 124 | } |
| 125 | // Typed not-found path — proves the AcceptResourceShareInvitation |
| 126 | // grant reaches the API. |
| 127 | const result = yield* acceptInvitation({ |
| 128 | resourceShareInvitationArn: fakeInvitationArn(shareArn), |
| 129 | }).pipe( |
| 130 | Effect.map(() => "Ok"), |
| 131 | Effect.catchTag( |
| 132 | [ |
| 133 | "ResourceShareInvitationArnNotFoundException", |
| 134 | "MalformedArnException", |
| 135 | ], |
| 136 | (e) => Effect.succeed(e._tag), |
| 137 | ), |
| 138 | ); |
| 139 | return yield* HttpServerResponse.json({ tag: result }); |
| 140 | } |
| 141 | |
| 142 | if (request.method === "GET" && pathname === "/reject-nonexistent") { |
| 143 | const shareArn = yield* firstShareArn; |
| 144 | if (shareArn === undefined) { |
| 145 | return yield* HttpServerResponse.json({ tag: "NoShares" }); |
| 146 | } |
| 147 | const result = yield* rejectInvitation({ |
| 148 | resourceShareInvitationArn: fakeInvitationArn(shareArn), |
| 149 | }).pipe( |
| 150 | Effect.map(() => "Ok"), |
| 151 | Effect.catchTag( |
| 152 | [ |
| 153 | "ResourceShareInvitationArnNotFoundException", |
| 154 | "MalformedArnException", |
| 155 | ], |
| 156 | (e) => Effect.succeed(e._tag), |
| 157 | ), |
| 158 | ); |
| 159 | return yield* HttpServerResponse.json({ tag: result }); |
| 160 | } |
| 161 | |
| 162 | if ( |
| 163 | request.method === "GET" && |
| 164 | pathname === "/pending-resources-nonexistent" |
| 165 | ) { |
| 166 | const shareArn = yield* firstShareArn; |
| 167 | if (shareArn === undefined) { |
| 168 | return yield* HttpServerResponse.json({ tag: "NoShares" }); |
| 169 | } |
| 170 | const result = yield* listPendingResources({ |
| 171 | resourceShareInvitationArn: fakeInvitationArn(shareArn), |
| 172 | }).pipe( |
| 173 | Effect.map(() => "Ok"), |
| 174 | Effect.catchTag( |
| 175 | [ |
| 176 | "ResourceShareInvitationArnNotFoundException", |
| 177 | "MalformedArnException", |
| 178 | "InvalidParameterException", |
| 179 | ], |
| 180 | (e) => Effect.succeed(e._tag), |
| 181 | ), |
| 182 | ); |
| 183 | return yield* HttpServerResponse.json({ tag: result }); |
| 184 | } |
| 185 | |
| 186 | if (request.method === "GET" && pathname === "/resources") { |
| 187 | const result = yield* listResources({ resourceOwner: "SELF" }); |
| 188 | return yield* HttpServerResponse.json({ |
| 189 | count: (result.resources ?? []).length, |
| 190 | }); |
| 191 | } |
| 192 | |
| 193 | if (request.method === "GET" && pathname === "/principals") { |
| 194 | const result = yield* listPrincipals({ resourceOwner: "SELF" }); |
| 195 | return yield* HttpServerResponse.json({ |
| 196 | count: (result.principals ?? []).length, |
| 197 | }); |
| 198 | } |
| 199 | |
| 200 | if ( |
| 201 | request.method === "GET" && |
| 202 | pathname === "/resource-policies-nonexistent" |
| 203 | ) { |
| 204 | const shareArn = yield* firstShareArn; |
| 205 | if (shareArn === undefined) { |
| 206 | return yield* HttpServerResponse.json({ tag: "NoShares" }); |
| 207 | } |
| 208 | // A fabricated subnet ARN — either the typed not-found or an empty |
| 209 | // policy list proves the grant. |
| 210 | const result = yield* getResourcePolicies({ |
| 211 | resourceArns: [fakeSubnetArn(shareArn)], |
| 212 | }).pipe( |
| 213 | Effect.map((r) => ({ |
| 214 | tag: "Ok" as string, |
| 215 | count: (r.policies ?? []).length, |
| 216 | })), |
| 217 | Effect.catchTag( |
| 218 | ["ResourceArnNotFoundException", "MalformedArnException"], |
| 219 | (e) => Effect.succeed({ tag: e._tag as string, count: 0 }), |
| 220 | ), |
| 221 | ); |
| 222 | return yield* HttpServerResponse.json(result); |
| 223 | } |
| 224 | |
| 225 | if (request.method === "GET" && pathname === "/permissions") { |
| 226 | const result = yield* listPermissions(); |
| 227 | return yield* HttpServerResponse.json({ |
| 228 | count: (result.permissions ?? []).length, |
| 229 | }); |
| 230 | } |
| 231 | |
| 232 | if (request.method === "GET" && pathname === "/permission") { |
| 233 | const listed = yield* listPermissions({ maxResults: 5 }); |
| 234 | const first = (listed.permissions ?? [])[0]; |
| 235 | if (first?.arn === undefined) { |
| 236 | return yield* HttpServerResponse.json({ tag: "NoPermissions" }); |
| 237 | } |
| 238 | const result = yield* getPermission({ permissionArn: first.arn }); |
| 239 | return yield* HttpServerResponse.json({ |
| 240 | tag: "Ok", |
| 241 | name: result.permission?.name, |
| 242 | }); |
| 243 | } |
| 244 | |
| 245 | return yield* HttpServerResponse.json( |
| 246 | { error: "Not found", method: request.method, pathname }, |
| 247 | { status: 404 }, |
| 248 | ); |
| 249 | }).pipe(Effect.orDie), |
| 250 | }; |
| 251 | }).pipe( |
| 252 | Effect.provide( |
| 253 | Layer.mergeAll( |
| 254 | Lambda.EventSource, |
| 255 | RAM.AcceptResourceShareInvitationHttp, |
| 256 | RAM.RejectResourceShareInvitationHttp, |
| 257 | RAM.GetResourceShareInvitationsHttp, |
| 258 | RAM.ListPendingInvitationResourcesHttp, |
| 259 | RAM.GetResourceSharesHttp, |
| 260 | RAM.GetResourceShareAssociationsHttp, |
| 261 | RAM.ListResourcesHttp, |
| 262 | RAM.ListPrincipalsHttp, |
| 263 | RAM.GetResourcePoliciesHttp, |
| 264 | RAM.GetPermissionHttp, |
| 265 | RAM.ListPermissionsHttp, |
| 266 | ), |
| 267 | ), |
| 268 | ), |
| 269 | ); |