Skip to content

Commit dd2f55c

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(cloudwatch): resolve selector credentials server-side
1 parent ad5a672 commit dd2f55c

11 files changed

Lines changed: 657 additions & 70 deletions

File tree

apps/sim/app/api/tools/cloudwatch/describe-log-groups/route.ts

Lines changed: 7 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
1-
import { DescribeLogGroupsCommand } from '@aws-sdk/client-cloudwatch-logs'
21
import { createLogger } from '@sim/logger'
32
import { toError } from '@sim/utils/errors'
43
import { type NextRequest, NextResponse } from 'next/server'
54
import { cloudwatchLogGroupsSelectorContract } from '@/lib/api/contracts/selectors/cloudwatch'
65
import { parseToolRequest } from '@/lib/api/server'
76
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
87
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9-
import { createCloudWatchLogsClient } from '@/app/api/tools/cloudwatch/utils'
8+
import { createCloudWatchLogsClient, describeLogGroups } from '@/app/api/tools/cloudwatch/utils'
109

1110
const logger = createLogger('CloudWatchDescribeLogGroups')
1211

13-
/** AWS DescribeLogGroups caps `limit` at 50 items per page. */
14-
const LOG_GROUPS_PAGE_SIZE = 50
15-
16-
/** Upper bound on pages drained to avoid unbounded loops on very large accounts. */
17-
const MAX_LOG_GROUPS_PAGES = 20
18-
1912
export const POST = withRouteHandler(async (request: NextRequest) => {
2013
try {
2114
const auth = await checkSessionOrInternalAuth(request)
@@ -39,58 +32,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
3932
})
4033

4134
try {
42-
const totalLimit = validatedData.limit
43-
const logGroups: {
44-
logGroupName: string
45-
arn: string
46-
storedBytes: number
47-
retentionInDays: number | undefined
48-
creationTime: number | undefined
49-
}[] = []
50-
let nextToken: string | undefined
51-
52-
for (let page = 0; page < MAX_LOG_GROUPS_PAGES; page++) {
53-
const pageLimit =
54-
totalLimit !== undefined
55-
? Math.min(LOG_GROUPS_PAGE_SIZE, totalLimit - logGroups.length)
56-
: LOG_GROUPS_PAGE_SIZE
57-
58-
const command = new DescribeLogGroupsCommand({
59-
...(validatedData.prefix && { logGroupNamePrefix: validatedData.prefix }),
60-
limit: pageLimit,
61-
...(nextToken && { nextToken }),
62-
})
63-
64-
const response = await client.send(command)
65-
66-
for (const lg of response.logGroups ?? []) {
67-
logGroups.push({
68-
logGroupName: lg.logGroupName ?? '',
69-
arn: lg.arn ?? '',
70-
storedBytes: lg.storedBytes ?? 0,
71-
retentionInDays: lg.retentionInDays,
72-
creationTime: lg.creationTime,
73-
})
74-
}
75-
76-
nextToken = response.nextToken
77-
if (!nextToken) break
78-
if (totalLimit !== undefined && logGroups.length >= totalLimit) break
79-
80-
if (page === MAX_LOG_GROUPS_PAGES - 1) {
81-
logger.warn(
82-
`DescribeLogGroups hit pagination cap of ${MAX_LOG_GROUPS_PAGES} pages; log group list may be incomplete`
83-
)
84-
}
85-
}
86-
87-
const cappedLogGroups = totalLimit !== undefined ? logGroups.slice(0, totalLimit) : logGroups
35+
const result = await describeLogGroups(client, {
36+
prefix: validatedData.prefix,
37+
limit: validatedData.limit,
38+
})
8839

89-
logger.info(`Successfully described ${cappedLogGroups.length} log groups`)
40+
logger.info(`Successfully described ${result.logGroups.length} log groups`)
9041

9142
return NextResponse.json({
9243
success: true,
93-
output: { logGroups: cappedLogGroups },
44+
output: { logGroups: result.logGroups },
9445
})
9546
} finally {
9647
client.destroy()
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import {
4+
cloudwatchLogGroupsBodySchema,
5+
cloudwatchSelectorLogGroupsContract,
6+
} from '@/lib/api/contracts/selectors/cloudwatch'
7+
import { parseRequest } from '@/lib/api/server'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
import {
10+
authenticateSelectorRequest,
11+
resolveAuthorizedSelectorContext,
12+
} from '@/lib/selectors/server/resolve-authorized-context'
13+
import { createCloudWatchLogsClient, describeLogGroups } from '@/app/api/tools/cloudwatch/utils'
14+
15+
const logger = createLogger('CloudWatchSelectorLogGroupsAPI')
16+
17+
export const POST = withRouteHandler(async (request: NextRequest) => {
18+
try {
19+
const authentication = await authenticateSelectorRequest(request)
20+
if (!authentication.ok) {
21+
return NextResponse.json({ error: authentication.error }, { status: authentication.status })
22+
}
23+
const parsed = await parseRequest(cloudwatchSelectorLogGroupsContract, request, {})
24+
if (!parsed.success) return parsed.response
25+
26+
const { workflowId, prefix, limit, ...context } = parsed.data.body
27+
const resolution = await resolveAuthorizedSelectorContext(authentication.principal, {
28+
workflowId,
29+
context,
30+
})
31+
if (!resolution.ok) {
32+
return NextResponse.json({ error: resolution.error }, { status: resolution.status })
33+
}
34+
35+
const validated = cloudwatchLogGroupsBodySchema.safeParse({
36+
...resolution.context,
37+
prefix,
38+
limit,
39+
})
40+
if (!validated.success) {
41+
return NextResponse.json(
42+
{ error: 'Invalid CloudWatch selector configuration' },
43+
{ status: 400 }
44+
)
45+
}
46+
47+
const client = createCloudWatchLogsClient(validated.data)
48+
try {
49+
const result = await describeLogGroups(client, {
50+
prefix: validated.data.prefix,
51+
limit: validated.data.limit,
52+
})
53+
return NextResponse.json({
54+
logGroups: result.logGroups.map(({ logGroupName }) => ({ logGroupName })),
55+
})
56+
} finally {
57+
client.destroy()
58+
}
59+
} catch {
60+
logger.error('CloudWatch selector log-group request failed')
61+
return NextResponse.json({ error: 'Failed to retrieve CloudWatch log groups' }, { status: 500 })
62+
}
63+
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import {
4+
cloudwatchLogStreamsBodySchema,
5+
cloudwatchSelectorLogStreamsContract,
6+
} from '@/lib/api/contracts/selectors/cloudwatch'
7+
import { parseRequest } from '@/lib/api/server'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
import {
10+
authenticateSelectorRequest,
11+
resolveAuthorizedSelectorContext,
12+
} from '@/lib/selectors/server/resolve-authorized-context'
13+
import { createCloudWatchLogsClient, describeLogStreams } from '@/app/api/tools/cloudwatch/utils'
14+
15+
const logger = createLogger('CloudWatchSelectorLogStreamsAPI')
16+
17+
export const POST = withRouteHandler(async (request: NextRequest) => {
18+
try {
19+
const authentication = await authenticateSelectorRequest(request)
20+
if (!authentication.ok) {
21+
return NextResponse.json({ error: authentication.error }, { status: authentication.status })
22+
}
23+
const parsed = await parseRequest(cloudwatchSelectorLogStreamsContract, request, {})
24+
if (!parsed.success) return parsed.response
25+
26+
const { workflowId, prefix, limit, logGroupName, ...context } = parsed.data.body
27+
const resolution = await resolveAuthorizedSelectorContext(authentication.principal, {
28+
workflowId,
29+
context,
30+
})
31+
if (!resolution.ok) {
32+
return NextResponse.json({ error: resolution.error }, { status: resolution.status })
33+
}
34+
35+
const validated = cloudwatchLogStreamsBodySchema.safeParse({
36+
...resolution.context,
37+
prefix,
38+
limit,
39+
logGroupName,
40+
})
41+
if (!validated.success) {
42+
return NextResponse.json(
43+
{ error: 'Invalid CloudWatch selector configuration' },
44+
{ status: 400 }
45+
)
46+
}
47+
48+
const client = createCloudWatchLogsClient(validated.data)
49+
try {
50+
const result = await describeLogStreams(client, validated.data.logGroupName, {
51+
prefix: validated.data.prefix,
52+
limit: validated.data.limit,
53+
})
54+
return NextResponse.json({
55+
logStreams: result.logStreams.map(({ logStreamName }) => ({ logStreamName })),
56+
})
57+
} finally {
58+
client.destroy()
59+
}
60+
} catch {
61+
logger.error('CloudWatch selector log-stream request failed')
62+
return NextResponse.json(
63+
{ error: 'Failed to retrieve CloudWatch log streams' },
64+
{ status: 500 }
65+
)
66+
}
67+
})

0 commit comments

Comments
 (0)