Skip to content

Commit adb3c42

Browse files
committed
read from external membership v2 table
1 parent 96ec366 commit adb3c42

File tree

8 files changed

+302
-18
lines changed

8 files changed

+302
-18
lines changed

cloudformation/main.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,12 @@ Resources:
444444
KeyType: "RANGE"
445445
Projection:
446446
ProjectionType: "KEYS_ONLY"
447+
- IndexName: "keysOnlyIndex"
448+
KeySchema:
449+
- AttributeName: "memberList"
450+
KeyType: "HASH"
451+
Projection:
452+
ProjectionType: "KEYS_ONLY"
447453

448454

449455
RoomRequestsTable:

generate_jwt.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
GetSecretValueCommand,
55
} from "@aws-sdk/client-secrets-manager";
66
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
7+
import { randomUUID } from "crypto";
78

89
export const getSecretValue = async (secretId) => {
910
const smClient = new SecretsManagerClient();
@@ -56,7 +57,7 @@ const payload = {
5657
sub: "subject",
5758
tid: "tenant-id",
5859
unique_name: username,
59-
uti: "uti-value",
60+
uti: randomUUID(),
6061
ver: "1.0",
6162
};
6263

src/api/functions/membership.ts

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {
2+
BatchWriteItemCommand,
23
ConditionalCheckFailedException,
34
DynamoDBClient,
45
PutItemCommand,
56
QueryCommand,
67
} from "@aws-sdk/client-dynamodb";
7-
import { marshall } from "@aws-sdk/util-dynamodb";
8+
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
89
import { genericConfig } from "common/config.js";
910
import {
1011
addToTenant,
@@ -13,15 +14,126 @@ import {
1314
patchUserProfile,
1415
resolveEmailToOid,
1516
} from "./entraId.js";
16-
import { EntraGroupError } from "common/errors/index.js";
17+
import { EntraGroupError, ValidationError } from "common/errors/index.js";
1718
import { EntraGroupActions } from "common/types/iam.js";
1819
import { pollUntilNoError } from "./general.js";
1920
import Redis from "ioredis";
20-
import { getKey } from "./redisCache.js";
21+
import { getKey, setKey } from "./redisCache.js";
2122
import { FastifyBaseLogger } from "fastify";
23+
import type pino from "pino";
2224

2325
export const MEMBER_CACHE_SECONDS = 43200; // 12 hours
2426

27+
export async function patchExternalMemberList({
28+
listId: oldListId,
29+
add: oldAdd,
30+
remove: oldRemove,
31+
clients: { dynamoClient, redisClient },
32+
logger,
33+
}: {
34+
listId: string;
35+
add: string[];
36+
remove: string[];
37+
clients: { dynamoClient: DynamoDBClient; redisClient: Redis.default };
38+
logger: pino.Logger | FastifyBaseLogger;
39+
}) {
40+
const listId = oldListId.toLowerCase();
41+
const add = oldAdd.map((x) => x.toLowerCase());
42+
const remove = oldRemove.map((x) => x.toLowerCase());
43+
if (add.length === 0 && remove.length === 0) {
44+
return;
45+
}
46+
const addSet = new Set(add);
47+
48+
const conflictingNetId = remove.find((netId) => addSet.has(netId));
49+
50+
if (conflictingNetId) {
51+
throw new ValidationError({
52+
message: `The netId '${conflictingNetId}' cannot be in both the 'add' and 'remove' lists simultaneously.`,
53+
});
54+
}
55+
const writeRequests = [];
56+
// Create PutRequest objects for each member to be added.
57+
for (const netId of add) {
58+
writeRequests.push({
59+
PutRequest: {
60+
Item: {
61+
memberList: { S: listId },
62+
netId: { S: netId },
63+
},
64+
},
65+
});
66+
}
67+
// Create DeleteRequest objects for each member to be removed.
68+
for (const netId of remove) {
69+
writeRequests.push({
70+
DeleteRequest: {
71+
Key: {
72+
memberList: { S: listId },
73+
netId: { S: netId },
74+
},
75+
},
76+
});
77+
}
78+
const BATCH_SIZE = 25;
79+
const batchPromises = [];
80+
for (let i = 0; i < writeRequests.length; i += BATCH_SIZE) {
81+
const batch = writeRequests.slice(i, i + BATCH_SIZE);
82+
const command = new BatchWriteItemCommand({
83+
RequestItems: {
84+
[genericConfig.ExternalMembershipTableName]: batch,
85+
},
86+
});
87+
batchPromises.push(dynamoClient.send(command));
88+
}
89+
const removeCacheInvalidation = remove.map((x) =>
90+
setKey({
91+
redisClient,
92+
key: `membership:${x}:${listId}`,
93+
data: JSON.stringify({ isMember: false }),
94+
expiresIn: MEMBER_CACHE_SECONDS,
95+
logger,
96+
}),
97+
);
98+
const addCacheInvalidation = add.map((x) =>
99+
setKey({
100+
redisClient,
101+
key: `membership:${x}:${listId}`,
102+
data: JSON.stringify({ isMember: true }),
103+
expiresIn: MEMBER_CACHE_SECONDS,
104+
logger,
105+
}),
106+
);
107+
await Promise.all([
108+
...removeCacheInvalidation,
109+
...addCacheInvalidation,
110+
...batchPromises,
111+
]);
112+
}
113+
export async function getExternalMemberList(
114+
list: string,
115+
dynamoClient: DynamoDBClient,
116+
): Promise<string[]> {
117+
const { Items } = await dynamoClient.send(
118+
new QueryCommand({
119+
TableName: genericConfig.ExternalMembershipTableName,
120+
KeyConditionExpression: "#pk = :pk",
121+
ExpressionAttributeNames: {
122+
"#pk": "memberList",
123+
},
124+
ExpressionAttributeValues: marshall({
125+
":pk": list,
126+
}),
127+
}),
128+
);
129+
if (!Items || Items.length === 0) {
130+
return [];
131+
}
132+
return Items.map((x) => unmarshall(x))
133+
.filter((x) => !!x)
134+
.map((x) => x.netId);
135+
}
136+
25137
export async function checkExternalMembership(
26138
netId: string,
27139
list: string,
@@ -31,6 +143,7 @@ export async function checkExternalMembership(
31143
new QueryCommand({
32144
TableName: genericConfig.ExternalMembershipTableName,
33145
KeyConditionExpression: "#pk = :pk and #sk = :sk",
146+
IndexName: "invertedIndex",
34147
ExpressionAttributeNames: {
35148
"#pk": "netId",
36149
"#sk": "memberList",

0 commit comments

Comments
 (0)