fix(cli): generate admin queries auth wiring - #14909
Conversation
| [factory.createIdentifier(apiVarName)], | ||
| ); | ||
|
|
||
| return factory.createTemplateExpression(factory.createTemplateHead('arn:aws:cognito-idp:'), [ |
There was a problem hiding this comment.
The generated Gen1 user-pool ARN hardcodes the aws partition. The sibling Gen2 pool ARN uses backend.auth.resources.userPool.userPoolArn (a partition-correct token), so only the Gen1 ARN is partition-blind.
return factory.createTemplateExpression(factory.createTemplateHead('arn:aws:cognito-idp:'), [Why it matters: cause: emitted ARN begins arn:aws:cognito-idp: regardless of partition -> mechanism: in aws-us-gov / aws-cn the real user-pool ARN uses arn:aws-us-gov: / arn:aws-cn:, so the policy resource never matches -> consequence: the AdminQueries Lambda's IAM statement grants nothing for the Gen1 pool, so admin operations targeting the Gen1 pool fail with AccessDenied after migration in those partitions.
Suggestion: We can emit the ARN via the stack's partition-aware formatter instead of a literal prefix, e.g. Stack.of(<api>).formatArn({ service: 'cognito-idp', resource: 'userpool', resourceName: <gen1UserPoolId> }), or interpolate Stack.of(<api>).partition.
There was a problem hiding this comment.
Fixed in b78acb6. The Gen1 pool ARN now uses Stack.of(AdminQueriesApi).formatArn(...), allowing CDK to supply the correct partition, region, and account. The complete output snapshot verifies this.
| @@ -1073,4 +1073,52 @@ describe('RestApiGenerator', () => { | |||
| const output = writtenFile('resource.ts'); | |||
| expect(output).toContain('myapiApi'); | |||
There was a problem hiding this comment.
The single new test covers only the happy path: an AdminQueries-named API, gen1UserPoolId present, one /{proxy+} path. Two branches this change introduces are untested.
it('renders Cognito authorizer and admin permissions for AdminQueries API', async () => {Why it matters: cause: no test exercises (a) the gen1UserPoolId absent branch, where the IAM resources array should contain only the Gen2 pool ARN, or (b) the generator's startsWith('AdminQueries') branch for a non-AdminQueries-named API -> mechanism: a future edit that breaks either branch (e.g. emits a malformed ARN when the Gen1 pool id is missing, or mis-detects admin APIs) compiles and passes CI -> consequence: the regression ships to migrating users undetected.
Suggestion: Adding two cases: an AdminQueries API with no Gen1 user pool (assert the IAM statement has exactly the Gen2 userPoolArn and no arn:...:userpool/ literal), and a non-AdminQueries-named API whose backing function starts with AdminQueries (assert admin wiring is applied).
There was a problem hiding this comment.
Added both edge cases. Missing UserPoolId now warns and emits only the Gen2 pool ARN. The prefix case is intentionally negative because the later least-privilege finding shows that enabling admin wiring for unrelated AdminQueries* Lambdas is unsafe.
| if (apiName === 'AdminQueries') { | ||
| return Array.from(functions); | ||
| } | ||
| return Array.from(functions).filter((name) => name.startsWith('AdminQueries')); |
There was a problem hiding this comment.
For any API not named AdminQueries, a backing function whose name merely starts with AdminQueries flips the whole API into admin mode (isAdminQueriesApi true). Detection is name-prefix based with no other signal.
return Array.from(functions).filter((name) => name.startsWith('AdminQueries'));Why it matters: cause: a user function named e.g. AdminQueriesReports on an unrelated REST API matches the prefix -> mechanism: renderPaths applies the Cognito aws.cognito.signin.user.admin-scope method options to ALL of that API's paths and renderAdminQueriesLambdaPolicies grants that Lambda 11 cognito-idp:Admin*/List* actions -> consequence: (1) the API silently starts requiring the admin scope so existing callers without it get 401, and (2) a Lambda that never needed Cognito admin powers is over-privileged (least-privilege violation). Input is the developer's own config, not attacker-controlled, so this is a footgun, not an external exploit.
Suggestion: Tighten detection beyond a bare name prefix (e.g. match the exact AdminQueries API name, or require the migration marker that identifies the CLI-owned AdminQueries feature) so unrelated user resources cannot inherit admin auth + admin IAM.
There was a problem hiding this comment.
Fixed by requiring the exact AdminQueries API, canonical Cognito and Lambda dependencies from amplify-meta.json, and a matching Lambda referenced by an API path.
| [ | ||
| factory.createPropertyAssignment( | ||
| 'cognitoUserPools', | ||
| factory.createArrayLiteralExpression([TS.propAccess('backend', 'auth', 'resources', 'userPool')]), |
There was a problem hiding this comment.
The admin wiring emits backend.auth.resources.userPool (authorizer, line 303) and backend.auth.resources.userPool.userPoolArn (Lambda policy, line 578) unconditionally whenever isAdminQueriesApi is true. It never consults the hasAuth flag the renderer is constructed with, even though the sibling Gen1-policy attachment at line 97 (if (this.hasPathAuth(restApi) && this.hasAuth)) already gates auth-dependent emission on hasAuth.
factory.createArrayLiteralExpression([TS.propAccess('backend', 'auth', 'resources', 'userPool')]),Why it matters: cause: isAdminQueriesApi is driven purely by the startsWith('AdminQueries') function-name match, independent of whether the Gen1 app has any Cognito auth -> mechanism: an app with NO Cognito (e.g. an API-key/IAM REST API whose backing Lambda is merely named AdminQueries*) has no auth resource migrated into defineBackend, so the generated resource.ts references a backend.auth that does not exist -> consequence: the generated backend fails to type-check (Property 'auth' does not exist on type 'Backend') or throws at synth (Cannot read properties of undefined (reading 'resources')), breaking the ENTIRE migrated backend.ts, not just this one API. Low probability (needs the name coincidence on a no-Cognito app) but high blast radius.
Suggestion: Guarding the admin wiring on this.hasAuth (only treat an API as AdminQueries when the app actually has Cognito auth) and/or fold a hasAuth check into isAdminQueriesApi, mirroring the existing this.hasAuth gate at line 97.
There was a problem hiding this comment.
Fixed. AdminQueries generation requires the referenced Cognito resource, and every renderer path—including Lambda policy emission—is gated by hasAuth. The no-auth regression verifies there is no authorizer, backend.auth, or Cognito admin policy.
Require canonical AdminQueries dependencies before generating auth. Generate IAM policies only for verified metadata. Build Gen1 user pool ARNs with CDK's partition-aware formatter. Warn when the referenced pool is unavailable. Cover prefix collisions, absent auth, missing pool IDs, and full output snapshots. Validate with the cli-internal build and full unit suite. --- Prompt: do it. make sure you verify and test if its all working by reproducing the error and the patch before pushing and then comment and reply whatever is necessary you have my approval
Description of changes
Fixes #14757.
When Gen1 AdminQueries REST APIs are generated for Gen2 migration, the generated REST API resource needs to preserve the Cognito behavior that AdminQueries depends on. This updates the REST API generator to detect AdminQueries APIs and emit the missing pieces:
OPTIONSAuthorizationheaderaws.cognito.signin.user.adminscopeANYmethods on the root/proxy resources using the Cognito method optionsRegular REST API generation is left unchanged unless the API is
AdminQueriesor uses anAdminQueries*backing function.Description of how you validated changes
PATH="$PWD/node_modules/.bin:$PATH" corepack yarn workspace @aws-amplify/cli-internal test src/__tests__/commands/gen2-migration/generate/amplify/rest-api/rest-api.generator.test.ts --runInBand --no-coveragePATH="$PWD/node_modules/.bin:$PATH" corepack yarn prettier --check packages/amplify-cli/src/commands/gen2-migration/generate/amplify/rest-api/rest-api.generator.ts packages/amplify-cli/src/commands/gen2-migration/generate/amplify/rest-api/rest-api.renderer.ts packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/amplify/rest-api/rest-api.generator.test.tsPATH="$PWD/node_modules/.bin:$PATH" corepack yarn eslint packages/amplify-cli/src/commands/gen2-migration/generate/amplify/rest-api/rest-api.generator.ts packages/amplify-cli/src/commands/gen2-migration/generate/amplify/rest-api/rest-api.renderer.ts packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/amplify/rest-api/rest-api.generator.test.ts --max-warnings=1500git diff --checkPATH="$PWD/node_modules/.bin:$PATH" corepack yarn sanitize-migration-appsPATH="$PWD/node_modules/.bin:$PATH" corepack yarn build-tests-changedNote: the first
build-tests-changedrun hit an NX daemon restart message; rerunning the same command succeeded. Local git hooks call a plainyarnbinary directly, while this environment exposes Yarn through Corepack, so commit/push hooks were bypassed after running the hook commands above manually.Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.