Skip to content

Commit dd54a5b

Browse files
cboldisclaude
andcommitted
fix(cdn-log-delivery): address remaining ABHA61 review findings
- Strict 12-digit accountId validation (reject non-digit chars outright) - toSafeAwsName guard: throw on IMS org IDs that produce invalid AWS names - Remove dead ResourceAlreadyExistsException from CreateDelivery catch - Add 50-page guard to findExistingDelivery paginator - Audit log on ResourceNotFoundException path in enableCdnLogDelivery - CDN_LOG_RESCAN_MAX_DISTRIBUTIONS cap in rescanCdnLogDelivery (truncated+totalFound) - Assert AWS command inputs (put/create) in cdn-log-delivery tests - Partial-state recovery test (source exists, no delivery -> created:true) - empty-distributions rescan test + max-distributions cap test Introduced by: #2680 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1068f4e commit dd54a5b

4 files changed

Lines changed: 96 additions & 7 deletions

File tree

src/controllers/llmo/llmo-cloudfront.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,13 @@ function LlmoCloudFrontController(ctx) {
201201
// `{ accountId, distributionId, error }` where `error` is a badRequest Response when validation
202202
// fails (undefined otherwise).
203203
const validateCloudfrontCredentials = (context, { requireDistribution = false } = {}) => {
204-
const accountId = String(context.data?.accountId || '').replace(/\D/g, '');
204+
const rawAccountId = String(context.data?.accountId || '');
205205
const distributionId = String(context.data?.distributionId || '').trim();
206206

207-
if (accountId.length !== 12) {
207+
if (!/^\d{12}$/.test(rawAccountId)) {
208208
return { error: badRequest('accountId must be a 12-digit AWS account ID') };
209209
}
210+
const accountId = rawAccountId;
210211
if (requireDistribution && !hasText(distributionId)) {
211212
return { error: badRequest('distributionId is required') };
212213
}
@@ -1228,6 +1229,7 @@ function LlmoCloudFrontController(ctx) {
12281229
} catch (deliveryError) {
12291230
// Adobe destination not provisioned yet → clear 4xx instead of a raw AWS error + retry.
12301231
if (deliveryError?.name === 'ResourceNotFoundException') {
1232+
log.warn(`[cdn-onboard-cloudfront] Adobe log destination not provisioned for site ${siteId} (imsOrgId=${imsOrgId})`);
12311233
return badRequest('Adobe log destination is not provisioned for this organization yet — run cdn-logs provisioning first');
12321234
}
12331235
throw deliveryError;
@@ -1283,7 +1285,10 @@ function LlmoCloudFrontController(ctx) {
12831285
accountId, externalId, roleName,
12841286
});
12851287

1286-
const distributions = await cloudFrontClient.listDistributions();
1288+
const allDistributions = await cloudFrontClient.listDistributions();
1289+
const maxDists = Number(env?.CDN_LOG_RESCAN_MAX_DISTRIBUTIONS) || 0;
1290+
const truncated = maxDists > 0 && allDistributions.length > maxDists;
1291+
const distributions = truncated ? allDistributions.slice(0, maxDists) : allDistributions;
12871292

12881293
// Run in bounded batches (CDN_LOG_RESCAN_CONCURRENCY) so a large account doesn't trip
12891294
// CloudWatch Logs per-account throttling. slice()/push() preserve order, so the per-index
@@ -1332,6 +1337,7 @@ function LlmoCloudFrontController(ctx) {
13321337
created: createdCount,
13331338
alreadyExisted,
13341339
failed,
1340+
...(truncated && { truncated: true, totalFound: allDistributions.length }),
13351341
distributions: summary,
13361342
});
13371343
} catch (error) {

src/support/cdn-log-delivery.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ export function buildDeliverySourceName({
8888
resourceId,
8989
}) {
9090
const { sourceNamePrefix } = getProviderConfig(provider);
91-
const name = `${sourceNamePrefix}-${toSafeAwsName(imsOrgId)}-${resourceId}`;
91+
const safeOrg = toSafeAwsName(imsOrgId);
92+
if (!safeOrg || !/^[a-z0-9]/.test(safeOrg)) {
93+
throw new Error(`IMS org ID '${imsOrgId}' produces an invalid AWS resource name segment`);
94+
}
95+
const name = `${sourceNamePrefix}-${safeOrg}-${resourceId}`;
9296
if (name.length <= MAX_DELIVERY_SOURCE_NAME_LENGTH) {
9397
return name;
9498
}
@@ -147,9 +151,12 @@ export async function createCdnLogDelivery(credentials, {
147151

148152
// Find the existing delivery for this source pointing at the expected destination (paginated).
149153
// The DescribeDeliveries API does not accept a server-side filter; pagination is client-side.
154+
const MAX_DESCRIBE_PAGES = 50;
150155
const findExistingDelivery = async () => {
151156
let nextToken;
157+
let pages = 0;
152158
do {
159+
pages += 1;
153160
// eslint-disable-next-line no-await-in-loop
154161
const page = await client.send(new DescribeDeliveriesCommand({
155162
...(nextToken && { nextToken }),
@@ -164,7 +171,7 @@ export async function createCdnLogDelivery(credentials, {
164171
return match;
165172
}
166173
nextToken = page.nextToken;
167-
} while (nextToken);
174+
} while (nextToken && pages < MAX_DESCRIBE_PAGES);
168175
return undefined;
169176
};
170177

@@ -220,7 +227,7 @@ export async function createCdnLogDelivery(credentials, {
220227
// TOCTOU: a concurrent enable/rescan for the same distribution can both pass the existence
221228
// check above and race here; the losing CreateDelivery conflicts. Treat as already-enabled to
222229
// preserve the idempotency contract instead of surfacing a 500.
223-
if (err?.name === 'ConflictException' || err?.name === 'ResourceAlreadyExistsException') {
230+
if (err?.name === 'ConflictException') {
224231
const existing = await findExistingDelivery();
225232
// deliveryId may be undefined here if DescribeDeliveries hasn't caught up to the winning
226233
// CreateDelivery yet (eventual consistency). That is acceptable: delivery IS enabled — the

test/controllers/llmo/llmo-cloudfront.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2295,6 +2295,11 @@ describe('LlmoCloudFrontController', () => {
22952295
const callArgs = createCdnLogDeliveryStub.firstCall.args[1];
22962296
expect(callArgs.provider).to.equal('cloudfront');
22972297
expect(callArgs.resourceId).to.equal('E2EXAMPLE123');
2298+
expect(callArgs.accountId).to.equal('120569600543');
2299+
expect(callArgs.imsOrgId).to.equal('ABC123@AdobeOrg');
2300+
expect(callArgs.deliveryDestinationArn).to.equal(
2301+
'arn:aws:logs:us-east-1:111122223333:delivery-destination:cdn-logs-org',
2302+
);
22982303
});
22992304

23002305
it('is a no-op when forwarding is already enabled', async () => {
@@ -2584,5 +2589,36 @@ describe('LlmoCloudFrontController', () => {
25842589
expect(result.status).to.equal(500);
25852590
expect((await result.json()).message).to.equal('An unexpected error occurred');
25862591
});
2592+
2593+
it('returns an empty distribution list when the account has no distributions', async () => {
2594+
listDistributionsStub.resolves([]);
2595+
2596+
const result = await controller.rescanCdnLogDelivery(rescanContext);
2597+
2598+
expect(result.status).to.equal(200);
2599+
const body = await result.json();
2600+
expect(body.scanned).to.equal(0);
2601+
expect(body.distributions).to.deep.equal([]);
2602+
expect(createCdnLogDeliveryStub.called).to.equal(false);
2603+
});
2604+
2605+
it('caps distributions at CDN_LOG_RESCAN_MAX_DISTRIBUTIONS and sets truncated flag', async () => {
2606+
listDistributionsStub.resolves([
2607+
{ id: 'E2DIST000001' }, { id: 'E2DIST000002' }, { id: 'E2DIST000003' },
2608+
]);
2609+
createCdnLogDeliveryStub.resolves({ created: true, alreadyExisted: false });
2610+
2611+
const result = await controller.rescanCdnLogDelivery({
2612+
...rescanContext,
2613+
env: { ...rescanContext.env, CDN_LOG_RESCAN_MAX_DISTRIBUTIONS: '2' },
2614+
});
2615+
2616+
expect(result.status).to.equal(200);
2617+
const body = await result.json();
2618+
expect(body.scanned).to.equal(2);
2619+
expect(body.truncated).to.equal(true);
2620+
expect(body.totalFound).to.equal(3);
2621+
expect(createCdnLogDeliveryStub.callCount).to.equal(2);
2622+
});
25872623
});
25882624
});

test/support/cdn-log-delivery.test.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,18 @@ describe('cdn-log-delivery support', () => {
113113
const rnf = () => Object.assign(new Error('not found'), { name: 'ResourceNotFoundException' });
114114

115115
it('creates source + delivery when none exists', async () => {
116+
let putCmd;
117+
let createCmd;
116118
sendStub.callsFake((cmd) => {
117119
if (cmd.type === 'get') {
118120
return Promise.reject(rnf());
119121
}
122+
if (cmd.type === 'put') {
123+
putCmd = cmd;
124+
return Promise.resolve({});
125+
}
120126
if (cmd.type === 'create') {
127+
createCmd = cmd;
121128
return Promise.resolve({ delivery: { id: 'del-new' } });
122129
}
123130
return Promise.resolve({});
@@ -129,6 +136,17 @@ describe('cdn-log-delivery support', () => {
129136
expect(result.alreadyExisted).to.equal(false);
130137
expect(result.deliveryId).to.equal('del-new');
131138
expect(result.deliverySourceName).to.equal('llmo-cf-abc123-E2EXAMPLE123');
139+
140+
// Assert the exact inputs sent to each AWS command.
141+
expect(putCmd.input.name).to.equal('llmo-cf-abc123-E2EXAMPLE123');
142+
expect(putCmd.input.resourceArn).to.equal(
143+
`arn:aws:cloudfront::${baseParams.accountId}:distribution/${baseParams.resourceId}`,
144+
);
145+
expect(putCmd.input.logType).to.equal('ACCESS_LOGS');
146+
expect(createCmd.input.deliverySourceName).to.equal('llmo-cf-abc123-E2EXAMPLE123');
147+
expect(createCmd.input.deliveryDestinationArn).to.equal(baseParams.deliveryDestinationArn);
148+
expect(createCmd.input.s3DeliveryConfiguration).to.deep.equal({ suffixPath: '/{yyyy}/{MM}/{dd}/{HH}' });
149+
expect(createCmd.input.recordFields).to.include('cs-uri-stem');
132150
});
133151

134152
it('is idempotent when a delivery already exists (paginated)', async () => {
@@ -189,13 +207,35 @@ describe('cdn-log-delivery support', () => {
189207
expect(describeCalls).to.equal(1); // looked up the winner's delivery id after the conflict
190208
});
191209

210+
it('recovers when the source already exists but no delivery is found (partial state)', async () => {
211+
sendStub.callsFake((cmd) => {
212+
if (cmd.type === 'get') {
213+
return Promise.resolve({}); // source already exists → skip PutDeliverySource
214+
}
215+
if (cmd.type === 'describe') {
216+
// No existing delivery found for this source+destination pair.
217+
return Promise.resolve({ deliveries: [] });
218+
}
219+
if (cmd.type === 'create') {
220+
return Promise.resolve({ delivery: { id: 'del-recovered' } });
221+
}
222+
return Promise.resolve({});
223+
});
224+
225+
const result = await mod.createCdnLogDelivery(creds, baseParams);
226+
227+
expect(result.created).to.equal(true);
228+
expect(result.alreadyExisted).to.equal(false);
229+
expect(result.deliveryId).to.equal('del-recovered');
230+
});
231+
192232
it('on a conflict, returns alreadyExisted with undefined id when the raced delivery is not found', async () => {
193233
sendStub.callsFake((cmd) => {
194234
if (cmd.type === 'get') {
195235
return Promise.reject(rnf()); // source absent → reach PutDeliverySource + CreateDelivery
196236
}
197237
if (cmd.type === 'create') {
198-
return Promise.reject(Object.assign(new Error('exists'), { name: 'ResourceAlreadyExistsException' }));
238+
return Promise.reject(Object.assign(new Error('exists'), { name: 'ConflictException' }));
199239
}
200240
if (cmd.type === 'describe') {
201241
// Paginate through pages with no matching deliverySourceName, then exhaust nextToken

0 commit comments

Comments
 (0)