Skip to content

Commit 5d3d7f3

Browse files
committed
add external list support
1 parent 6fe0a7c commit 5d3d7f3

File tree

5 files changed

+82
-10
lines changed

5 files changed

+82
-10
lines changed

cloudformation/iam.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Resources:
6868
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-grouproles
6969
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links
7070
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-membership-provisioning
71+
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-membership-external
7172

7273
- Sid: DynamoDBCacheAccess
7374
Effect: Allow

cloudformation/main.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,23 @@ Resources:
275275
- AttributeName: email
276276
KeyType: HASH
277277

278+
ExternalMembershipRecordsTable:
279+
Type: "AWS::DynamoDB::Table"
280+
DeletionPolicy: "Retain"
281+
UpdateReplacePolicy: "Retain"
282+
Properties:
283+
BillingMode: "PAY_PER_REQUEST"
284+
TableName: infra-core-api-membership-external
285+
DeletionProtectionEnabled: true
286+
PointInTimeRecoverySpecification:
287+
PointInTimeRecoveryEnabled: !If [IsProd, true, false]
288+
AttributeDefinitions:
289+
- AttributeName: netid_list
290+
AttributeType: S
291+
KeySchema:
292+
- AttributeName: netid_list
293+
KeyType: HASH
294+
278295
IamGroupRolesTable:
279296
Type: "AWS::DynamoDB::Table"
280297
DeletionPolicy: "Retain"

src/api/functions/membership.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,29 @@ import { isUserInGroup, modifyGroup } from "./entraId.js";
1010
import { EntraGroupError } from "common/errors/index.js";
1111
import { EntraGroupActions } from "common/types/iam.js";
1212

13+
export async function checkExternalMembership(
14+
netId: string,
15+
list: string,
16+
dynamoClient: DynamoDBClient,
17+
): Promise<boolean> {
18+
const { Items } = await dynamoClient.send(
19+
new QueryCommand({
20+
TableName: genericConfig.ExternalMembershipTableName,
21+
KeyConditionExpression: "#pk = :pk",
22+
ExpressionAttributeNames: {
23+
"#pk": "netid_list",
24+
},
25+
ExpressionAttributeValues: marshall({
26+
":pk": `${netId}_${list}`,
27+
}),
28+
}),
29+
);
30+
if (!Items || Items.length == 0) {
31+
return false;
32+
}
33+
return true;
34+
}
35+
1336
export async function checkPaidMembershipFromTable(
1437
netId: string,
1538
dynamoClient: DynamoDBClient,

src/api/routes/membership.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
checkExternalMembership,
23
checkPaidMembershipFromEntra,
34
checkPaidMembershipFromTable,
45
setPaidMembershipInTable,
@@ -14,7 +15,7 @@ import { getEntraIdToken } from "api/functions/entraId.js";
1415
import { genericConfig, roleArns } from "common/config.js";
1516
import { getRoleCredentials } from "api/functions/sts.js";
1617
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
17-
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
18+
import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb";
1819
import rateLimiter from "api/plugins/rateLimiter.js";
1920
import { createCheckoutSession } from "api/functions/stripe.js";
2021
import { getSecretValue } from "api/plugins/auth.js";
@@ -70,9 +71,9 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
7071
});
7172
fastify.get<{
7273
Body: undefined;
73-
Querystring: { netId: string };
74+
Params: { netId: string };
7475
}>("/checkout/:netId", async (request, reply) => {
75-
const netId = (request.params as Record<string, string>).netId;
76+
const netId = request.params.netId;
7677
if (!validateNetId(netId)) {
7778
throw new ValidationError({
7879
message: `${netId} is not a valid Illinois NetID!`,
@@ -143,26 +144,50 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
143144
});
144145
fastify.get<{
145146
Body: undefined;
146-
Querystring: { netId: string };
147+
Querystring: { list?: string };
148+
Params: { netId: string };
147149
}>("/:netId", async (request, reply) => {
148-
const netId = (request.params as Record<string, string>).netId;
150+
const netId = request.params.netId;
151+
const list = request.query.list || "acmpaid";
149152
if (!validateNetId(netId)) {
150153
throw new ValidationError({
151154
message: `${netId} is not a valid Illinois NetID!`,
152155
});
153156
}
154-
if (fastify.nodeCache.get(`isMember_${netId}`) !== undefined) {
157+
if (fastify.nodeCache.get(`isMember_${netId}_${list}`) !== undefined) {
155158
return reply.header("X-ACM-Data-Source", "cache").send({
156159
netId,
157-
isPaidMember: fastify.nodeCache.get(`isMember_${netId}`),
160+
list: list === "acmpaid" ? undefined : list,
161+
isPaidMember: fastify.nodeCache.get(`isMember_${netId}_${list}`),
162+
});
163+
}
164+
if (list !== "acmpaid") {
165+
const isMember = await checkExternalMembership(
166+
netId,
167+
list,
168+
fastify.dynamoClient,
169+
);
170+
fastify.nodeCache.set(
171+
`isMember_${netId}_${list}`,
172+
isMember,
173+
MEMBER_CACHE_SECONDS,
174+
);
175+
return reply.header("X-ACM-Data-Source", "dynamo").send({
176+
netId,
177+
list,
178+
isPaidMember: isMember,
158179
});
159180
}
160181
const isDynamoMember = await checkPaidMembershipFromTable(
161182
netId,
162183
fastify.dynamoClient,
163184
);
164185
if (isDynamoMember) {
165-
fastify.nodeCache.set(`isMember_${netId}`, true, MEMBER_CACHE_SECONDS);
186+
fastify.nodeCache.set(
187+
`isMember_${netId}_${list}`,
188+
true,
189+
MEMBER_CACHE_SECONDS,
190+
);
166191
return reply
167192
.header("X-ACM-Data-Source", "dynamo")
168193
.send({ netId, isPaidMember: true });
@@ -178,15 +203,19 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
178203
paidMemberGroup,
179204
);
180205
if (isAadMember) {
181-
fastify.nodeCache.set(`isMember_${netId}`, true, MEMBER_CACHE_SECONDS);
206+
fastify.nodeCache.set(
207+
`isMember_${netId}_${list}`,
208+
true,
209+
MEMBER_CACHE_SECONDS,
210+
);
182211
reply
183212
.header("X-ACM-Data-Source", "aad")
184213
.send({ netId, isPaidMember: true });
185214
await setPaidMembershipInTable(netId, fastify.dynamoClient);
186215
return;
187216
}
188217
fastify.nodeCache.set(
189-
`isMember_${netId}`,
218+
`isMember_${netId}_${list}`,
190219
false,
191220
NONMEMBER_CACHE_SECONDS,
192221
);

src/common/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type GenericConfigType = {
3535
TicketPurchasesTableName: string;
3636
TicketMetadataTableName: string;
3737
MembershipTableName: string;
38+
ExternalMembershipTableName: string;
3839
MerchStoreMetadataTableName: string;
3940
IAMTablePrefix: string;
4041
ProtectedEntraIDGroups: string[]; // these groups are too privileged to be modified via this portal and must be modified directly in Entra ID.
@@ -70,6 +71,7 @@ const genericConfig: GenericConfigType = {
7071
IAMTablePrefix: "infra-core-api-iam",
7172
ProtectedEntraIDGroups: [infraChairsGroupId, officersGroupId],
7273
MembershipTableName: "infra-core-api-membership-provisioning",
74+
ExternalMembershipTableName: "infra-membership-api-external-lists"
7375
} as const;
7476

7577
const environmentConfig: EnvironmentConfigType = {

0 commit comments

Comments
 (0)