11import {
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" ;
89import { genericConfig } from "common/config.js" ;
910import {
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" ;
1718import { EntraGroupActions } from "common/types/iam.js" ;
1819import { pollUntilNoError } from "./general.js" ;
1920import Redis from "ioredis" ;
20- import { getKey } from "./redisCache.js" ;
21+ import { getKey , setKey } from "./redisCache.js" ;
2122import { FastifyBaseLogger } from "fastify" ;
23+ import type pino from "pino" ;
2224
2325export 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+
25137export 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