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 ResourcePolicyProps {
14 /**
15 * The ARN of the service network or service the resource policy is attached
16 * to. Immutable — changing it replaces the resource.
17 */
18 resourceArn: string;
19 /**
20 * The resource-based permission policy, either as a structured
21 * {@link PolicyDocument} or a raw JSON string (escape hatch). Must contain
22 * the same actions and condition statements as the RAM permission for
23 * sharing services and service networks.
24 */
25 policy: PolicyDocument | string;
26}
27
28export interface ResourcePolicy extends Resource<
29 "AWS.VpcLattice.ResourcePolicy",
30 ResourcePolicyProps,
31 {
32 /**
33 * ARN of the service network or service the policy is attached to.
34 */
35 resourceArn: string;
36 /**
37 * The attached policy document as a JSON string.
38 */
39 policy: string;
40 },
41 never,
42 Providers
43> {}
44
45/**
46 * A resource-based permission policy on a VPC Lattice service or service
47 * network — the policy AWS RAM manages when sharing Lattice resources across
48 * accounts, attachable directly for fine-grained cross-account control.
49 *
50 * ### Attaching Resource Policies
51 * **Example:** Allow Another Account to Associate with a Service Network
52 * ```typescript
53 * const network = yield* ServiceNetwork("SharedNetwork", {});
54 * const policy = yield* ResourcePolicy("SharePolicy", {
55 * resourceArn: network.serviceNetworkArn,
56 * policy: {
57 * Version: "2012-10-17",
58 * Statement: [
59 * {
60 * Effect: "Allow",
61 * Principal: { AWS: "arn:aws:iam::123456789012:root" },
62 * Action: [
63 * "vpc-lattice:CreateServiceNetworkVpcAssociation",
64 * "vpc-lattice:CreateServiceNetworkServiceAssociation",
65 * "vpc-lattice:GetServiceNetwork",
66 * ],
67 * Resource: network.serviceNetworkArn,
68 * },
69 * ],
70 * },
71 * });
72 * ```
73 *
74 * @resource
75 */
76export const ResourcePolicy = Resource<ResourcePolicy>(
77 "AWS.VpcLattice.ResourcePolicy",
78);
79
80export const ResourcePolicyProvider = () =>
81 Provider.effect(
82 ResourcePolicy,
83 Effect.gen(function* () {
84 // A missing parent and a parent without a policy are both "no policy" —
85 // getResourcePolicy answers with an empty body for the latter, so treat
86 // an absent `policy` as non-existence.
87 const observe = (resourceArn: string) =>
88 vpclattice.getResourcePolicy({ resourceArn }).pipe(
89 Effect.map((response) => response.policy),
90 Effect.catchTag("ResourceNotFoundException", () =>
91 Effect.succeed(undefined),
92 ),
93 );
94
95 const toDesired = (policy: PolicyDocument | string) =>
96 typeof policy === "string" ? policy : stringifyPolicyDocument(policy);
97
98 return {
99 stables: ["resourceArn"],
100 diff: Effect.fn(function* ({ olds, news }) {
101 if (!isResolved(news)) return;
102 if (olds && olds.resourceArn !== news.resourceArn) {
103 return { action: "replace" } as const;
104 }
105 }),
106 read: Effect.fn(function* ({ olds, output }) {
107 const resourceArn = output?.resourceArn ?? olds?.resourceArn;
108 if (!resourceArn) return undefined;
109 const observed = yield* observe(resourceArn);
110 if (observed === undefined) return undefined;
111 return { resourceArn, policy: observed };
112 }),
113 reconcile: Effect.fn(function* ({ news, session }) {
114 const resourceArn = news.resourceArn;
115 const desired = toDesired(news.policy);
116
117 // Observe — putResourcePolicy is an upsert. Compare the
118 // canonicalized live document against the canonicalized desired one
119 // so a re-deploy of an equivalent policy (key order, whitespace)
120 // skips the API call entirely.
121 const observed = yield* observe(resourceArn);
122 if (
123 observed === undefined ||
124 normalizePolicyDocument(observed) !==
125 normalizePolicyDocument(desired)
126 ) {
127 yield* vpclattice.putResourcePolicy({
128 resourceArn,
129 policy: desired,
130 });
131 }
132
133 yield* session.note(resourceArn);
134 return { resourceArn, policy: desired };
135 }),
136 // Resource policies are sub-resources keyed entirely by their parent
137 // service network / service ARN — there is no enumeration API.
138 list: () => Effect.succeed([] as ResourcePolicy["Attributes"][]),
139 delete: Effect.fn(function* ({ output }) {
140 yield* vpclattice
141 .deleteResourcePolicy({ resourceArn: output.resourceArn })
142 .pipe(
143 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
144 );
145 }),
146 };
147 }),
148 );