| 1 | import * as AWS from "@/AWS"; |
| 2 | import { ClientVpnAuthorizationRule } from "@/AWS/EC2/ClientVpnAuthorizationRule.ts"; |
| 3 | import { ClientVpnEndpoint } from "@/AWS/EC2/ClientVpnEndpoint.ts"; |
| 4 | import * as Alchemy from "@/index.ts"; |
| 5 | import * as Provider from "@/Provider"; |
| 6 | import * as Test from "@/Test/Alchemy"; |
| 7 | import * as ec2 from "@distilled.cloud/aws/ec2"; |
| 8 | import { expect } from "alchemy-test"; |
| 9 | import * as Effect from "effect/Effect"; |
| 10 | import { |
| 11 | assertClientVpnAuthorizationDeleted, |
| 12 | assertClientVpnCertificateDeleted, |
| 13 | clientVpnEndpointProps, |
| 14 | clientVpnTestTimeout, |
| 15 | importClientVpnCertificate, |
| 16 | readClientVpnAuthorizationRules, |
| 17 | waitForClientVpn, |
| 18 | } from "./fixtures/client-vpn.ts"; |
| 19 | |
| 20 | const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ |
| 21 | providers: AWS.providers(), |
| 22 | }); |
| 23 | const certificate = beforeAll( |
| 24 | importClientVpnCertificate("ClientVpnAuthorizationPrerequisites"), |
| 25 | ); |
| 26 | const Stack = Alchemy.Stack( |
| 27 | "ClientVpnAuthorizationPrerequisites", |
| 28 | { providers: AWS.providers(), state: Alchemy.localState() }, |
| 29 | Effect.gen(function* () { |
| 30 | const certificateArn = yield* certificate; |
| 31 | const endpoint = yield* ClientVpnEndpoint( |
| 32 | "Endpoint", |
| 33 | clientVpnEndpointProps(certificateArn), |
| 34 | ); |
| 35 | return { endpoint }; |
| 36 | }), |
| 37 | ); |
| 38 | const prerequisites = beforeAll(deploy(Stack), { |
| 39 | timeout: clientVpnTestTimeout, |
| 40 | }); |
| 41 | afterAll( |
| 42 | destroy(Stack).pipe( |
| 43 | Effect.andThen( |
| 44 | certificate.pipe(Effect.flatMap(assertClientVpnCertificateDeleted)), |
| 45 | ), |
| 46 | ), |
| 47 | { timeout: clientVpnTestTimeout }, |
| 48 | ); |
| 49 | |
| 50 | test.provider( |
| 51 | "creates, lists, replaces, and deletes authorization rules without a subnet association", |
| 52 | (stack) => |
| 53 | Effect.gen(function* () { |
| 54 | yield* stack.destroy(); |
| 55 | const { endpoint } = yield* prerequisites; |
| 56 | const clientVpnEndpointId = endpoint.clientVpnEndpointId; |
| 57 | const deployRule = (targetNetworkCidr: string, description?: string) => |
| 58 | stack.deploy( |
| 59 | ClientVpnAuthorizationRule("Authorization", { |
| 60 | clientVpnEndpointId, |
| 61 | targetNetworkCidr, |
| 62 | authorizeAllGroups: true, |
| 63 | description, |
| 64 | }), |
| 65 | ); |
| 66 | const created = yield* deployRule( |
| 67 | "10.171.0.0/16", |
| 68 | "Initial authorization", |
| 69 | ); |
| 70 | expect(created.clientVpnEndpointId).toBe(clientVpnEndpointId); |
| 71 | expect(created.targetNetworkCidr).toBe("10.171.0.0/16"); |
| 72 | const initial = yield* waitForClientVpn( |
| 73 | readClientVpnAuthorizationRules(clientVpnEndpointId), |
| 74 | (rules) => |
| 75 | rules.some( |
| 76 | (rule) => |
| 77 | rule.DestinationCidr === "10.171.0.0/16" && |
| 78 | rule.Status?.Code === "active", |
| 79 | ), |
| 80 | "active authorization", |
| 81 | ); |
| 82 | expect(initial).toContainEqual( |
| 83 | expect.objectContaining({ |
| 84 | DestinationCidr: "10.171.0.0/16", |
| 85 | AccessAll: true, |
| 86 | Description: "Initial authorization", |
| 87 | }), |
| 88 | ); |
| 89 | const provider = yield* Provider.findProvider(ClientVpnAuthorizationRule); |
| 90 | const listed = yield* waitForClientVpn( |
| 91 | provider.list(), |
| 92 | (rules) => |
| 93 | rules.some( |
| 94 | (rule) => |
| 95 | rule.clientVpnEndpointId === clientVpnEndpointId && |
| 96 | rule.targetNetworkCidr === "10.171.0.0/16", |
| 97 | ), |
| 98 | "authorization provider list", |
| 99 | ); |
| 100 | expect(listed).toContainEqual( |
| 101 | expect.objectContaining({ |
| 102 | clientVpnEndpointId, |
| 103 | targetNetworkCidr: "10.171.0.0/16", |
| 104 | authorizeAllGroups: true, |
| 105 | }), |
| 106 | ); |
| 107 | |
| 108 | yield* deployRule("10.171.0.0/16", "Initial authorization"); |
| 109 | expect( |
| 110 | (yield* readClientVpnAuthorizationRules(clientVpnEndpointId)).filter( |
| 111 | (rule) => |
| 112 | rule.DestinationCidr === "10.171.0.0/16" && |
| 113 | rule.Status?.Code === "active", |
| 114 | ), |
| 115 | ).toHaveLength(1); |
| 116 | |
| 117 | // AWS has no modify-authorization API; a description change must revoke and recreate. |
| 118 | yield* deployRule("10.171.0.0/16", "Updated authorization"); |
| 119 | const updated = yield* waitForClientVpn( |
| 120 | readClientVpnAuthorizationRules(clientVpnEndpointId), |
| 121 | (rules) => |
| 122 | rules.some( |
| 123 | (rule) => |
| 124 | rule.DestinationCidr === "10.171.0.0/16" && |
| 125 | rule.Description === "Updated authorization" && |
| 126 | rule.Status?.Code === "active", |
| 127 | ), |
| 128 | "replaced authorization description", |
| 129 | ); |
| 130 | expect( |
| 131 | updated.filter( |
| 132 | (rule) => |
| 133 | rule.DestinationCidr === "10.171.0.0/16" && |
| 134 | rule.Status?.Code === "active", |
| 135 | ), |
| 136 | ).toHaveLength(1); |
| 137 | |
| 138 | const replaced = yield* deployRule("10.172.0.0/16"); |
| 139 | expect(replaced.targetNetworkCidr).toBe("10.172.0.0/16"); |
| 140 | const replacement = yield* waitForClientVpn( |
| 141 | readClientVpnAuthorizationRules(clientVpnEndpointId), |
| 142 | (rules) => |
| 143 | rules.some( |
| 144 | (rule) => |
| 145 | rule.DestinationCidr === "10.172.0.0/16" && |
| 146 | rule.Status?.Code === "active", |
| 147 | ), |
| 148 | "replacement authorization CIDR", |
| 149 | ); |
| 150 | expect( |
| 151 | replacement.find( |
| 152 | (rule) => |
| 153 | rule.DestinationCidr === "10.172.0.0/16" && |
| 154 | rule.Status?.Code === "active", |
| 155 | )?.Description ?? "", |
| 156 | ).toBe(""); |
| 157 | yield* assertClientVpnAuthorizationDeleted( |
| 158 | clientVpnEndpointId, |
| 159 | "10.171.0.0/16", |
| 160 | ); |
| 161 | yield* stack.destroy(); |
| 162 | yield* assertClientVpnAuthorizationDeleted( |
| 163 | clientVpnEndpointId, |
| 164 | "10.172.0.0/16", |
| 165 | ); |
| 166 | }), |
| 167 | { |
| 168 | tags: ["provider:aws", "provider:aws:acm", "provider:aws:ec2", "live"], |
| 169 | timeout: clientVpnTestTimeout, |
| 170 | }, |
| 171 | ); |
| 172 | |
| 173 | test.provider( |
| 174 | "recreates an authorization rule revoked out of band", |
| 175 | (stack) => |
| 176 | Effect.gen(function* () { |
| 177 | yield* stack.destroy(); |
| 178 | const { endpoint } = yield* prerequisites; |
| 179 | const clientVpnEndpointId = endpoint.clientVpnEndpointId; |
| 180 | const targetNetworkCidr = "10.173.0.0/16"; |
| 181 | const program = ClientVpnAuthorizationRule("Authorization", { |
| 182 | clientVpnEndpointId, |
| 183 | targetNetworkCidr, |
| 184 | authorizeAllGroups: true, |
| 185 | description: "Managed authorization", |
| 186 | }); |
| 187 | yield* stack.deploy(program); |
| 188 | yield* ec2.revokeClientVpnIngress({ |
| 189 | ClientVpnEndpointId: clientVpnEndpointId, |
| 190 | TargetNetworkCidr: targetNetworkCidr, |
| 191 | RevokeAllGroups: true, |
| 192 | }); |
| 193 | yield* assertClientVpnAuthorizationDeleted( |
| 194 | clientVpnEndpointId, |
| 195 | targetNetworkCidr, |
| 196 | ); |
| 197 | yield* stack.deploy(program); |
| 198 | const repaired = yield* waitForClientVpn( |
| 199 | readClientVpnAuthorizationRules(clientVpnEndpointId), |
| 200 | (rules) => |
| 201 | rules.some( |
| 202 | (rule) => |
| 203 | rule.DestinationCidr === targetNetworkCidr && |
| 204 | rule.Status?.Code === "active", |
| 205 | ), |
| 206 | "recreated authorization", |
| 207 | ); |
| 208 | expect(repaired).toContainEqual( |
| 209 | expect.objectContaining({ |
| 210 | DestinationCidr: targetNetworkCidr, |
| 211 | AccessAll: true, |
| 212 | Description: "Managed authorization", |
| 213 | }), |
| 214 | ); |
| 215 | yield* stack.destroy(); |
| 216 | yield* assertClientVpnAuthorizationDeleted( |
| 217 | clientVpnEndpointId, |
| 218 | targetNetworkCidr, |
| 219 | ); |
| 220 | }), |
| 221 | { |
| 222 | tags: ["provider:aws", "provider:aws:acm", "provider:aws:ec2", "live"], |
| 223 | timeout: clientVpnTestTimeout, |
| 224 | }, |
| 225 | ); |
| 226 | |
| 227 | test.provider( |
| 228 | "discovers an authorization rule by natural key without prior state and deletes it", |
| 229 | (stack) => |
| 230 | Effect.gen(function* () { |
| 231 | yield* stack.destroy(); |
| 232 | const { endpoint } = yield* prerequisites; |
| 233 | const clientVpnEndpointId = endpoint.clientVpnEndpointId; |
| 234 | const targetNetworkCidr = "10.174.0.0/16"; |
| 235 | const description = "Cold natural-key discovery"; |
| 236 | yield* ec2.authorizeClientVpnIngress({ |
| 237 | ClientVpnEndpointId: clientVpnEndpointId, |
| 238 | TargetNetworkCidr: targetNetworkCidr, |
| 239 | AuthorizeAllGroups: true, |
| 240 | Description: description, |
| 241 | }); |
| 242 | const preexisting = yield* waitForClientVpn( |
| 243 | readClientVpnAuthorizationRules(clientVpnEndpointId), |
| 244 | (rules) => |
| 245 | rules.some( |
| 246 | (rule) => |
| 247 | rule.DestinationCidr === targetNetworkCidr && |
| 248 | rule.AccessAll === true && |
| 249 | rule.Status?.Code === "active", |
| 250 | ), |
| 251 | "SDK-created authorization", |
| 252 | ); |
| 253 | const original = preexisting.filter( |
| 254 | (rule) => |
| 255 | rule.DestinationCidr === targetNetworkCidr && rule.AccessAll === true, |
| 256 | ); |
| 257 | expect(original).toHaveLength(1); |
| 258 | |
| 259 | const managed = yield* stack.deploy( |
| 260 | ClientVpnAuthorizationRule("Authorization", { |
| 261 | clientVpnEndpointId, |
| 262 | targetNetworkCidr, |
| 263 | authorizeAllGroups: true, |
| 264 | description, |
| 265 | }), |
| 266 | ); |
| 267 | expect(managed).toMatchObject({ |
| 268 | clientVpnEndpointId, |
| 269 | targetNetworkCidr, |
| 270 | authorizeAllGroups: true, |
| 271 | description, |
| 272 | status: "active", |
| 273 | }); |
| 274 | const observed = |
| 275 | yield* readClientVpnAuthorizationRules(clientVpnEndpointId); |
| 276 | expect( |
| 277 | observed.filter( |
| 278 | (rule) => |
| 279 | rule.DestinationCidr === targetNetworkCidr && |
| 280 | rule.AccessAll === true, |
| 281 | ), |
| 282 | ).toEqual(original); |
| 283 | |
| 284 | yield* stack.destroy(); |
| 285 | yield* assertClientVpnAuthorizationDeleted( |
| 286 | clientVpnEndpointId, |
| 287 | targetNetworkCidr, |
| 288 | ); |
| 289 | }), |
| 290 | { |
| 291 | tags: ["provider:aws", "provider:aws:acm", "provider:aws:ec2", "live"], |
| 292 | timeout: clientVpnTestTimeout, |
| 293 | }, |
| 294 | ); |