@reventlessdev/reventless-aws 3.0.0-alpha.291 → 3.0.0-alpha.293
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/package.json +9 -9
- package/src/Platform.res +85 -5
- package/src/Platform.res.mjs +62 -6
- package/src/Platform_Stack.res +78 -0
- package/src/Platform_Stack.res.mjs +17 -2
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +10 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +8 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +89 -12
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +55 -4
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +309 -0
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +230 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore.res +280 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +163 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res +148 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs +128 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +180 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +105 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +201 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +126 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res +16 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +6 -0
- package/tests/Auth_ActiveRolePoolAttachmentTest.res +211 -0
- package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +232 -0
- package/tests/Auth_ActiveRoleStoreTest.res +42 -0
- package/tests/Auth_ActiveRoleStoreTest.res.mjs +41 -0
- package/tests/Auth_ActiveRoleTrigger_OpsTest.res +169 -0
- package/tests/Auth_ActiveRoleTrigger_OpsTest.res.mjs +189 -0
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res +110 -0
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res.mjs +87 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Runtime handler for `Mutation.Platform_SetActiveRole` — compiled, type-checked
|
|
2
|
+
// and Pulumi-free so it ships as an EntryPoint module (`Auth_ActiveRoleStore`
|
|
3
|
+
// bundles it and attaches it as the domain API's Lambda data source). See
|
|
4
|
+
// [Upload_Presign_S3_Ops.res] for why an EntryPoint rather than a serialized
|
|
5
|
+
// closure, and [docs/plans/active-role-narrows-the-token.md] §6 for the design.
|
|
6
|
+
//
|
|
7
|
+
// What this writes is a *preference*, not a token. Cognito mints the token, and
|
|
8
|
+
// the pre-token-generation trigger ([Auth_ActiveRoleTrigger_Ops.res]) reads this
|
|
9
|
+
// row on the next refresh and narrows the group claim to it. Nothing here can
|
|
10
|
+
// grant anything: the trigger re-checks the stored role against real membership
|
|
11
|
+
// before applying it, so a row is at most a request that the trigger may honour.
|
|
12
|
+
//
|
|
13
|
+
// 🚨 **The subject is taken from the verified identity, never from the arguments.**
|
|
14
|
+
// The row key is `ctx.identity.sub` as the AppSync Cognito authorizer resolved it.
|
|
15
|
+
// There is deliberately no `sub` argument on the mutation — with one, a caller
|
|
16
|
+
// could set another caller's role, and this becomes a privilege-granting surface
|
|
17
|
+
// instead of a preference.
|
|
18
|
+
|
|
19
|
+
open AwsSdk
|
|
20
|
+
|
|
21
|
+
// ── Environment ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
let getEnv = (k: string): option<string> =>
|
|
24
|
+
switch NodeProcess.env->Dict.get(k) {
|
|
25
|
+
| Some("") | None => None
|
|
26
|
+
| Some(v) => Some(v)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let tableName = (): string =>
|
|
30
|
+
switch getEnv("ACTIVE_ROLE_TABLE") {
|
|
31
|
+
| Some(t) => t
|
|
32
|
+
| None => JsError.throwWithMessage("ACTIVE_ROLE_TABLE is not configured")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let userPoolId = (): string =>
|
|
36
|
+
switch getEnv("COGNITO_USER_POOL_ID") {
|
|
37
|
+
| Some(p) => p
|
|
38
|
+
| None => JsError.throwWithMessage("COGNITO_USER_POOL_ID is not configured")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Membership ──────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
The caller's actual group membership, read from Cognito.
|
|
45
|
+
|
|
46
|
+
🚨 **Not read from the presented token.** A narrowed token carries exactly one
|
|
47
|
+
group, so judging membership by `ctx.identity.groups` would make the switch
|
|
48
|
+
one-way: a caller who narrowed to `Shopper` could never widen back, because the
|
|
49
|
+
token they hold no longer mentions the role they are asking for. This is the same
|
|
50
|
+
trap the local path documents on `LocalAuth.Login.reissue`, where membership is
|
|
51
|
+
re-read from the user store rather than from the token's own record of it.
|
|
52
|
+
|
|
53
|
+
Paginated deliberately: `AdminListGroupsForUser` caps a page at 60 groups, and a
|
|
54
|
+
truncated read would silently refuse a role the caller genuinely holds.
|
|
55
|
+
*/
|
|
56
|
+
let membershipOf = async (~sub: string, ~poolId: string): array<string> => {
|
|
57
|
+
let collected = []
|
|
58
|
+
let nextToken = ref(None)
|
|
59
|
+
let more = ref(true)
|
|
60
|
+
while more.contents {
|
|
61
|
+
let page = await CognitoIdentityServiceProvider.AdminListGroupsForUserCommand.make({
|
|
62
|
+
username: sub,
|
|
63
|
+
userPoolId: poolId,
|
|
64
|
+
nextToken: ?nextToken.contents,
|
|
65
|
+
})->CognitoIdentityServiceProvider.AdminListGroupsForUserCommand.send
|
|
66
|
+
page.groups
|
|
67
|
+
->Option.getOr([])
|
|
68
|
+
->Array.forEach(g => g.groupName->Option.forEach(n => collected->Array.push(n)))
|
|
69
|
+
switch page.nextToken {
|
|
70
|
+
| Some(_) as t => nextToken := t
|
|
71
|
+
| None => more := false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
collected
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── The subset rule ─────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
Whether a requested role may be stored.
|
|
81
|
+
|
|
82
|
+
Narrowing only, never widening — a client that tampers with the request can only
|
|
83
|
+
ever reduce its own privilege. Pure and exported so the conformance table in
|
|
84
|
+
`ReventlessCore.Auth_ActiveRole` can be driven against it without a Cognito pool
|
|
85
|
+
or a DynamoDB table in existence.
|
|
86
|
+
*/
|
|
87
|
+
let mayActAs = (~membership: array<string>, ~requested: string): bool =>
|
|
88
|
+
membership->Array.includes(requested)
|
|
89
|
+
|
|
90
|
+
// ── AppSync resolver event / result shapes ──────────────────────────────────
|
|
91
|
+
|
|
92
|
+
type identity = {sub?: string}
|
|
93
|
+
type activeRoleArgs = {activeRole?: Nullable.t<string>}
|
|
94
|
+
type appSyncEvent = {
|
|
95
|
+
arguments?: activeRoleArgs,
|
|
96
|
+
identity?: identity,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let result = (~activeRole: option<string>, ~availableRoles: array<string>): JSON.t =>
|
|
100
|
+
Dict.fromArray([
|
|
101
|
+
("activeRole", activeRole->Option.mapOr(JSON.Null, JSON.Encode.string)),
|
|
102
|
+
("availableRoles", availableRoles->Array.map(JSON.Encode.string)->JSON.Encode.array),
|
|
103
|
+
])->JSON.Encode.object
|
|
104
|
+
|
|
105
|
+
// ── Handler ─────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
let handler = async (event: appSyncEvent): JSON.t => {
|
|
108
|
+
let sub = event.identity->Option.flatMap(i => i.sub)->Option.getOr("")
|
|
109
|
+
if sub == "" {
|
|
110
|
+
// The authorizer should have refused first; this is the guard that keeps an
|
|
111
|
+
// unauthenticated path from ever writing a row keyed on the empty subject.
|
|
112
|
+
JsError.throwWithMessage("unauthenticated")
|
|
113
|
+
}
|
|
114
|
+
let table = tableName()
|
|
115
|
+
let membership = await membershipOf(~sub, ~poolId=userPoolId())
|
|
116
|
+
|
|
117
|
+
// An absent argument and an explicit `null` mean the same thing — clear the
|
|
118
|
+
// preference and go back to full membership on the next refresh. That is not an
|
|
119
|
+
// escalation: the set being widened to is the one Cognito says the caller holds.
|
|
120
|
+
let requested = switch event.arguments->Option.flatMap(a => a.activeRole) {
|
|
121
|
+
| Some(Value(role)) => Some(role)
|
|
122
|
+
| Some(Null) | Some(Undefined) | None => None
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
switch requested {
|
|
126
|
+
| None =>
|
|
127
|
+
let _ = await DynamoDb_DocumentClient.deleteById(~tableName=table, ~id=sub)
|
|
128
|
+
result(~activeRole=None, ~availableRoles=membership)
|
|
129
|
+
| Some(role) =>
|
|
130
|
+
// Refused, specifically — not ignored and stored anyway. A client asking for
|
|
131
|
+
// something it cannot have is either confused or hostile, and both are better
|
|
132
|
+
// served by an error than by a stored role that quietly never takes effect.
|
|
133
|
+
if !mayActAs(~membership, ~requested=role) {
|
|
134
|
+
JsError.throwWithMessage(`Cannot act as "${role}": not a group this user holds`)
|
|
135
|
+
}
|
|
136
|
+
let item =
|
|
137
|
+
Dict.fromArray([
|
|
138
|
+
("id", JSON.Encode.string(sub)),
|
|
139
|
+
("activeRole", JSON.Encode.string(role)),
|
|
140
|
+
("updatedAt", JSON.Encode.string(Date.make()->Date.toISOString)),
|
|
141
|
+
])->JSON.Encode.object
|
|
142
|
+
let _ = await DynamoDb_DocumentClient.PutCommand.make({
|
|
143
|
+
tableName: table,
|
|
144
|
+
item,
|
|
145
|
+
})->DynamoDb_DocumentClient.PutCommand.send
|
|
146
|
+
result(~activeRole=Some(role), ~availableRoles=membership)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
5
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
6
|
+
import * as LibDynamodb from "@aws-sdk/lib-dynamodb";
|
|
7
|
+
import * as DynamoDb_DocumentClient$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_DocumentClient.res.mjs";
|
|
8
|
+
import * as CognitoIdentityServiceProvider$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/CognitoIdentityServiceProvider.res.mjs";
|
|
9
|
+
import * as ClientCognitoIdentityProvider from "@aws-sdk/client-cognito-identity-provider";
|
|
10
|
+
|
|
11
|
+
function getEnv(k) {
|
|
12
|
+
let v = process.env[k];
|
|
13
|
+
if (v !== undefined && v !== "") {
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function tableName() {
|
|
19
|
+
let t = getEnv("ACTIVE_ROLE_TABLE");
|
|
20
|
+
if (t !== undefined) {
|
|
21
|
+
return t;
|
|
22
|
+
} else {
|
|
23
|
+
return Stdlib_JsError.throwWithMessage("ACTIVE_ROLE_TABLE is not configured");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function userPoolId() {
|
|
28
|
+
let p = getEnv("COGNITO_USER_POOL_ID");
|
|
29
|
+
if (p !== undefined) {
|
|
30
|
+
return p;
|
|
31
|
+
} else {
|
|
32
|
+
return Stdlib_JsError.throwWithMessage("COGNITO_USER_POOL_ID is not configured");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function membershipOf(sub, poolId) {
|
|
37
|
+
let collected = [];
|
|
38
|
+
let nextToken;
|
|
39
|
+
let more = true;
|
|
40
|
+
while (more) {
|
|
41
|
+
let page = await CognitoIdentityServiceProvider$AwsSdk.AdminListGroupsForUserCommand.send(new ClientCognitoIdentityProvider.AdminListGroupsForUserCommand({
|
|
42
|
+
Username: sub,
|
|
43
|
+
UserPoolId: poolId,
|
|
44
|
+
NextToken: nextToken
|
|
45
|
+
}));
|
|
46
|
+
Stdlib_Option.getOr(page.Groups, []).forEach(g => Stdlib_Option.forEach(g.GroupName, n => {
|
|
47
|
+
collected.push(n);
|
|
48
|
+
}));
|
|
49
|
+
let t = page.NextToken;
|
|
50
|
+
if (t !== undefined) {
|
|
51
|
+
nextToken = t;
|
|
52
|
+
} else {
|
|
53
|
+
more = false;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return collected;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mayActAs(membership, requested) {
|
|
60
|
+
return membership.includes(requested);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function result(activeRole, availableRoles) {
|
|
64
|
+
return Object.fromEntries([
|
|
65
|
+
[
|
|
66
|
+
"activeRole",
|
|
67
|
+
Stdlib_Option.mapOr(activeRole, null, prim => prim)
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"availableRoles",
|
|
71
|
+
availableRoles.map(prim => prim)
|
|
72
|
+
]
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function handler(event) {
|
|
77
|
+
let sub = Stdlib_Option.getOr(Stdlib_Option.flatMap(event.identity, i => i.sub), "");
|
|
78
|
+
if (sub === "") {
|
|
79
|
+
Stdlib_JsError.throwWithMessage("unauthenticated");
|
|
80
|
+
}
|
|
81
|
+
let table = tableName();
|
|
82
|
+
let membership = await membershipOf(sub, userPoolId());
|
|
83
|
+
let match = Stdlib_Option.flatMap(event.arguments, a => a.activeRole);
|
|
84
|
+
let requested;
|
|
85
|
+
if (match !== undefined) {
|
|
86
|
+
let role = Primitive_option.valFromOption(match);
|
|
87
|
+
requested = (role == null) ? undefined : role;
|
|
88
|
+
} else {
|
|
89
|
+
requested = undefined;
|
|
90
|
+
}
|
|
91
|
+
if (requested !== undefined) {
|
|
92
|
+
if (!membership.includes(requested)) {
|
|
93
|
+
Stdlib_JsError.throwWithMessage(`Cannot act as "` + requested + `": not a group this user holds`);
|
|
94
|
+
}
|
|
95
|
+
let item = Object.fromEntries([
|
|
96
|
+
[
|
|
97
|
+
"id",
|
|
98
|
+
sub
|
|
99
|
+
],
|
|
100
|
+
[
|
|
101
|
+
"activeRole",
|
|
102
|
+
requested
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
"updatedAt",
|
|
106
|
+
new Date().toISOString()
|
|
107
|
+
]
|
|
108
|
+
]);
|
|
109
|
+
await DynamoDb_DocumentClient$AwsSdk.PutCommand.send(new LibDynamodb.PutCommand({
|
|
110
|
+
Item: item,
|
|
111
|
+
TableName: table
|
|
112
|
+
}));
|
|
113
|
+
return result(requested, membership);
|
|
114
|
+
}
|
|
115
|
+
await DynamoDb_DocumentClient$AwsSdk.deleteById(table, sub);
|
|
116
|
+
return result(undefined, membership);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
getEnv,
|
|
121
|
+
tableName,
|
|
122
|
+
userPoolId,
|
|
123
|
+
membershipOf,
|
|
124
|
+
mayActAs,
|
|
125
|
+
result,
|
|
126
|
+
handler,
|
|
127
|
+
}
|
|
128
|
+
/* @aws-sdk/lib-dynamodb Not a pure module */
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Deploy-time half of the Cognito pre-token-generation trigger — the Lambda that
|
|
2
|
+
// narrows `cognito:groups` to the role a caller chose. Runtime logic lives in
|
|
3
|
+
// [Auth_ActiveRoleTrigger_Ops.res]; see
|
|
4
|
+
// [docs/plans/active-role-narrows-the-token.md] §6.
|
|
5
|
+
//
|
|
6
|
+
// `make` provisions the function and its execution role (Logs + a read of the
|
|
7
|
+
// one role-state table). It does **not** attach itself to a user pool, and does
|
|
8
|
+
// not grant Cognito permission to invoke it: both need the pool, and in auto
|
|
9
|
+
// mode the pool needs this function's ARN first — the pool cannot be declared
|
|
10
|
+
// with a trigger that does not exist yet. So `make` runs before the pool, and
|
|
11
|
+
// [grantInvoke] runs after it.
|
|
12
|
+
//
|
|
13
|
+
// Attaching is a separate step again ([Auth_ActiveRolePoolAttachment.res] in BYO
|
|
14
|
+
// mode, `lambdaConfig` on the declared pool in auto mode), kept apart because it
|
|
15
|
+
// is the only part of this feature that can damage something that already
|
|
16
|
+
// exists.
|
|
17
|
+
|
|
18
|
+
open PulumiAws
|
|
19
|
+
|
|
20
|
+
type triggerOutputs = {
|
|
21
|
+
functionArn: Pulumi.Output.t<string>,
|
|
22
|
+
functionName: Pulumi.Output.t<string>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let make = (
|
|
26
|
+
~activeRoleTableName: Pulumi.Input.t<string>,
|
|
27
|
+
~activeRoleTableArn: Pulumi.Input.t<string>,
|
|
28
|
+
~name: string="ActiveRoleTrigger",
|
|
29
|
+
~opts: Pulumi.ComponentResource.options,
|
|
30
|
+
): triggerOutputs => {
|
|
31
|
+
let opts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
32
|
+
|
|
33
|
+
let lambdaRole = IAM.Role.makeWithDefaultPolicy(
|
|
34
|
+
~name,
|
|
35
|
+
~servicePrincipal=AWS.Lambda.principal->Pulumi.Output.make,
|
|
36
|
+
~tags=AWS.Tags.make(
|
|
37
|
+
~name,
|
|
38
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
39
|
+
~role=Identity,
|
|
40
|
+
~scope=Platform,
|
|
41
|
+
),
|
|
42
|
+
~opts,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// Logs and a single-item read. Nothing else: the trigger needs no Cognito
|
|
46
|
+
// permission at all, because the membership it checks against arrives in the
|
|
47
|
+
// event as `request.groupConfiguration.groupsToOverride`. `GetItem` alone —
|
|
48
|
+
// this function must never be able to write the preference it reads.
|
|
49
|
+
let _policy =
|
|
50
|
+
activeRoleTableArn
|
|
51
|
+
->Pulumi.Output.fromInput
|
|
52
|
+
->Pulumi.Output.apply(tableArn => {
|
|
53
|
+
let _ = IAM.RolePolicy.make(
|
|
54
|
+
~name=name ++ "Policy",
|
|
55
|
+
~args={
|
|
56
|
+
policy: PolicyDocument.make(
|
|
57
|
+
~id=name ++ "Policy",
|
|
58
|
+
~statements=[
|
|
59
|
+
{
|
|
60
|
+
sid: "AllowLambdaLogging",
|
|
61
|
+
effect: Allow,
|
|
62
|
+
actions: Actions([
|
|
63
|
+
"logs:CreateLogGroup",
|
|
64
|
+
"logs:CreateLogStream",
|
|
65
|
+
"logs:PutLogEvents",
|
|
66
|
+
]),
|
|
67
|
+
resources: Resource("arn:aws:logs:*:*:*"),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
sid: "AllowReadingStoredRole",
|
|
71
|
+
effect: Allow,
|
|
72
|
+
actions: Action("dynamodb:GetItem"),
|
|
73
|
+
resources: Resource(tableArn),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
->PolicyDocument.toJsonString
|
|
78
|
+
->Pulumi.Input.make,
|
|
79
|
+
role: lambdaRole.id->Pulumi.Output.asInput,
|
|
80
|
+
},
|
|
81
|
+
~opts,
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
let packageDirs = Dict.fromArray([
|
|
86
|
+
(
|
|
87
|
+
"@reventlessdev/reventless-aws",
|
|
88
|
+
Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-aws"),
|
|
89
|
+
),
|
|
90
|
+
])
|
|
91
|
+
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
92
|
+
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs",
|
|
93
|
+
~packageDirs,
|
|
94
|
+
~bundleRuntimeExtensions=false,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
let layers =
|
|
98
|
+
Lambda.reventlessLayerArn
|
|
99
|
+
->Option.map(arn => [arn->Pulumi.Input.make])
|
|
100
|
+
->Option.getOr([])
|
|
101
|
+
->Pulumi.Input.make
|
|
102
|
+
|
|
103
|
+
let logGroup = Util_LambdaLogging.makeManagedLogGroup(
|
|
104
|
+
~name,
|
|
105
|
+
~tags=AWS.Tags.make(
|
|
106
|
+
~name=name ++ "LogGroup",
|
|
107
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
108
|
+
~role=Logs,
|
|
109
|
+
~scope=Platform,
|
|
110
|
+
),
|
|
111
|
+
~opts,
|
|
112
|
+
(),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// This function sits in the critical path of every token the pool mints, so a
|
|
116
|
+
// cold start is a login's latency and a timeout is a failed sign-in. One
|
|
117
|
+
// `GetItem` needs neither much memory nor much time; 5s is well inside
|
|
118
|
+
// Cognito's own 5s trigger budget, so the function fails before Cognito gives
|
|
119
|
+
// up on it rather than after.
|
|
120
|
+
let lambda = Lambda.Function.make(
|
|
121
|
+
~name,
|
|
122
|
+
~args={
|
|
123
|
+
handler: "index.handler"->Pulumi.Input.make,
|
|
124
|
+
runtime: "nodejs22.x"->Pulumi.Input.make,
|
|
125
|
+
code: code->Pulumi.Input.make,
|
|
126
|
+
sourceCodeHash: sourceCodeHash->Pulumi.Input.make,
|
|
127
|
+
role: lambdaRole.arn->Pulumi.Output.asInput,
|
|
128
|
+
memorySize: 256->Pulumi.Input.make,
|
|
129
|
+
timeout: 5->Pulumi.Input.make,
|
|
130
|
+
layers,
|
|
131
|
+
tags: AWS.Tags.make(
|
|
132
|
+
~name,
|
|
133
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
134
|
+
~role=Runtime,
|
|
135
|
+
~scope=Platform,
|
|
136
|
+
),
|
|
137
|
+
environment: (
|
|
138
|
+
{
|
|
139
|
+
Lambda.Function.variables: Dict.fromArray([
|
|
140
|
+
("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
|
|
141
|
+
("ACTIVE_ROLE_TABLE", activeRoleTableName),
|
|
142
|
+
("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
|
|
143
|
+
("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
|
|
144
|
+
Util_LambdaLogging.logLevelEntry(),
|
|
145
|
+
]),
|
|
146
|
+
}: Lambda.Function.functionEnvironment
|
|
147
|
+
)->Pulumi.Input.make,
|
|
148
|
+
loggingConfig: ?Util_LambdaLogging.loggingConfigFor(logGroup),
|
|
149
|
+
},
|
|
150
|
+
~opts,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
{functionArn: lambda.arn, functionName: lambda.name}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Let Cognito invoke the trigger — scoped to one pool, so a function attached
|
|
157
|
+
to a pool nobody declared still cannot be invoked by it.
|
|
158
|
+
|
|
159
|
+
Separate from [make] because the pool's ARN is not known until the pool exists,
|
|
160
|
+
and in auto mode the pool is declared *with* the function's ARN. */
|
|
161
|
+
let grantInvoke = (
|
|
162
|
+
~trigger: triggerOutputs,
|
|
163
|
+
~cognitoUserPoolArn: Pulumi.Input.t<string>,
|
|
164
|
+
~name: string="ActiveRoleTrigger",
|
|
165
|
+
~opts: Pulumi.ComponentResource.options,
|
|
166
|
+
): unit => {
|
|
167
|
+
let opts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
168
|
+
let _permission = trigger.functionName->Pulumi.Output.apply(functionName => {
|
|
169
|
+
let _ = Lambda.Permission.make(
|
|
170
|
+
~name=name ++ "Invoke",
|
|
171
|
+
~args={
|
|
172
|
+
action: "lambda:InvokeFunction",
|
|
173
|
+
function: functionName->Pulumi.Input.make,
|
|
174
|
+
principal: "cognito-idp.amazonaws.com",
|
|
175
|
+
sourceArn: cognitoUserPoolArn,
|
|
176
|
+
},
|
|
177
|
+
~opts,
|
|
178
|
+
)
|
|
179
|
+
})
|
|
180
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Aws from "@pulumi/aws";
|
|
4
|
+
import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
7
|
+
import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
|
|
8
|
+
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
9
|
+
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
10
|
+
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
11
|
+
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
12
|
+
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
13
|
+
import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
|
|
14
|
+
|
|
15
|
+
function make(activeRoleTableName, activeRoleTableArn, nameOpt, opts) {
|
|
16
|
+
let name = nameOpt !== undefined ? nameOpt : "ActiveRoleTrigger";
|
|
17
|
+
let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
18
|
+
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.Lambda.principal), AWS_Tags$ReventlessAws.make(name, "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
|
|
19
|
+
activeRoleTableArn.apply(tableArn => {
|
|
20
|
+
new (Aws.iam.RolePolicy)(name + "Policy", {
|
|
21
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "Policy", [
|
|
22
|
+
{
|
|
23
|
+
Sid: "AllowLambdaLogging",
|
|
24
|
+
Effect: "Allow",
|
|
25
|
+
Action: [
|
|
26
|
+
"logs:CreateLogGroup",
|
|
27
|
+
"logs:CreateLogStream",
|
|
28
|
+
"logs:PutLogEvents"
|
|
29
|
+
],
|
|
30
|
+
Resource: "arn:aws:logs:*:*:*"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
Sid: "AllowReadingStoredRole",
|
|
34
|
+
Effect: "Allow",
|
|
35
|
+
Action: "dynamodb:GetItem",
|
|
36
|
+
Resource: tableArn
|
|
37
|
+
}
|
|
38
|
+
])),
|
|
39
|
+
role: lambdaRole.id
|
|
40
|
+
}, opts$1);
|
|
41
|
+
});
|
|
42
|
+
let packageDirs = Object.fromEntries([[
|
|
43
|
+
"@reventlessdev/reventless-aws",
|
|
44
|
+
Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
|
|
45
|
+
]]);
|
|
46
|
+
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs", packageDirs, undefined, false);
|
|
47
|
+
let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
|
|
48
|
+
let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name, undefined, AWS_Tags$ReventlessAws.make(name + "LogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
|
|
49
|
+
let lambda = new (Aws.lambda.Function)(name, {
|
|
50
|
+
handler: "index.handler",
|
|
51
|
+
runtime: "nodejs22.x",
|
|
52
|
+
code: match.code,
|
|
53
|
+
role: lambdaRole.arn,
|
|
54
|
+
memorySize: 256,
|
|
55
|
+
timeout: 5,
|
|
56
|
+
layers: layers,
|
|
57
|
+
tags: AWS_Tags$ReventlessAws.make(name, "Platform", "Runtime", "Platform", undefined, undefined, undefined, undefined),
|
|
58
|
+
environment: {
|
|
59
|
+
variables: Object.fromEntries([
|
|
60
|
+
[
|
|
61
|
+
"Environment",
|
|
62
|
+
Pulumi.getStack()
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
"ACTIVE_ROLE_TABLE",
|
|
66
|
+
activeRoleTableName
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
"NODE_OPTIONS",
|
|
70
|
+
Util_Bundle$ReventlessAws.esmLoaderNodeOptions
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"ESM_FALLBACK_DIRS",
|
|
74
|
+
Util_Bundle$ReventlessAws.esmFallbackDirs
|
|
75
|
+
],
|
|
76
|
+
Util_LambdaLogging$ReventlessAws.logLevelEntry()
|
|
77
|
+
])
|
|
78
|
+
},
|
|
79
|
+
sourceCodeHash: match.sourceCodeHash,
|
|
80
|
+
loggingConfig: Util_LambdaLogging$ReventlessAws.loggingConfigFor(logGroup)
|
|
81
|
+
}, opts$1);
|
|
82
|
+
return {
|
|
83
|
+
functionArn: lambda.arn,
|
|
84
|
+
functionName: lambda.name
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function grantInvoke(trigger, cognitoUserPoolArn, nameOpt, opts) {
|
|
89
|
+
let name = nameOpt !== undefined ? nameOpt : "ActiveRoleTrigger";
|
|
90
|
+
let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
91
|
+
trigger.functionName.apply(functionName => {
|
|
92
|
+
new (Aws.lambda.Permission)(name + "Invoke", {
|
|
93
|
+
action: "lambda:InvokeFunction",
|
|
94
|
+
function: functionName,
|
|
95
|
+
principal: "cognito-idp.amazonaws.com",
|
|
96
|
+
sourceArn: cognitoUserPoolArn
|
|
97
|
+
}, opts$1);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
make,
|
|
103
|
+
grantInvoke,
|
|
104
|
+
}
|
|
105
|
+
/* @pulumi/aws Not a pure module */
|