Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51a0a60a73 |
@@ -16,7 +16,9 @@ import {AwsLightsailAdapter} from './AwsLightsailAdapter'
|
|||||||
const originalEndpoint = process.env.FLOCI_ENDPOINT
|
const originalEndpoint = process.env.FLOCI_ENDPOINT
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.env.FLOCI_ENDPOINT = originalEndpoint
|
// Assigning undefined would store the string "undefined" for every later test file.
|
||||||
|
if (originalEndpoint === undefined) delete process.env.FLOCI_ENDPOINT
|
||||||
|
else process.env.FLOCI_ENDPOINT = originalEndpoint
|
||||||
})
|
})
|
||||||
|
|
||||||
function stubLightsail() {
|
function stubLightsail() {
|
||||||
|
|||||||
@@ -125,31 +125,9 @@ describe('AwsUserPoolsAdapter', () => {
|
|||||||
mfaConfiguration: 'OFF',
|
mfaConfiguration: 'OFF',
|
||||||
deletionProtection: 'INACTIVE',
|
deletionProtection: 'INACTIVE',
|
||||||
appClients: [{clientId: 'a8e5c7d38d3b4a1a97c9d822de', clientName: 'orders-users-client'}],
|
appClients: [{clientId: 'a8e5c7d38d3b4a1a97c9d822de', clientName: 'orders-users-client'}],
|
||||||
lambdaTriggers: [],
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('lists the pool\'s Lambda triggers in sign-in order, with the trigger version when set', async () => {
|
|
||||||
const arn = (name: string) => `arn:aws:lambda:us-east-1:000000000000:function:${name}`
|
|
||||||
const {client} = stubCognito((command) => {
|
|
||||||
if (command instanceof DescribeUserPoolCommand) return {UserPool: {...pool, LambdaConfig: {
|
|
||||||
PreTokenGeneration: arn('claims'),
|
|
||||||
PreTokenGenerationConfig: {LambdaArn: arn('claims'), LambdaVersion: 'V2_0'},
|
|
||||||
PreSignUp: `${arn('signup')}:live`,
|
|
||||||
CustomEmailSender: {LambdaArn: arn('mailer'), LambdaVersion: 'V1_0'},
|
|
||||||
KMSKeyID: 'arn:aws:kms:us-east-1:000000000000:key/1',
|
|
||||||
}}}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
|
|
||||||
const resource = await new AwsUserPoolsAdapter(client).get(POOL_ID)
|
|
||||||
expect(resource?.metadata.lambdaTriggers).toEqual([
|
|
||||||
{trigger: 'PreSignUp', label: 'Pre sign-up', functionArn: `${arn('signup')}:live`, functionName: 'signup', version: null},
|
|
||||||
{trigger: 'PreTokenGeneration', label: 'Pre token generation', functionArn: arn('claims'), functionName: 'claims', version: 'V2_0'},
|
|
||||||
{trigger: 'CustomEmailSender', label: 'Custom email sender', functionArn: arn('mailer'), functionName: 'mailer', version: 'V1_0'},
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('counts users itself, since the runtime leaves EstimatedNumberOfUsers at 0', async () => {
|
test('counts users itself, since the runtime leaves EstimatedNumberOfUsers at 0', async () => {
|
||||||
const {client} = stubCognito((command) => {
|
const {client} = stubCognito((command) => {
|
||||||
if (command instanceof DescribeUserPoolCommand) return {UserPool: {...pool, EstimatedNumberOfUsers: 0}}
|
if (command instanceof DescribeUserPoolCommand) return {UserPool: {...pool, EstimatedNumberOfUsers: 0}}
|
||||||
|
|||||||
@@ -460,43 +460,10 @@ function toResource(pool: UserPoolType, clients: UserPoolClientDescription[], us
|
|||||||
mfaConfiguration: pool.MfaConfiguration ?? null,
|
mfaConfiguration: pool.MfaConfiguration ?? null,
|
||||||
deletionProtection: pool.DeletionProtection ?? null,
|
deletionProtection: pool.DeletionProtection ?? null,
|
||||||
appClients: clients.map((client) => ({clientId: client.ClientId ?? '', clientName: client.ClientName ?? ''})),
|
appClients: clients.map((client) => ({clientId: client.ClientId ?? '', clientName: client.ClientName ?? ''})),
|
||||||
lambdaTriggers: lambdaTriggersOf(pool.LambdaConfig),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels as the AWS console names them, in the order a sign-up and sign-in reach them.
|
|
||||||
const LAMBDA_TRIGGERS = [
|
|
||||||
['PreSignUp', 'Pre sign-up'],
|
|
||||||
['UserMigration', 'User migration'],
|
|
||||||
['CustomMessage', 'Custom message'],
|
|
||||||
['PostConfirmation', 'Post confirmation'],
|
|
||||||
['PreAuthentication', 'Pre authentication'],
|
|
||||||
['DefineAuthChallenge', 'Define auth challenge'],
|
|
||||||
['CreateAuthChallenge', 'Create auth challenge'],
|
|
||||||
['VerifyAuthChallengeResponse', 'Verify auth challenge response'],
|
|
||||||
['PostAuthentication', 'Post authentication'],
|
|
||||||
['PreTokenGeneration', 'Pre token generation'],
|
|
||||||
['CustomEmailSender', 'Custom email sender'],
|
|
||||||
['CustomSMSSender', 'Custom SMS sender'],
|
|
||||||
] as const
|
|
||||||
|
|
||||||
type LambdaTrigger = {trigger: string; label: string; functionArn: string; functionName: string; version: string | null}
|
|
||||||
|
|
||||||
function lambdaTriggersOf(config: UserPoolType['LambdaConfig']): LambdaTrigger[] {
|
|
||||||
if (!config) return []
|
|
||||||
return LAMBDA_TRIGGERS.flatMap(([trigger, label]) => {
|
|
||||||
// PreTokenGenerationConfig carries the trigger version and supersedes the older PreTokenGeneration field.
|
|
||||||
const versioned = trigger === 'PreTokenGeneration' ? config.PreTokenGenerationConfig
|
|
||||||
: trigger === 'CustomEmailSender' ? config.CustomEmailSender
|
|
||||||
: trigger === 'CustomSMSSender' ? config.CustomSMSSender
|
|
||||||
: undefined
|
|
||||||
const functionArn = versioned?.LambdaArn ?? config[trigger]
|
|
||||||
if (typeof functionArn !== 'string' || !functionArn) return []
|
|
||||||
return [{trigger, label, functionArn, functionName: functionArn.split(':')[6] ?? functionArn, version: versioned?.LambdaVersion ?? null}]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function toUserItem(
|
function toUserItem(
|
||||||
username: string | undefined,
|
username: string | undefined,
|
||||||
attributes: AttributeType[] | undefined,
|
attributes: AttributeType[] | undefined,
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ const realFetch = globalThis.fetch
|
|||||||
let passwordHash: string
|
let passwordHash: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
passwordHash = await Bun.password.hash('correct-horse-battery')
|
// Cheap parameters keep the many sign-in attempts below fast on slow CI runners; verification
|
||||||
|
// reads the parameters from the hash, so the code under test is unchanged.
|
||||||
|
passwordHash = await Bun.password.hash('correct-horse-battery', {algorithm: 'argon2id', memoryCost: 1024, timeCost: 1})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -239,7 +241,8 @@ describe('auth state cleanup', () => {
|
|||||||
test('caps pending OIDC requests and sweeps expired state', async () => {
|
test('caps pending OIDC requests and sweeps expired state', async () => {
|
||||||
const config = authConfig(oidcEnv({FLOCI_UI_OIDC_ALLOW_ALL_USERS: 'true'}))
|
const config = authConfig(oidcEnv({FLOCI_UI_OIDC_ALLOW_ALL_USERS: 'true'}))
|
||||||
const hono = app(config)
|
const hono = app(config)
|
||||||
globalThis.fetch = (async () => Response.json({authorization_endpoint: 'https://idp.test/authorize', token_endpoint: 'https://idp.test/token', jwks_uri: 'https://idp.test/jwks'})) as unknown as typeof fetch
|
// The same document as mockIdp: discovery is cached per issuer for the rest of the run.
|
||||||
|
globalThis.fetch = (async () => Response.json({authorization_endpoint: 'https://idp.test/authorize', token_endpoint: 'https://idp.test/token', jwks_uri: 'https://idp.test/jwks', userinfo_endpoint: 'https://idp.test/userinfo'})) as unknown as typeof fetch
|
||||||
for (let index = 0; index < 1_050; index++) await request(hono, '/api/auth/oidc/login', `10.1.${Math.floor(index / 250)}.${index % 250}`)
|
for (let index = 0; index < 1_050; index++) await request(hono, '/api/auth/oidc/login', `10.1.${Math.floor(index / 250)}.${index % 250}`)
|
||||||
|
|
||||||
expect(authStateSizes().oidcRequests).toBe(1_000)
|
expect(authStateSizes().oidcRequests).toBe(1_000)
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import {beforeEach, describe, expect, mock, test} from 'bun:test'
|
import {beforeEach, describe, expect, mock, test} from 'bun:test'
|
||||||
|
|
||||||
// The secretsmanager router imports the `secretsManager` singleton directly from
|
// The secretsmanager router imports the `secretsManager` singleton directly from
|
||||||
// `../aws`, so we replace that module with a stub whose `send()` is driven per
|
// `../aws`, so we replace that export with a stub whose `send()` is driven per
|
||||||
// test. `responder` returns the fake AWS SDK response and `lastCommand` captures
|
// test. Bun keeps a module mock for the rest of the run, so every other export
|
||||||
|
// stays real: test files that load `../aws` after this one still get it whole. `responder` returns the fake AWS SDK response and `lastCommand` captures
|
||||||
// the command that was dispatched so we can assert on its input shape.
|
// the command that was dispatched so we can assert on its input shape.
|
||||||
let lastCommand: {constructor: {name: string}; input: Record<string, unknown>} | null = null
|
let lastCommand: {constructor: {name: string}; input: Record<string, unknown>} | null = null
|
||||||
let responder: (commandName: string, command: {input: Record<string, unknown>}) => unknown
|
let responder: (commandName: string, command: {input: Record<string, unknown>}) => unknown
|
||||||
|
|
||||||
|
const realAws = await import('../aws')
|
||||||
mock.module('../aws', () => ({
|
mock.module('../aws', () => ({
|
||||||
|
...realAws,
|
||||||
secretsManager: {
|
secretsManager: {
|
||||||
async send(command: {constructor: {name: string}; input: Record<string, unknown>}) {
|
async send(command: {constructor: {name: string}; input: Record<string, unknown>}) {
|
||||||
lastCommand = command
|
lastCommand = command
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ const originalEndpoint = process.env.FLOCI_ENDPOINT
|
|||||||
const originalFetch = globalThis.fetch
|
const originalFetch = globalThis.fetch
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.env.FLOCI_ENDPOINT = originalEndpoint
|
// Assigning undefined would store the string "undefined" for every later test file.
|
||||||
|
if (originalEndpoint === undefined) delete process.env.FLOCI_ENDPOINT
|
||||||
|
else process.env.FLOCI_ENDPOINT = originalEndpoint
|
||||||
globalThis.fetch = originalFetch
|
globalThis.fetch = originalFetch
|
||||||
mock.restore()
|
mock.restore()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import {ScrollText, Zap} from 'lucide-react'
|
|
||||||
import type {CloudResource} from '@/types/resource'
|
|
||||||
|
|
||||||
type LambdaTrigger = {trigger: string; label: string; functionArn: string; functionName: string; version: string | null}
|
|
||||||
|
|
||||||
export function CognitoTriggersSection({pool}: {pool?: CloudResource}) {
|
|
||||||
const triggers = triggersOf(pool)
|
|
||||||
return <>
|
|
||||||
<div className="input-row resource-table-bar">
|
|
||||||
<div><p className="eyebrow">Lambda triggers</p><span className="muted">Functions Cognito runs during sign-up, sign-in and token issue</span></div>
|
|
||||||
</div>
|
|
||||||
{pool && triggers.length === 0 && <p className="muted padded">This pool runs no Lambda triggers.</p>}
|
|
||||||
{triggers.length > 0 && <div className="cognito-table"><table className="table"><thead><tr><th>Trigger</th><th>Function</th><th>Version</th><th/></tr></thead><tbody>{triggers.map((trigger) => (
|
|
||||||
<tr key={trigger.trigger}>
|
|
||||||
<td>{trigger.label}</td>
|
|
||||||
<td><code title={trigger.functionArn}>{trigger.functionName}</code></td>
|
|
||||||
<td>{trigger.version ? <code>{trigger.version}</code> : '—'}</td>
|
|
||||||
<td className="cognito-row-actions">
|
|
||||||
<a className="button" href={`/cloud-explorer/aws/serverless?resource=${encodeURIComponent(trigger.functionName)}`} aria-label={`Open function ${trigger.functionName}`}><Zap size={13}/> Function</a>
|
|
||||||
<a className="button" href={`/cloud-explorer/aws/logs?resource=${encodeURIComponent(`/aws/lambda/${trigger.functionName}`)}`} aria-label={`Open logs for ${trigger.functionName}`}><ScrollText size={13}/> Logs</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}</tbody></table></div>}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggersOf(pool?: CloudResource): LambdaTrigger[] {
|
|
||||||
const triggers = pool?.metadata.lambdaTriggers
|
|
||||||
if (!Array.isArray(triggers)) return []
|
|
||||||
return triggers.filter((trigger): trigger is LambdaTrigger => typeof trigger?.functionName === 'string' && typeof trigger?.label === 'string')
|
|
||||||
}
|
|
||||||
@@ -64,24 +64,6 @@ test('shows the app client id and the pool users', async () => {
|
|||||||
expect(api.getCloudResource).toHaveBeenCalledWith('aws', 'userpools', summary.id, expect.anything())
|
expect(api.getCloudResource).toHaveBeenCalledWith('aws', 'userpools', summary.id, expect.anything())
|
||||||
})
|
})
|
||||||
|
|
||||||
test('lists the pool\'s Lambda triggers with links to the function and its logs', async () => {
|
|
||||||
api.getCloudResource.mockResolvedValue({...pool('email'), metadata: {...pool('email').metadata, lambdaTriggers: [
|
|
||||||
{trigger: 'PreTokenGeneration', label: 'Pre token generation', functionArn: 'arn:aws:lambda:us-east-1:000000000000:function:notes-demo-pre-token', functionName: 'notes-demo-pre-token', version: null},
|
|
||||||
]}})
|
|
||||||
renderPanel()
|
|
||||||
|
|
||||||
expect(await screen.findByText('Pre token generation')).toBeInTheDocument()
|
|
||||||
expect(screen.getByRole('link', {name: 'Open function notes-demo-pre-token'})).toHaveAttribute('href', '/cloud-explorer/aws/serverless?resource=notes-demo-pre-token')
|
|
||||||
expect(screen.getByRole('link', {name: 'Open logs for notes-demo-pre-token'})).toHaveAttribute('href', '/cloud-explorer/aws/logs?resource=%2Faws%2Flambda%2Fnotes-demo-pre-token')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('says when a pool has no Lambda triggers', async () => {
|
|
||||||
api.getCloudResource.mockResolvedValue(pool('email'))
|
|
||||||
renderPanel()
|
|
||||||
|
|
||||||
expect(await screen.findByText('This pool runs no Lambda triggers.')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('creates an email user who can sign in immediately', async () => {
|
test('creates an email user who can sign in immediately', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
api.getCloudResource.mockResolvedValue(pool('email'))
|
api.getCloudResource.mockResolvedValue(pool('email'))
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {useEffect, useState} from 'react'
|
|||||||
import {deleteFlatItem, getCloudResource, listFlatItems, listUserPoolGroups, putFlatItem} from '@/api/cloudProxyClient'
|
import {deleteFlatItem, getCloudResource, listFlatItems, listUserPoolGroups, putFlatItem} from '@/api/cloudProxyClient'
|
||||||
import {CognitoGroupsSection} from '@/components/CognitoGroupsSection'
|
import {CognitoGroupsSection} from '@/components/CognitoGroupsSection'
|
||||||
import {CognitoSignInTest} from '@/components/CognitoSignInTest'
|
import {CognitoSignInTest} from '@/components/CognitoSignInTest'
|
||||||
import {CognitoTriggersSection} from '@/components/CognitoTriggersSection'
|
|
||||||
import {CognitoUserDetail} from '@/components/CognitoUserDetail'
|
import {CognitoUserDetail} from '@/components/CognitoUserDetail'
|
||||||
import {CopyValueButton} from '@/components/CopyValueButton'
|
import {CopyValueButton} from '@/components/CopyValueButton'
|
||||||
import {useAccountId} from '@/lib/accountStore'
|
import {useAccountId} from '@/lib/accountStore'
|
||||||
@@ -98,8 +97,6 @@ export function CognitoUsersPanel({resource, runtimeReachable}: Props) {
|
|||||||
<tr key={client.clientId}><td>{client.clientName || '—'}</td><td><span className="copy-value mono">{client.clientId}<CopyValueButton value={client.clientId} label="Client ID"/></span></td></tr>
|
<tr key={client.clientId}><td>{client.clientName || '—'}</td><td><span className="copy-value mono">{client.clientId}<CopyValueButton value={client.clientId} label="Client ID"/></span></td></tr>
|
||||||
))}</tbody></table></div>}
|
))}</tbody></table></div>}
|
||||||
|
|
||||||
<CognitoTriggersSection pool={poolQuery.data ?? undefined}/>
|
|
||||||
|
|
||||||
<CognitoSignInTest poolId={resource.id} appClients={appClients} loginLabel={loginLabel} runtimeReachable={runtimeReachable}/>
|
<CognitoSignInTest poolId={resource.id} appClients={appClients} loginLabel={loginLabel} runtimeReachable={runtimeReachable}/>
|
||||||
|
|
||||||
<CognitoGroupsSection poolId={resource.id} groupsQuery={groupsQuery} groupsKey={groupsKey} runtimeReachable={runtimeReachable}/>
|
<CognitoGroupsSection poolId={resource.id} groupsQuery={groupsQuery} groupsKey={groupsKey} runtimeReachable={runtimeReachable}/>
|
||||||
|
|||||||
Reference in New Issue
Block a user