@@ -83,45 +83,15 @@ function parseRoleArn(arn) {
8383 return { accountId : match [ 1 ] , roleName : match [ 2 ] } ;
8484}
8585
86- /**
87- * Group missing entries by role, collecting all affected buckets per role.
88- *
89- * Input (results from check-replication-permissions.js):
90- * [
91- * { bucket: "bucket-old-1", ownerDisplayName: "testaccount", sourceRole: "arn:aws:iam::123:role/crr-role" },
92- * { bucket: "bucket-old-2", ownerDisplayName: "testaccount", sourceRole: "arn:aws:iam::123:role/crr-role" },
93- * ]
94- *
95- * Output:
96- * [
97- * { accountId: "123", accountName: "testaccount", roleName: "crr-role",
98- * roleArn: "arn:aws:iam::123:role/crr-role", buckets: ["bucket-old-1", "bucket-old-2"] }
99- * ]
100- */
101- function groupByRole ( results ) {
102- const roles = Object . groupBy ( results , entry => entry . sourceRole ) ;
103-
104- return Object . entries ( roles ) . map ( ( [ roleArn , entries ] ) => {
105- const { accountId, roleName } = parseRoleArn ( roleArn ) ;
106- return {
107- accountId,
108- accountName : entries [ 0 ] . ownerDisplayName ,
109- roleName,
110- roleArn,
111- buckets : entries . map ( e => e . bucket ) ,
112- } ;
113- } ) ;
114- }
115-
116- /** Build the IAM policy document */
117- function buildPolicyDocument ( buckets ) {
86+ /** Build the IAM policy document for a single bucket */
87+ function buildPolicyDocument ( bucket ) {
11888 return {
11989 Version : '2012-10-17' ,
12090 Statement : [ {
12191 Sid : STATEMENT_ID ,
12292 Effect : 'Allow' ,
12393 Action : 's3:ReplicateObject' ,
124- Resource : buckets . map ( b => `arn:aws:s3:::${ b } /*` ) ,
94+ Resource : `arn:aws:s3:::${ bucket } /*` ,
12595 } ] ,
12696 } ;
12797}
@@ -190,13 +160,11 @@ async function main() {
190160 process . exit ( 1 ) ;
191161 }
192162
193- // Group by role, sorted by account so roles in the same account are
194- // processed consecutively — reduces the chance of cached credentials
195- // expiring while other accounts are being processed.
196- const roles = groupByRole ( entries )
197- . sort ( ( a , b ) => ( a . accountId < b . accountId ? - 1 : 1 ) ) ;
163+ // Sort by sourceRole so entries in the same account are processed
164+ // consecutively, maximising credential cache hits.
165+ entries . sort ( ( a , b ) => ( a . sourceRole < b . sourceRole ? - 1 : 1 ) ) ;
198166
199- log ( `Processing ${ roles . length } role (s)` ) ;
167+ log ( `Processing ${ entries . length } bucket (s)` ) ;
200168 log ( '' ) ;
201169
202170 // Read admin credentials
@@ -223,7 +191,7 @@ async function main() {
223191 inputFile : config . inputFile ,
224192 dryRun : config . dryRun ,
225193 counts : {
226- totalRolesProcessed : roles . length ,
194+ totalBucketsProcessed : entries . length ,
227195 totalBucketsFixed : 0 ,
228196 policiesCreated : 0 ,
229197 policiesAttached : 0 ,
@@ -236,33 +204,34 @@ async function main() {
236204 errors : [ ] ,
237205 } ;
238206
239- // Cache IAM client per account to reuse connections across roles
207+ // Cache IAM client per account to reuse connections across buckets
240208 // and for cleanup (key deletion).
241209 // Map<accountId, { accountName, accessKeyId, iamClient }>
242210 const accountCache = new Map ( ) ;
243211
244- for ( let i = 0 ; i < roles . length ; i ++ ) {
245- const { accountId, accountName, roleName, roleArn, buckets } = roles [ i ] ;
246- const policyName = `${ POLICY_PREFIX } -${ roleName } ` ;
247- const policyDocument = buildPolicyDocument ( buckets ) ;
212+ for ( let i = 0 ; i < entries . length ; i ++ ) {
213+ const entry = entries [ i ] ;
214+ const { accountId, roleName } = parseRoleArn ( entry . sourceRole ) ;
215+ const { bucket, ownerDisplayName : accountName } = entry ;
216+ const policyName = `${ POLICY_PREFIX } -${ bucket } ` ;
217+ const policyDocument = buildPolicyDocument ( bucket ) ;
248218
249- log ( `[${ i + 1 } /${ roles . length } ] Role "${ roleName } " — account "${ accountName } " ( ${ buckets . length } bucket(s)) ` ) ;
219+ log ( `[${ i + 1 } /${ entries . length } ] Bucket "${ bucket } " — role "${ roleName } " — account " ${ accountName } " ` ) ;
250220
251221 const fix = {
252222 accountId,
253223 accountName,
254224 roleName,
255- roleArn,
225+ roleArn : entry . sourceRole ,
256226 policyName,
257- buckets ,
227+ bucket ,
258228 status : 'pending' ,
259229 } ;
260230
261231 if ( config . dryRun ) {
262232 fix . status = 'dry-run' ;
263233 log ( ` [DRY-RUN] Would create policy "${ policyName } "` ) ;
264234 log ( ` [DRY-RUN] Would attach to role "${ roleName } "` ) ;
265- log ( ` Buckets: ${ buckets . join ( ', ' ) } ` ) ;
266235 outcome . fixes . push ( fix ) ;
267236 continue ;
268237 }
@@ -284,9 +253,10 @@ async function main() {
284253
285254 const { iamClient } = accountCache . get ( accountId ) ;
286255
287- // Idempotent/safe to re-run: CreatePolicy reuses an existing policy
288- // with the same name, and AttachRolePolicy is a no-op if
289- // the policy is already attached to the role.
256+ // Idempotent/safe to re-run: each bucket has its own policy,
257+ // so EntityAlreadyExists means the exact same policy document
258+ // already exists — a true no-op. AttachRolePolicy is also
259+ // a no-op if the policy is already attached to the role.
290260 let policyArn ;
291261 try {
292262 const resp = await iamClient . send ( new CreatePolicyCommand ( {
@@ -300,7 +270,7 @@ async function main() {
300270 if ( err . name === 'EntityAlreadyExistsException'
301271 || err . Code === 'EntityAlreadyExists' ) {
302272 policyArn = `arn:aws:iam::${ accountId } :policy/${ policyName } ` ;
303- log ( ` Policy "${ policyName } " already exists, reusing ` ) ;
273+ log ( ` Policy "${ policyName } " already exists, skipping ` ) ;
304274 } else {
305275 throw err ;
306276 }
@@ -316,7 +286,7 @@ async function main() {
316286 log ( ` Attached policy to role "${ roleName } "` ) ;
317287
318288 fix . status = 'success' ;
319- outcome . metadata . counts . totalBucketsFixed += buckets . length ;
289+ outcome . metadata . counts . totalBucketsFixed ++ ;
320290 } catch ( err ) {
321291 fix . status = 'error' ;
322292 fix . error = err . message ;
@@ -325,6 +295,7 @@ async function main() {
325295 accountId,
326296 accountName,
327297 roleName,
298+ bucket,
328299 policyName,
329300 message : err . message ,
330301 } ) ;
@@ -356,7 +327,7 @@ async function main() {
356327
357328 // Print summary
358329 log ( '\n=== Summary ===' ) ;
359- log ( `Roles processed: ${ outcome . metadata . counts . totalRolesProcessed } ` ) ;
330+ log ( `Buckets processed: ${ outcome . metadata . counts . totalBucketsProcessed } ` ) ;
360331 log ( `Buckets fixed: ${ outcome . metadata . counts . totalBucketsFixed } ` ) ;
361332 log ( `Policies created: ${ outcome . metadata . counts . policiesCreated } ` ) ;
362333 log ( `Policies attached: ${ outcome . metadata . counts . policiesAttached } ` ) ;
0 commit comments