1import * as avp from "@distilled.cloud/aws/verifiedpermissions";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import type { Providers } from "../Providers.ts";
8
9const unwrap = (v: string | Redacted.Redacted<string> | undefined) =>
10 v === undefined ? undefined : Redacted.isRedacted(v) ? Redacted.value(v) : v;
11
12export interface PolicyTemplateProps {
13 /**
14 * The ID of the policy store the template belongs to. Changing the store
15 * replaces the template.
16 */
17 policyStoreId: string;
18 /**
19 * The Cedar policy-template statement. Templates use the `?principal` and
20 * `?resource` placeholders which are filled in when a template-linked
21 * policy is created, e.g.
22 * `permit(principal == ?principal, action == PhotoApp::Action::"viewPhoto", resource);`.
23 * Mutable — updating re-submits the statement via `UpdatePolicyTemplate`.
24 */
25 statement: string;
26 /**
27 * An optional description stored alongside the template statement.
28 */
29 description?: string;
30}
31
32export interface PolicyTemplate extends Resource<
33 "AWS.VerifiedPermissions.PolicyTemplate",
34 PolicyTemplateProps,
35 {
36 /**
37 * ID of the policy store the template belongs to.
38 */
39 policyStoreId: string;
40 /**
41 * Service-assigned unique ID of the policy template within the store —
42 * pass it as the `templateId` of a template-linked {@link Policy}.
43 */
44 policyTemplateId: string;
45 },
46 {},
47 Providers
48> {}
49
50/**
51 * A Cedar policy template in a Verified Permissions policy store. Templates
52 * contain `?principal` / `?resource` placeholders; template-linked policies
53 * instantiate the template for a concrete principal and resource, and every
54 * linked policy automatically picks up template updates.
55 * ### Creating Policy Templates
56 * **Example:** Template with a Principal Placeholder
57 * ```typescript
58 * import * as AWS from "alchemy/AWS";
59 *
60 * const store = yield* AWS.VerifiedPermissions.PolicyStore("Store", {});
61 *
62 * const template = yield* AWS.VerifiedPermissions.PolicyTemplate("ViewPhoto", {
63 * policyStoreId: store.policyStoreId,
64 * statement: `permit(
65 * principal == ?principal,
66 * action == PhotoApp::Action::"viewPhoto",
67 * resource
68 * );`,
69 * description: "Grant a user access to view photos",
70 * });
71 * ```
72 *
73 * **Example:** Link a Policy to the Template
74 * ```typescript
75 * yield* AWS.VerifiedPermissions.Policy("AliceCanView", {
76 * policyStoreId: store.policyStoreId,
77 * templateId: template.policyTemplateId,
78 * principal: { entityType: "PhotoApp::User", entityId: "alice" },
79 * });
80 * ```
81 *
82 * @resource
83 */
84export const PolicyTemplate = Resource<PolicyTemplate>(
85 "AWS.VerifiedPermissions.PolicyTemplate",
86);
87
88export const PolicyTemplateProvider = () =>
89 Provider.effect(
90 PolicyTemplate,
91 Effect.gen(function* () {
92 const observe = Effect.fn(function* (
93 policyStoreId: string,
94 policyTemplateId: string,
95 ) {
96 return yield* avp
97 .getPolicyTemplate({ policyStoreId, policyTemplateId })
98 .pipe(
99 Effect.catchTag("ResourceNotFoundException", () =>
100 Effect.succeed(undefined),
101 ),
102 );
103 });
104
105 return PolicyTemplate.Provider.of({
106 stables: ["policyStoreId", "policyTemplateId"],
107
108 // child of a policy store — not enumerable account-wide
109 list: () => Effect.succeed([]),
110
111 read: Effect.fn(function* ({ olds, output }) {
112 const policyStoreId = output?.policyStoreId ?? olds?.policyStoreId;
113 const policyTemplateId = output?.policyTemplateId;
114 if (policyStoreId === undefined || policyTemplateId === undefined) {
115 return undefined;
116 }
117 const template = yield* observe(policyStoreId, policyTemplateId);
118 if (template === undefined) return undefined;
119 return { policyStoreId, policyTemplateId: template.policyTemplateId };
120 }),
121
122 diff: Effect.fn(function* ({ news, olds }) {
123 if (!isResolved(news)) return undefined;
124 if (olds.policyStoreId !== news.policyStoreId) {
125 return { action: "replace" } as const;
126 }
127 // statement / description are mutable → default update path
128 }),
129
130 reconcile: Effect.fn(function* ({ news, output, session }) {
131 // 1. OBSERVE — cloud state is authoritative
132 const existing =
133 output?.policyTemplateId !== undefined
134 ? yield* observe(news.policyStoreId, output.policyTemplateId)
135 : undefined;
136
137 // 2. ENSURE
138 if (existing === undefined) {
139 const created = yield* avp.createPolicyTemplate({
140 policyStoreId: news.policyStoreId,
141 statement: news.statement,
142 description: news.description,
143 });
144 yield* session.note(created.policyTemplateId);
145 return {
146 policyStoreId: news.policyStoreId,
147 policyTemplateId: created.policyTemplateId,
148 };
149 }
150
151 // 3. SYNC — diff observed statement/description against desired
152 const observedStatement = unwrap(existing.statement);
153 const observedDescription = unwrap(existing.description);
154 if (
155 observedStatement !== news.statement ||
156 observedDescription !== (news.description ?? undefined)
157 ) {
158 yield* avp.updatePolicyTemplate({
159 policyStoreId: news.policyStoreId,
160 policyTemplateId: existing.policyTemplateId,
161 statement: news.statement,
162 description: news.description,
163 });
164 }
165
166 yield* session.note(existing.policyTemplateId);
167 return {
168 policyStoreId: news.policyStoreId,
169 policyTemplateId: existing.policyTemplateId,
170 };
171 }),
172
173 delete: Effect.fn(function* ({ output }) {
174 yield* avp
175 .deletePolicyTemplate({
176 policyStoreId: output.policyStoreId,
177 policyTemplateId: output.policyTemplateId,
178 })
179 .pipe(
180 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
181 );
182 }),
183 });
184 }),
185 );