Skip to content

Commit 3d58b0a

Browse files
committed
Add member to sig WIP
1 parent db23ccc commit 3d58b0a

File tree

4 files changed

+309
-51
lines changed

4 files changed

+309
-51
lines changed

src/api/functions/siglead.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
import {
2+
DeleteItemCommand,
23
DynamoDBClient,
4+
PutItemCommand,
35
QueryCommand,
46
ScanCommand,
57
} from "@aws-sdk/client-dynamodb";
6-
import { unmarshall } from "@aws-sdk/util-dynamodb";
8+
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
9+
import {
10+
EntraFetchError,
11+
EntraGroupError,
12+
EntraPatchError,
13+
NotImplementedError,
14+
} from "common/errors/index.js";
715
import { OrganizationList } from "common/orgs.js";
816
import {
917
SigDetailRecord,
18+
SigEntraRecord,
1019
SigMemberCount,
1120
SigMemberRecord,
1221
} from "common/types/siglead.js";
1322
import { transformSigLeadToURI } from "common/utils.js";
1423
import { string } from "zod";
24+
import { getEntraIdToken, modifyGroup } from "./entraId.js";
25+
import { EntraGroupActions } from "common/types/iam.js";
1526

1627
export async function fetchMemberRecords(
1728
sigid: string,
@@ -62,14 +73,42 @@ export async function fetchSigDetail(
6273
return (result.Items || [{}]).map((item) => {
6374
const unmarshalledItem = unmarshall(item);
6475

65-
// Strip '#' from access field
6676
delete unmarshalledItem.leadGroupId;
6777
delete unmarshalledItem.memberGroupId;
6878

6979
return unmarshalledItem as SigDetailRecord;
7080
})[0];
7181
}
7282

83+
export async function fetchSigEntraDetail(
84+
sigid: string,
85+
tableName: string,
86+
dynamoClient: DynamoDBClient,
87+
) {
88+
const fetchSigDetail = new QueryCommand({
89+
TableName: tableName,
90+
KeyConditionExpression: "#sigid = :accessVal",
91+
ExpressionAttributeNames: {
92+
"#sigid": "sigid",
93+
},
94+
ExpressionAttributeValues: {
95+
":accessVal": { S: sigid },
96+
},
97+
ScanIndexForward: false,
98+
});
99+
100+
const result = await dynamoClient.send(fetchSigDetail);
101+
102+
// Process the results
103+
return (result.Items || [{}]).map((item) => {
104+
const unmarshalledItem = unmarshall(item);
105+
106+
delete unmarshalledItem.description;
107+
108+
return unmarshalledItem as SigEntraRecord;
109+
})[0];
110+
}
111+
73112
// select count(sigid)
74113
// from table
75114
// groupby sigid
@@ -113,3 +152,46 @@ export async function fetchSigCounts(
113152
console.log(countsArray);
114153
return countsArray;
115154
}
155+
156+
export async function addMemberRecordToSig(
157+
newMemberRecord: SigMemberRecord,
158+
sigMemberTableName: string,
159+
dynamoClient: DynamoDBClient,
160+
entraIdToken: string,
161+
) {
162+
await dynamoClient.send(
163+
new PutItemCommand({
164+
TableName: sigMemberTableName,
165+
Item: marshall(newMemberRecord),
166+
}),
167+
);
168+
try {
169+
const sigEntraDetails: SigEntraRecord = await fetchSigEntraDetail(
170+
newMemberRecord.sigGroupId,
171+
sigMemberTableName,
172+
dynamoClient,
173+
);
174+
await modifyGroup(
175+
entraIdToken,
176+
newMemberRecord.email,
177+
sigEntraDetails.memberGroupId,
178+
EntraGroupActions.ADD,
179+
dynamoClient,
180+
);
181+
} catch (e: unknown) {
182+
// restore original Dynamo status if AAD update fails.
183+
await dynamoClient.send(
184+
new DeleteItemCommand({
185+
TableName: sigMemberTableName,
186+
Key: {
187+
sigGroupId: { S: newMemberRecord.sigGroupId },
188+
email: { S: newMemberRecord.email },
189+
},
190+
}),
191+
);
192+
throw new EntraPatchError({
193+
message: "Could not add member to sig AAD group.",
194+
email: newMemberRecord.email,
195+
});
196+
}
197+
}

src/api/routes/siglead.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FastifyPluginAsync } from "fastify";
1+
import fastify, { FastifyPluginAsync } from "fastify";
22
import { z } from "zod";
33
import { AppRoles } from "../../common/roles.js";
44
import {
@@ -22,12 +22,14 @@ import {
2222
TransactWriteItem,
2323
GetItemCommand,
2424
TransactionCanceledException,
25+
InternalServerError,
2526
} from "@aws-sdk/client-dynamodb";
2627
import { CloudFrontKeyValueStoreClient } from "@aws-sdk/client-cloudfront-keyvaluestore";
2728
import {
2829
genericConfig,
2930
EVENT_CACHED_DURATION,
3031
LinkryGroupUUIDToGroupNameMap,
32+
roleArns,
3133
} from "../../common/config.js";
3234
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
3335
import rateLimiter from "api/plugins/rateLimiter.js";
@@ -44,13 +46,62 @@ import {
4446
SigMemberRecord,
4547
} from "common/types/siglead.js";
4648
import {
49+
addMemberRecordToSig,
4750
fetchMemberRecords,
4851
fetchSigCounts,
4952
fetchSigDetail,
5053
} from "api/functions/siglead.js";
5154
import { intersection } from "api/plugins/auth.js";
55+
import {
56+
FastifyZodOpenApiSchema,
57+
FastifyZodOpenApiTypeProvider,
58+
} from "fastify-zod-openapi";
59+
import { withRoles, withTags } from "api/components/index.js";
60+
import { AnyARecord } from "dns";
61+
import { getEntraIdToken } from "api/functions/entraId.js";
62+
import { getRoleCredentials } from "api/functions/sts.js";
63+
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
64+
import { logger } from "api/sqs/logger.js";
65+
import fastifyStatic from "@fastify/static";
66+
67+
const postAddSigMemberSchema = z.object({
68+
sigGroupId: z.string().min(1),
69+
email: z.string().min(1), // TODO: verify email and @illinois.edu
70+
designation: z.string().min(1).max(1),
71+
memberName: z.string().min(1),
72+
});
5273

5374
const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => {
75+
const getAuthorizedClients = async () => {
76+
if (roleArns.Entra) {
77+
fastify.log.info(
78+
`Attempting to assume Entra role ${roleArns.Entra} to get the Entra token...`,
79+
);
80+
const credentials = await getRoleCredentials(roleArns.Entra);
81+
const clients = {
82+
smClient: new SecretsManagerClient({
83+
region: genericConfig.AwsRegion,
84+
credentials,
85+
}),
86+
dynamoClient: new DynamoDBClient({
87+
region: genericConfig.AwsRegion,
88+
credentials,
89+
}),
90+
};
91+
fastify.log.info(
92+
`Assumed Entra role ${roleArns.Entra} to get the Entra token.`,
93+
);
94+
return clients;
95+
} else {
96+
fastify.log.debug(
97+
"Did not assume Entra role as no env variable was present",
98+
);
99+
return {
100+
smClient: fastify.secretsManagerClient,
101+
dynamoClient: fastify.dynamoClient,
102+
};
103+
}
104+
};
54105
const limitedRoutes: FastifyPluginAsync = async (fastify) => {
55106
/*fastify.register(rateLimiter, {
56107
limit: 30,
@@ -163,6 +214,81 @@ const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => {
163214
reply.code(200).send(sigMemCounts);
164215
},
165216
);
217+
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().post(
218+
"/addMember/:sigid",
219+
{
220+
schema: withRoles(
221+
[AppRoles.SIGLEAD_MANAGER],
222+
withTags(["Sigid"], {
223+
// response: {
224+
// 201: z.object({
225+
// id: z.string(),
226+
// resource: z.string(),
227+
// }),
228+
// },
229+
body: postAddSigMemberSchema,
230+
summary: "Add a member to a sig.",
231+
}),
232+
) satisfies FastifyZodOpenApiSchema,
233+
onRequest: fastify.authorizeFromSchema,
234+
},
235+
async (request, reply) => {
236+
const { sigGroupId, email, designation, memberName } = request.body;
237+
const tableName = genericConfig.SigleadDynamoSigMemberTableName;
238+
239+
// First try-catch: See if the member already exists
240+
let sigMembers: SigMemberRecord[];
241+
try {
242+
sigMembers = await fetchMemberRecords(
243+
sigGroupId,
244+
tableName,
245+
fastify.dynamoClient,
246+
);
247+
} catch (error) {
248+
request.log.error(
249+
`Could not verify the member does not already exist in the sig: ${error instanceof Error ? error.toString() : "Unknown error"}`,
250+
);
251+
throw new DatabaseFetchError({
252+
message: "Failed to fetch sig member records from Dynamo table.",
253+
});
254+
}
255+
256+
for (const sigMember of sigMembers) {
257+
if (sigMember.email === email) {
258+
throw new ValidationError({
259+
message: "Member already exists in sig.",
260+
});
261+
}
262+
}
263+
264+
const newMemberRecord: SigMemberRecord = request.body;
265+
// Second try-catch: Try to add the member to Dynamo and AAD, rolling back if failure
266+
try {
267+
//FIXME: this is failing due to auth
268+
const entraIdToken = await getEntraIdToken(
269+
await getAuthorizedClients(),
270+
fastify.environmentConfig.AadValidClientId,
271+
);
272+
273+
await addMemberRecordToSig(
274+
newMemberRecord,
275+
tableName,
276+
fastify.dynamoClient,
277+
entraIdToken,
278+
);
279+
} catch (error: any) {
280+
request.log.error(
281+
`Error while adding member to sig: ${error instanceof Error ? error.toString() : "Unknown error"}`,
282+
);
283+
throw error;
284+
}
285+
286+
// Send the response
287+
reply.code(200).send({
288+
message: "Added member to sig.",
289+
});
290+
},
291+
);
166292
};
167293

168294
fastify.register(limitedRoutes);

src/common/types/siglead.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ export type SigDetailRecord = {
44
description: string;
55
};
66

7+
export type SigEntraRecord = {
8+
sigid: string;
9+
signame: string;
10+
leadGroupId: string;
11+
memberGroupId: string;
12+
};
13+
714
export type SigMemberRecord = {
815
sigGroupId: string;
916
email: string;

0 commit comments

Comments
 (0)