1import * as AWS from "@/AWS";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Result from "effect/Result";
5import { HttpServerRequest } from "effect/http/HttpServerRequest";
6import * as HttpServerResponse from "effect/http/HttpServerResponse";
7import path from "pathe";
8
9const main = path.resolve(import.meta.dirname, "handler.ts");
10
11export class VerifiedPermissionsTestFunction extends AWS.Lambda.Function<AWS.Lambda.Function>()(
12 "VerifiedPermissionsTestFunction",
13) {}
14
15export default VerifiedPermissionsTestFunction.make(
16 {
17 main,
18 functionUrl: true,
19 // isAuthorized fans out an AVP API call; the 3s default is too tight
20 timeout: Duration.seconds(30),
21 },
22 Effect.gen(function* () {
23 const store = yield* AWS.VerifiedPermissions.PolicyStore("Store", {
24 validationMode: "OFF",
25 });
26 // permit only alice to view photos
27 const allowAlice = yield* AWS.VerifiedPermissions.Policy("AllowAlice", {
28 policyStoreId: store.policyStoreId,
29 statement: `permit(
30 principal == PhotoApp::User::"alice",
31 action == PhotoApp::Action::"viewPhoto",
32 resource
33 );`,
34 });
35
36 const authz = yield* AWS.VerifiedPermissions.IsAuthorized(store);
37 const policies = yield* AWS.VerifiedPermissions.GetPolicies(store);
38 const PolicyStoreId = yield* store.policyStoreId;
39 const AllowAlicePolicyId = yield* allowAlice.policyId;
40
41 return {
42 fetch: Effect.gen(function* () {
43 const request = yield* HttpServerRequest;
44 const url = new URL(request.originalUrl);
45 const pathname = url.pathname;
46
47 if (request.method === "GET" && pathname === "/info") {
48 const policyStoreId = yield* PolicyStoreId;
49 return yield* HttpServerResponse.json({ policyStoreId });
50 }
51
52 // /authorize?user=alice -> ALLOW, any other user -> DENY
53 if (request.method === "GET" && pathname === "/authorize") {
54 const user = url.searchParams.get("user") ?? "alice";
55 const result = yield* authz.isAuthorized({
56 principal: { entityType: "PhotoApp::User", entityId: user },
57 action: {
58 actionType: "PhotoApp::Action",
59 actionId: "viewPhoto",
60 },
61 resource: {
62 entityType: "PhotoApp::Photo",
63 entityId: "vacation.jpg",
64 },
65 });
66 return yield* HttpServerResponse.json({
67 decision: result.decision,
68 });
69 }
70
71 // /batch -> two decisions for one resource, alice + bob
72 if (request.method === "GET" && pathname === "/batch") {
73 const result = yield* authz.batchIsAuthorized({
74 requests: [
75 {
76 principal: { entityType: "PhotoApp::User", entityId: "alice" },
77 action: {
78 actionType: "PhotoApp::Action",
79 actionId: "viewPhoto",
80 },
81 resource: {
82 entityType: "PhotoApp::Photo",
83 entityId: "vacation.jpg",
84 },
85 },
86 {
87 principal: { entityType: "PhotoApp::User", entityId: "bob" },
88 action: {
89 actionType: "PhotoApp::Action",
90 actionId: "viewPhoto",
91 },
92 resource: {
93 entityType: "PhotoApp::Photo",
94 entityId: "vacation.jpg",
95 },
96 },
97 ],
98 });
99 return yield* HttpServerResponse.json({
100 decisions: result.results.map((r) => r.decision),
101 });
102 }
103
104 // /policies -> batchGetPolicy for the AllowAlice policy
105 if (request.method === "GET" && pathname === "/policies") {
106 const policyId = yield* AllowAlicePolicyId;
107 const result = yield* policies.batchGetPolicy({
108 policyIds: [policyId],
109 });
110 return yield* HttpServerResponse.json({
111 ids: result.results.map((r) => r.policyId),
112 types: result.results.map((r) => r.policyType),
113 errors: result.errors.length,
114 });
115 }
116
117 // /batch-token -> batchIsAuthorizedWithToken with a malformed token;
118 // no identity source is configured, so AVP rejects the request with
119 // a typed ValidationException — proving IAM + wiring end-to-end
120 if (request.method === "GET" && pathname === "/batch-token") {
121 const result = yield* Effect.result(
122 authz.batchIsAuthorizedWithToken({
123 accessToken: "not-a-jwt",
124 requests: [
125 {
126 action: {
127 actionType: "PhotoApp::Action",
128 actionId: "viewPhoto",
129 },
130 resource: {
131 entityType: "PhotoApp::Photo",
132 entityId: "vacation.jpg",
133 },
134 },
135 ],
136 }),
137 );
138 return yield* HttpServerResponse.json({
139 tag: Result.isFailure(result)
140 ? result.failure._tag
141 : "unexpected-success",
142 });
143 }
144
145 return yield* HttpServerResponse.json(
146 { error: "Not found" },
147 { status: 404 },
148 );
149 }).pipe(Effect.orDie),
150 };
151 }).pipe(
152 Effect.provide([
153 AWS.VerifiedPermissions.IsAuthorizedHttp,
154 AWS.VerifiedPermissions.GetPoliciesHttp,
155 ]),
156 ),
157);