1import * as vpclattice from "@distilled.cloud/aws/vpc-lattice";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import type { PolicyDocument } from "../IAM/Policy.ts";
7import {
8 normalizePolicyDocument,
9 stringifyPolicyDocument,
10} from "../IAM/Policy.ts";
11import type { Providers } from "../Providers.ts";
12
13export interface AuthPolicyProps {
14 /**
15 * The ID or ARN of the service network or service the auth policy applies
16 * to. Immutable — changing it replaces the resource.
17 */
18 resourceIdentifier: string;
19 /**
20 * The IAM auth policy controlling access to the service network or service,
21 * either as a structured {@link PolicyDocument} or a raw JSON string
22 * (escape hatch).
23 */
24 policy: PolicyDocument | string;
25}
26
27export interface AuthPolicy extends Resource<
28 "AWS.VpcLattice.AuthPolicy",
29 AuthPolicyProps,
30 {
31 /**
32 * ID or ARN of the service network or service the policy is attached to.
33 */
34 resourceIdentifier: string;
35 /**
36 * The attached policy document as a JSON string.
37 */
38 policy: string;
39 /**
40 * Policy state reported by the API (`Active` or `Inactive` — `Inactive`
41 * when the target's `authType` is `NONE`).
42 */
43 state: string | undefined;
44 },
45 never,
46 Providers
47> {}
48
49/**
50 * An auth policy for a VPC Lattice service network or service — the IAM
51 * resource policy evaluated on every request when the target's `authType`
52 * is `AWS_IAM`.
53 *
54 * ### Attaching Auth Policies
55 * **Example:** Allow Authenticated Invoke on a Service Network
56 * ```typescript
57 * const network = yield* ServiceNetwork("SecureNetwork", {
58 * authType: "AWS_IAM",
59 * });
60 * const authPolicy = yield* AuthPolicy("NetworkAuthPolicy", {
61 * resourceIdentifier: network.serviceNetworkId,
62 * policy: {
63 * Version: "2012-10-17",
64 * Statement: [
65 * {
66 * Effect: "Allow",
67 * Principal: { AWS: "*" },
68 * Action: ["vpc-lattice-svcs:Invoke"],
69 * Resource: "*",
70 * },
71 * ],
72 * },
73 * });
74 * ```
75 *
76 * **Example:** Raw JSON Escape Hatch
77 * ```typescript
78 * const authPolicy = yield* AuthPolicy("ServiceAuthPolicy", {
79 * resourceIdentifier: service.serviceId,
80 * policy: JSON.stringify({
81 * Version: "2012-10-17",
82 * Statement: [{ Effect: "Allow", Principal: "*", Action: "vpc-lattice-svcs:Invoke", Resource: "*" }],
83 * }),
84 * });
85 * ```
86 *
87 * @resource
88 */
89export const AuthPolicy = Resource<AuthPolicy>("AWS.VpcLattice.AuthPolicy");
90
91export const AuthPolicyProvider = () =>
92 Provider.effect(
93 AuthPolicy,
94 Effect.gen(function* () {
95 // A missing parent and a parent without an auth policy are both
96 // "no policy" — getAuthPolicy answers with an empty body for the
97 // latter, so treat an absent `policy` as non-existence.
98 const observe = (resourceIdentifier: string) =>
99 vpclattice.getAuthPolicy({ resourceIdentifier }).pipe(
100 Effect.map((response) =>
101 response.policy === undefined ? undefined : response,
102 ),
103 Effect.catchTag("ResourceNotFoundException", () =>
104 Effect.succeed(undefined),
105 ),
106 );
107
108 const toDesired = (policy: PolicyDocument | string) =>
109 typeof policy === "string" ? policy : stringifyPolicyDocument(policy);
110
111 return {
112 stables: ["resourceIdentifier"],
113 diff: Effect.fn(function* ({ olds, news }) {
114 if (!isResolved(news)) return;
115 if (olds && olds.resourceIdentifier !== news.resourceIdentifier) {
116 return { action: "replace" } as const;
117 }
118 }),
119 read: Effect.fn(function* ({ olds, output }) {
120 const resourceIdentifier =
121 output?.resourceIdentifier ?? olds?.resourceIdentifier;
122 if (!resourceIdentifier) return undefined;
123 const observed = yield* observe(resourceIdentifier);
124 if (!observed?.policy) return undefined;
125 return {
126 resourceIdentifier,
127 policy: observed.policy,
128 state: observed.state,
129 };
130 }),
131 reconcile: Effect.fn(function* ({ news, session }) {
132 const resourceIdentifier = news.resourceIdentifier;
133 const desired = toDesired(news.policy);
134
135 // Observe — putAuthPolicy is an upsert. Compare the canonicalized
136 // live document against the canonicalized desired one so a
137 // re-deploy of an equivalent policy (key order, whitespace) skips
138 // the API call entirely.
139 const observed = yield* observe(resourceIdentifier);
140 let state = observed?.state;
141 if (
142 observed?.policy === undefined ||
143 normalizePolicyDocument(observed.policy) !==
144 normalizePolicyDocument(desired)
145 ) {
146 const put = yield* vpclattice.putAuthPolicy({
147 resourceIdentifier,
148 policy: desired,
149 });
150 state = put.state;
151 }
152
153 yield* session.note(resourceIdentifier);
154 return { resourceIdentifier, policy: desired, state };
155 }),
156 // Auth policies are sub-resources keyed entirely by their parent
157 // service network / service — there is no enumeration API.
158 list: () => Effect.succeed([] as AuthPolicy["Attributes"][]),
159 delete: Effect.fn(function* ({ output }) {
160 yield* vpclattice
161 .deleteAuthPolicy({
162 resourceIdentifier: output.resourceIdentifier,
163 })
164 .pipe(
165 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
166 );
167 }),
168 };
169 }),
170 );