1import * as avp from "@distilled.cloud/aws/verifiedpermissions";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import type { Providers } from "../Providers.ts";
7
8/**
9 * Identifies a Cedar entity, e.g.
10 * `{ entityType: "PhotoApp::User", entityId: "alice" }`.
11 */
12export interface EntityIdentifier {
13 /** The Cedar entity type, e.g. `PhotoApp::User`. */
14 entityType: string;
15 /** The entity ID, e.g. `alice`. */
16 entityId: string;
17}
18
19export interface PolicyProps {
20 /**
21 * The ID of the policy store the policy belongs to. Changing the store
22 * replaces the policy.
23 */
24 policyStoreId: string;
25 /**
26 * The Cedar policy statement for a **static** policy, e.g.
27 * `permit(principal, action == Action::"view", resource);`. Mutable —
28 * updating re-submits the statement via `UpdatePolicy` (only annotations
29 * and conditions may change; the principal/resource scope is fixed).
30 *
31 * Exactly one of `statement` (static) or `templateId` (template-linked)
32 * must be provided.
33 */
34 statement?: string;
35 /**
36 * An optional description stored alongside a static policy statement.
37 */
38 description?: string;
39 /**
40 * The ID of a {@link PolicyTemplate} to instantiate as a
41 * **template-linked** policy. Template-linked policies cannot be updated
42 * in place — changing `templateId`, `principal`, or `resource` replaces
43 * the policy (the template itself is the mutable part).
44 */
45 templateId?: string;
46 /**
47 * The principal to fill into the template's `?principal` placeholder.
48 * Only valid with `templateId`.
49 */
50 principal?: EntityIdentifier;
51 /**
52 * The resource to fill into the template's `?resource` placeholder.
53 * Only valid with `templateId`.
54 */
55 resource?: EntityIdentifier;
56}
57
58export interface Policy extends Resource<
59 "AWS.VerifiedPermissions.Policy",
60 PolicyProps,
61 {
62 /**
63 * ID of the policy store the policy belongs to.
64 */
65 policyStoreId: string;
66 /**
67 * Service-assigned unique ID of the policy within the store.
68 */
69 policyId: string;
70 },
71 {},
72 Providers
73> {}
74
75/**
76 * A static Cedar policy in a Verified Permissions policy store. Static
77 * policies contain a complete Cedar statement and are evaluated for every
78 * matching authorization request.
79 * ### Creating Policies
80 * **Example:** Permit a Specific Principal
81 * ```typescript
82 * import * as AWS from "alchemy/AWS";
83 *
84 * const store = yield* AWS.VerifiedPermissions.PolicyStore("Store", {});
85 *
86 * yield* AWS.VerifiedPermissions.Policy("AllowAlice", {
87 * policyStoreId: store.policyStoreId,
88 * statement: `permit(
89 * principal == PhotoApp::User::"alice",
90 * action == PhotoApp::Action::"viewPhoto",
91 * resource
92 * );`,
93 * description: "Alice can view any photo",
94 * });
95 * ```
96 *
97 * ### Template-Linked Policies
98 * **Example:** Instantiate a Policy Template for a Principal
99 * ```typescript
100 * const template = yield* AWS.VerifiedPermissions.PolicyTemplate("ViewPhoto", {
101 * policyStoreId: store.policyStoreId,
102 * statement: `permit(
103 * principal == ?principal,
104 * action == PhotoApp::Action::"viewPhoto",
105 * resource
106 * );`,
107 * });
108 *
109 * yield* AWS.VerifiedPermissions.Policy("AliceCanView", {
110 * policyStoreId: store.policyStoreId,
111 * templateId: template.policyTemplateId,
112 * principal: { entityType: "PhotoApp::User", entityId: "alice" },
113 * });
114 * ```
115 *
116 * @resource
117 */
118export const Policy = Resource<Policy>("AWS.VerifiedPermissions.Policy");
119
120/** Desired props → the wire `PolicyDefinition` union. */
121const toDefinition = (news: PolicyProps) =>
122 Effect.gen(function* () {
123 if (news.templateId !== undefined && news.statement !== undefined) {
124 return yield* Effect.fail(
125 new Error(
126 "a Policy accepts either `statement` (static) or `templateId` (template-linked), not both",
127 ),
128 );
129 }
130 if (news.templateId !== undefined) {
131 return {
132 templateLinked: {
133 policyTemplateId: news.templateId,
134 principal: news.principal,
135 resource: news.resource,
136 },
137 } as const;
138 }
139 if (news.statement === undefined) {
140 return yield* Effect.fail(
141 new Error(
142 "a Policy requires either `statement` (static) or `templateId` (template-linked)",
143 ),
144 );
145 }
146 return {
147 static: { statement: news.statement, description: news.description },
148 } as const;
149 });
150
151export const PolicyProvider = () =>
152 Provider.effect(
153 Policy,
154 Effect.gen(function* () {
155 const observe = Effect.fn(function* (
156 policyStoreId: string,
157 policyId: string,
158 ) {
159 return yield* avp
160 .getPolicy({ policyStoreId, policyId })
161 .pipe(
162 Effect.catchTag("ResourceNotFoundException", () =>
163 Effect.succeed(undefined),
164 ),
165 );
166 });
167
168 return Policy.Provider.of({
169 stables: ["policyStoreId", "policyId"],
170
171 // child of a policy store — not enumerable account-wide
172 list: () => Effect.succeed([]),
173
174 read: Effect.fn(function* ({ olds, output }) {
175 const policyStoreId = output?.policyStoreId ?? olds?.policyStoreId;
176 const policyId = output?.policyId;
177 if (policyStoreId === undefined || policyId === undefined) {
178 return undefined;
179 }
180 const policy = yield* observe(policyStoreId, policyId);
181 if (policy === undefined) return undefined;
182 return { policyStoreId, policyId: policy.policyId };
183 }),
184
185 diff: Effect.fn(function* ({ news, olds }) {
186 if (!isResolved(news)) return undefined;
187 if (olds.policyStoreId !== news.policyStoreId) {
188 return { action: "replace" } as const;
189 }
190 // switching static <-> template-linked replaces; UpdatePolicy only
191 // supports static definitions, and a template-linked policy's
192 // template/principal/resource are fixed at creation
193 if (
194 (olds.templateId === undefined) !==
195 (news.templateId === undefined) ||
196 (news.templateId !== undefined &&
197 (olds.templateId !== news.templateId ||
198 olds.principal?.entityType !== news.principal?.entityType ||
199 olds.principal?.entityId !== news.principal?.entityId ||
200 olds.resource?.entityType !== news.resource?.entityType ||
201 olds.resource?.entityId !== news.resource?.entityId))
202 ) {
203 return { action: "replace" } as const;
204 }
205 // static statement / description are mutable → default update path
206 }),
207
208 reconcile: Effect.fn(function* ({ news, output, session }) {
209 const definition = yield* toDefinition(news);
210
211 // 1. OBSERVE — cloud state is authoritative
212 const existing =
213 output?.policyId !== undefined
214 ? yield* observe(news.policyStoreId, output.policyId)
215 : undefined;
216
217 // 2. ENSURE / SYNC
218 let policyId: string;
219 if (existing === undefined) {
220 const created = yield* avp.createPolicy({
221 policyStoreId: news.policyStoreId,
222 definition,
223 });
224 policyId = created.policyId;
225 } else if (
226 "static" in definition &&
227 definition.static !== undefined
228 ) {
229 const updated = yield* avp.updatePolicy({
230 policyStoreId: news.policyStoreId,
231 policyId: existing.policyId,
232 definition: { static: definition.static },
233 });
234 policyId = updated.policyId;
235 } else {
236 // template-linked policies have no in-place update — diff replaces
237 // on any change, so the observed policy is already converged
238 policyId = existing.policyId;
239 }
240
241 yield* session.note(policyId);
242 return { policyStoreId: news.policyStoreId, policyId };
243 }),
244
245 delete: Effect.fn(function* ({ output }) {
246 yield* avp
247 .deletePolicy({
248 policyStoreId: output.policyStoreId,
249 policyId: output.policyId,
250 })
251 .pipe(
252 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
253 );
254 }),
255 });
256 }),
257 );