Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions iam.tf
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
resource "aws_cloudformation_stack" "lambda_permissions" {
name = "${var.name}-lambda-permissions-${aws_api_gateway_rest_api.this.id}"
template_body = jsonencode({
Resources = merge([
for urlPath, config in local.definition : {
for httpMethod, definition in config : "AllowExecutionFromAPIGateway${substr(sha256("${upper(httpMethod)} ${urlPath}"), 0, 8)}" => {
Type = "AWS::Lambda::Permission"
Properties = {
FunctionName = definition.lambda.function_name
Action = "lambda:InvokeFunction"
Principal = "apigateway.amazonaws.com"
Resources = merge(
merge([
for urlPath, config in local.definition : {
for httpMethod, definition in config : "AllowExecutionFromAPIGateway${substr(sha256("${upper(httpMethod)} ${urlPath}"), 0, 8)}" => {
Type = "AWS::Lambda::Permission"
Properties = {
FunctionName = definition.lambda.function_name
Action = "lambda:InvokeFunction"
Principal = "apigateway.amazonaws.com"

# # More: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html
SourceArn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.this.id}/*/${upper(httpMethod)}${urlPath}"
}
# # More: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html
SourceArn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.this.id}/*/${upper(httpMethod)}${urlPath}"
}
} if can(definition.lambda.function_name)
}
}
]...)
]...),
merge([
for urlPath, config in local.definition : {
for httpMethod, definition in config : "AllowExecutionFromAPIGatewayAuthorizer${substr(sha256(definition.authorizer.name), 0, 8)}" => {
Type = "AWS::Lambda::Permission"
Properties = {
FunctionName = definition.authorizer.lambda.function_name
Action = "lambda:InvokeFunction"
Principal = "apigateway.amazonaws.com"

# # More: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html
SourceArn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.this.id}/authorizers/*"
}
} if can(definition.authorizer.lambda.function_name)
}
]...)
)
})
}

Expand Down
29 changes: 29 additions & 0 deletions scripts/src/create-openapi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ test('simple', () => {
input: {
'/v1/foo': {
get: {
authorizer: {
name: 'barAuthorizer',
lambda: {
function_name: 'bar',
invoke_arn: 'arn:bar',
},
},

lambda: {
function_name: 'foo',
invoke_arn: 'arn:foo',
Expand All @@ -18,11 +26,32 @@ test('simple', () => {
})
).toMatchInlineSnapshot(`
Object {
"components": Object {
"securitySchemes": Object {
"barAuthorizer": Object {
"in": "header",
"name": "Authorization",
"type": "apiKey",
"x-amazon-apigateway-authorizer": Object {
"authorizerResultTtlInSeconds": 0,
"authorizerUri": "arn:bar",
"identitySource": "method.request.header.Authorization",
"type": "request",
},
"x-amazon-apigateway-authtype": "custom",
},
},
},
"openapi": "3.0.1",
"paths": Object {
"/v1/foo": Object {
"get": Object {
"parameters": undefined,
"security": Array [
Object {
"barAuthorizer": Array [],
},
],
"x-amazon-apigateway-integration": Object {
"httpMethod": "POST",
"type": "aws_proxy",
Expand Down
61 changes: 59 additions & 2 deletions scripts/src/create-openapi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ApiDefinitionInput } from './input.type'
import { stableHash } from './util'

function formatMethod(method: string): string {
method = method.toLowerCase()
Expand Down Expand Up @@ -42,23 +43,79 @@ function collectPathParameters(p: string, config: Record<string, unknown>) {
}

export function createOpenApiSpec({ input, extensions }: { input: ApiDefinitionInput; extensions: string | undefined }) {
const ext = JSON.parse(extensions ?? '{}') as Record<string, unknown>
const authorizers = Object.values(input).flatMap((configs) =>
Object.values(configs)
.map(({ authorizer }) => {
if (authorizer === undefined) {
return undefined
}
const {
name,
lambda,
header = 'Authorization',
authorizerType = 'request',
identitySource,
cacheTtl = 0,
...config
} = authorizer
return [
name,
{
type: 'apiKey',
'x-amazon-apigateway-authtype': 'custom',
...config,
in: 'header',
name: header,
'x-amazon-apigateway-authorizer': {
type: authorizerType,
identitySource: identitySource ?? `method.request.header.${header}`,
authorizerUri: lambda?.invoke_arn,
authorizerResultTtlInSeconds: cacheTtl,
...(config['x-amazon-apigateway-authorizer'] as Record<string, unknown>),
},
},
] as const
})
.filter(<T>(x: T | undefined): x is T => x !== undefined)
)
const authorizerNames = [...new Set(authorizers.map(([name]) => name))]
for (const name of authorizerNames) {
const matches = authorizers.filter(([n]) => n === name).map(([, a]) => a)
if (matches.length > 1 && [...new Set(matches.map(stableHash))].length !== 1) {
throw new Error(`Encountered mismatching definitions for authorizer: ${name}`)
}
}
const components = {
...(ext?.components as Record<string, unknown>),
securitySchemes: {
...((ext?.components as Record<string, unknown>)?.securitySchemes as Record<string, unknown>),
...Object.fromEntries(authorizers),
},
}
return {
openapi: '3.0.1',
...(JSON.parse(extensions ?? '{}') as Record<string, unknown>),
...ext,
paths: Object.fromEntries(
Object.entries(input).map(([p, configs]) => [
p,
Object.fromEntries(
Object.entries(configs).map(([method, { lambda, ...config }]) => [
Object.entries(configs).map(([method, { lambda, authorizer, ...config }]) => [
formatMethod(method),
{
...config,
parameters: collectPathParameters(p, config),
'x-amazon-apigateway-integration': formatLambdaIntegration(lambda, config),
security:
authorizer !== undefined
? [{ [authorizer.name]: [] }, ...((config.security as unknown[] | undefined) ?? [])]
: config.security,
},
])
),
])
),
components:
Object.keys(components).length > 1 || Object.keys(components.securitySchemes).length > 0 ? components : undefined,
}
}
15 changes: 14 additions & 1 deletion scripts/src/input.schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { $object, $optional, $string, $unknown, $validator } from '@skyleague/therefore'
import { $number, $object, $optional, $string, $unknown, $validator } from '@skyleague/therefore'

const lambdaResource = $object(
{
Expand All @@ -17,6 +17,19 @@ export const apiDefinitionInput = $validator(
indexSignature: $object(
{
lambda: $optional(lambdaResource),
authorizer: $optional(
$object(
{
lambda: $optional(lambdaResource),
name: $string(),
authorizerType: $optional($string),
identitySource: $optional($string),
header: $optional($string),
cacheTtl: $optional($number),
},
{ indexSignature: $unknown }
)
),
},
{ indexSignature: $unknown }
),
Expand Down
17 changes: 15 additions & 2 deletions scripts/src/input.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ export interface ApiDefinitionInput {
function_name: string
[k: string]: unknown
}
authorizer?: {
lambda?: {
invoke_arn: string
function_name: string
[k: string]: unknown
}
name: string
authorizerType?: string
identitySource?: string
header?: string
cacheTtl?: number
[k: string]: unknown
}
[k: string]: unknown
}
}
Expand All @@ -23,7 +36,7 @@ export const ApiDefinitionInput = {
get schema() {
return ApiDefinitionInput.validate.schema
},
source: `${__dirname}input.schema`,
source: `${__dirname}/input.schema`,
sourceSymbol: 'apiDefinitionInput',
is: (o: unknown): o is ApiDefinitionInput => ApiDefinitionInput.validate(o) === true,
} as const
Expand All @@ -38,7 +51,7 @@ export const ApiDefinitionInputStringified = {
get schema() {
return ApiDefinitionInputStringified.validate.schema
},
source: `${__dirname}input.schema`,
source: `${__dirname}/input.schema`,
sourceSymbol: 'apiDefinitionInputStringified',
is: (o: unknown): o is ApiDefinitionInputStringified => ApiDefinitionInputStringified.validate(o) === true,
} as const

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading