fix(api): count Cognito pool users directly
The runtime leaves EstimatedNumberOfUsers at 0, so the pool metadata now carries a userCount from ListUsers (capped at 1000), which the delete confirmation reads.
This commit is contained in:
@@ -121,12 +121,25 @@ describe('AwsUserPoolsAdapter', () => {
|
||||
signInWith: 'email',
|
||||
passwordPolicy: '8+ characters with uppercase, lowercase, number, symbol',
|
||||
estimatedNumberOfUsers: 1,
|
||||
userCount: 0,
|
||||
mfaConfiguration: 'OFF',
|
||||
deletionProtection: 'INACTIVE',
|
||||
appClients: [{clientId: 'a8e5c7d38d3b4a1a97c9d822de', clientName: 'orders-users-client'}],
|
||||
})
|
||||
})
|
||||
|
||||
test('counts users itself, since the runtime leaves EstimatedNumberOfUsers at 0', async () => {
|
||||
const {client} = stubCognito((command) => {
|
||||
if (command instanceof DescribeUserPoolCommand) return {UserPool: {...pool, EstimatedNumberOfUsers: 0}}
|
||||
if (command instanceof ListUsersCommand) return command.input.PaginationToken ? {Users: [alice]} : {Users: [alice, alice], PaginationToken: 'page-2'}
|
||||
return {}
|
||||
})
|
||||
const resource = await new AwsUserPoolsAdapter(client).get(POOL_ID)
|
||||
|
||||
expect(resource?.metadata.userCount).toBe(3)
|
||||
expect(resource?.metadata.estimatedNumberOfUsers).toBe(0)
|
||||
})
|
||||
|
||||
test('summarises a relaxed password policy without character rules', async () => {
|
||||
const {client} = stubCognito((command) => {
|
||||
if (command instanceof DescribeUserPoolCommand) return {UserPool: {...pool, Policies: {PasswordPolicy: {MinimumLength: 8, RequireUppercase: false, RequireLowercase: false, RequireNumbers: false, RequireSymbols: false}}}}
|
||||
|
||||
@@ -57,6 +57,8 @@ import type {
|
||||
|
||||
/** Cognito caps every list page at 60. */
|
||||
const COGNITO_PAGE_LIMIT = 60
|
||||
/** Counting stops here so a very large pool cannot stall the pool list. */
|
||||
const USER_COUNT_CAP = 1000
|
||||
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
/**
|
||||
@@ -72,6 +74,8 @@ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
* symbol, matching AWS.
|
||||
* - `InitiateAuth` with `USER_PASSWORD_AUTH` issues real tokens once a user's
|
||||
* password is permanent, so users created here can sign in immediately.
|
||||
* - `EstimatedNumberOfUsers` stays 0 however many users exist (AWS also only
|
||||
* updates it periodically), so `userCount` is counted with `ListUsers`.
|
||||
* - An admin action the user's state does not allow (confirming a confirmed
|
||||
* user) is `NotAuthorizedException`, surfaced here as a conflict because the
|
||||
* console caller is authorised and a 401 would end the console session.
|
||||
@@ -110,11 +114,12 @@ export class AwsUserPoolsAdapter implements CloudServiceAdapter {
|
||||
|
||||
async get(id: string): Promise<CloudResource | null> {
|
||||
try {
|
||||
const [pool, clients] = await Promise.all([
|
||||
const [pool, clients, userCount] = await Promise.all([
|
||||
this.cognito.send(new DescribeUserPoolCommand({UserPoolId: id})),
|
||||
this.listClients(id),
|
||||
this.countUsers(id),
|
||||
])
|
||||
return pool.UserPool ? toResource(pool.UserPool, clients) : null
|
||||
return pool.UserPool ? toResource(pool.UserPool, clients, userCount) : null
|
||||
} catch (error) {
|
||||
if (isMissing(error)) return null
|
||||
throw error
|
||||
@@ -284,6 +289,17 @@ export class AwsUserPoolsAdapter implements CloudServiceAdapter {
|
||||
return listGroupNamesForUser(this.cognito, poolId, username)
|
||||
}
|
||||
|
||||
private async countUsers(poolId: string): Promise<number> {
|
||||
let count = 0
|
||||
let token: string | undefined
|
||||
do {
|
||||
const res = await this.cognito.send(new ListUsersCommand({UserPoolId: poolId, Limit: COGNITO_PAGE_LIMIT, ...(token ? {PaginationToken: token} : {})}))
|
||||
count += res.Users?.length ?? 0
|
||||
token = res.PaginationToken || undefined
|
||||
} while (token && count < USER_COUNT_CAP)
|
||||
return Math.min(count, USER_COUNT_CAP)
|
||||
}
|
||||
|
||||
private async listClients(poolId: string): Promise<UserPoolClientDescription[]> {
|
||||
const clients: UserPoolClientDescription[] = []
|
||||
let nextToken: string | undefined
|
||||
@@ -423,7 +439,7 @@ function toSummaryResource(pool: UserPoolDescriptionType): CloudResource[] {
|
||||
}]
|
||||
}
|
||||
|
||||
function toResource(pool: UserPoolType, clients: UserPoolClientDescription[]): CloudResource {
|
||||
function toResource(pool: UserPoolType, clients: UserPoolClientDescription[], userCount: number): CloudResource {
|
||||
const id = pool.Id ?? ''
|
||||
const policy = pool.Policies?.PasswordPolicy
|
||||
return {
|
||||
@@ -440,6 +456,7 @@ function toResource(pool: UserPoolType, clients: UserPoolClientDescription[]): C
|
||||
signInWith: pool.UsernameAttributes?.includes('email') ? 'email' : 'username',
|
||||
passwordPolicy: policy ? describePasswordPolicy(policy) : null,
|
||||
estimatedNumberOfUsers: pool.EstimatedNumberOfUsers ?? null,
|
||||
userCount,
|
||||
mfaConfiguration: pool.MfaConfiguration ?? null,
|
||||
deletionProtection: pool.DeletionProtection ?? null,
|
||||
appClients: clients.map((client) => ({clientId: client.ClientId ?? '', clientName: client.ClientName ?? ''})),
|
||||
|
||||
@@ -86,7 +86,7 @@ export function awsUserPoolsSchema(): ServiceSchema {
|
||||
filters: userPoolFilters,
|
||||
columns: userPoolColumns,
|
||||
deleteImpact: [
|
||||
{path: 'metadata.estimatedNumberOfUsers', singular: 'user', plural: 'users'},
|
||||
{path: 'metadata.userCount', singular: 'user', plural: 'users'},
|
||||
{path: 'metadata.appClients', singular: 'app client', plural: 'app clients'},
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user