1
1
import {
2
+ BatchWriteItemCommand ,
2
3
ConditionalCheckFailedException ,
3
4
DynamoDBClient ,
4
5
PutItemCommand ,
5
6
QueryCommand ,
6
7
} from "@aws-sdk/client-dynamodb" ;
7
- import { marshall } from "@aws-sdk/util-dynamodb" ;
8
+ import { marshall , unmarshall } from "@aws-sdk/util-dynamodb" ;
8
9
import { genericConfig } from "common/config.js" ;
9
10
import {
10
11
addToTenant ,
@@ -13,15 +14,161 @@ import {
13
14
patchUserProfile ,
14
15
resolveEmailToOid ,
15
16
} from "./entraId.js" ;
16
- import { EntraGroupError } from "common/errors/index.js" ;
17
+ import { EntraGroupError , ValidationError } from "common/errors/index.js" ;
17
18
import { EntraGroupActions } from "common/types/iam.js" ;
18
19
import { pollUntilNoError } from "./general.js" ;
19
20
import Redis from "ioredis" ;
20
- import { getKey } from "./redisCache.js" ;
21
+ import { getKey , setKey } from "./redisCache.js" ;
21
22
import { FastifyBaseLogger } from "fastify" ;
23
+ import type pino from "pino" ;
24
+ import { createAuditLogEntry } from "./auditLog.js" ;
25
+ import { Modules } from "common/modules.js" ;
22
26
23
27
export const MEMBER_CACHE_SECONDS = 43200 ; // 12 hours
24
28
29
+ export async function patchExternalMemberList ( {
30
+ listId : oldListId ,
31
+ add : oldAdd ,
32
+ remove : oldRemove ,
33
+ clients : { dynamoClient, redisClient } ,
34
+ logger,
35
+ auditLogData : { actor, requestId } ,
36
+ } : {
37
+ listId : string ;
38
+ add : string [ ] ;
39
+ remove : string [ ] ;
40
+ clients : { dynamoClient : DynamoDBClient ; redisClient : Redis . default } ;
41
+ logger : pino . Logger | FastifyBaseLogger ;
42
+ auditLogData : { actor : string ; requestId : string } ;
43
+ } ) {
44
+ const listId = oldListId . toLowerCase ( ) ;
45
+ const add = oldAdd . map ( ( x ) => x . toLowerCase ( ) ) ;
46
+ const remove = oldRemove . map ( ( x ) => x . toLowerCase ( ) ) ;
47
+ if ( add . length === 0 && remove . length === 0 ) {
48
+ return ;
49
+ }
50
+ const addSet = new Set ( add ) ;
51
+
52
+ const conflictingNetId = remove . find ( ( netId ) => addSet . has ( netId ) ) ;
53
+
54
+ if ( conflictingNetId ) {
55
+ throw new ValidationError ( {
56
+ message : `The netId '${ conflictingNetId } ' cannot be in both the 'add' and 'remove' lists simultaneously.` ,
57
+ } ) ;
58
+ }
59
+ const writeRequests = [ ] ;
60
+ // Create PutRequest objects for each member to be added.
61
+ for ( const netId of add ) {
62
+ writeRequests . push ( {
63
+ PutRequest : {
64
+ Item : {
65
+ memberList : { S : listId } ,
66
+ netId : { S : netId } ,
67
+ } ,
68
+ } ,
69
+ } ) ;
70
+ }
71
+ // Create DeleteRequest objects for each member to be removed.
72
+ for ( const netId of remove ) {
73
+ writeRequests . push ( {
74
+ DeleteRequest : {
75
+ Key : {
76
+ memberList : { S : listId } ,
77
+ netId : { S : netId } ,
78
+ } ,
79
+ } ,
80
+ } ) ;
81
+ }
82
+ const BATCH_SIZE = 25 ;
83
+ const batchPromises = [ ] ;
84
+ for ( let i = 0 ; i < writeRequests . length ; i += BATCH_SIZE ) {
85
+ const batch = writeRequests . slice ( i , i + BATCH_SIZE ) ;
86
+ const command = new BatchWriteItemCommand ( {
87
+ RequestItems : {
88
+ [ genericConfig . ExternalMembershipTableName ] : batch ,
89
+ } ,
90
+ } ) ;
91
+ batchPromises . push ( dynamoClient . send ( command ) ) ;
92
+ }
93
+ const removeCacheInvalidation = remove . map ( ( x ) =>
94
+ setKey ( {
95
+ redisClient,
96
+ key : `membership:${ x } :${ listId } ` ,
97
+ data : JSON . stringify ( { isMember : false } ) ,
98
+ expiresIn : MEMBER_CACHE_SECONDS ,
99
+ logger,
100
+ } ) ,
101
+ ) ;
102
+ const addCacheInvalidation = add . map ( ( x ) =>
103
+ setKey ( {
104
+ redisClient,
105
+ key : `membership:${ x } :${ listId } ` ,
106
+ data : JSON . stringify ( { isMember : true } ) ,
107
+ expiresIn : MEMBER_CACHE_SECONDS ,
108
+ logger,
109
+ } ) ,
110
+ ) ;
111
+ const auditLogPromises = [ ] ;
112
+ if ( add . length > 0 ) {
113
+ auditLogPromises . push (
114
+ createAuditLogEntry ( {
115
+ dynamoClient,
116
+ entry : {
117
+ module : Modules . EXTERNAL_MEMBERSHIP ,
118
+ actor,
119
+ requestId,
120
+ message : `Added ${ add . length } member(s) to target list.` ,
121
+ target : listId ,
122
+ } ,
123
+ } ) ,
124
+ ) ;
125
+ }
126
+ if ( remove . length > 0 ) {
127
+ auditLogPromises . push (
128
+ createAuditLogEntry ( {
129
+ dynamoClient,
130
+ entry : {
131
+ module : Modules . EXTERNAL_MEMBERSHIP ,
132
+ actor,
133
+ requestId,
134
+ message : `Removed ${ remove . length } member(s) from target list.` ,
135
+ target : listId ,
136
+ } ,
137
+ } ) ,
138
+ ) ;
139
+ }
140
+ await Promise . all ( [
141
+ ...removeCacheInvalidation ,
142
+ ...addCacheInvalidation ,
143
+ ...batchPromises ,
144
+ ] ) ;
145
+ await Promise . all ( auditLogPromises ) ;
146
+ }
147
+ export async function getExternalMemberList (
148
+ list : string ,
149
+ dynamoClient : DynamoDBClient ,
150
+ ) : Promise < string [ ] > {
151
+ const { Items } = await dynamoClient . send (
152
+ new QueryCommand ( {
153
+ TableName : genericConfig . ExternalMembershipTableName ,
154
+ KeyConditionExpression : "#pk = :pk" ,
155
+ ExpressionAttributeNames : {
156
+ "#pk" : "memberList" ,
157
+ } ,
158
+ ExpressionAttributeValues : marshall ( {
159
+ ":pk" : list ,
160
+ } ) ,
161
+ } ) ,
162
+ ) ;
163
+ if ( ! Items || Items . length === 0 ) {
164
+ return [ ] ;
165
+ }
166
+ return Items . map ( ( x ) => unmarshall ( x ) )
167
+ . filter ( ( x ) => ! ! x )
168
+ . map ( ( x ) => x . netId )
169
+ . sort ( ) ;
170
+ }
171
+
25
172
export async function checkExternalMembership (
26
173
netId : string ,
27
174
list : string ,
@@ -30,12 +177,15 @@ export async function checkExternalMembership(
30
177
const { Items } = await dynamoClient . send (
31
178
new QueryCommand ( {
32
179
TableName : genericConfig . ExternalMembershipTableName ,
33
- KeyConditionExpression : "#pk = :pk" ,
180
+ KeyConditionExpression : "#pk = :pk and #sk = :sk" ,
181
+ IndexName : "invertedIndex" ,
34
182
ExpressionAttributeNames : {
35
- "#pk" : "netid_list" ,
183
+ "#pk" : "netId" ,
184
+ "#sk" : "memberList" ,
36
185
} ,
37
186
ExpressionAttributeValues : marshall ( {
38
- ":pk" : `${ netId } _${ list } ` ,
187
+ ":pk" : netId ,
188
+ ":sk" : list ,
39
189
} ) ,
40
190
} ) ,
41
191
) ;
0 commit comments