in packages/type-safe-api/src/construct/type-safe-rest-api.ts [115:474]
constructor(scope: Construct, id: string, props: TypeSafeRestApiProps) {
super(scope, id);
addMetric(scope, "type-safe-rest-api");
const {
integrations,
specPath,
operationLookup,
defaultAuthorizer,
corsOptions,
outputSpecBucket,
...options
} = props;
// Upload the spec to s3 as an asset
const inputSpecAsset = new Asset(this, "InputSpec", {
path: specPath,
});
const prepareSpecOutputBucket = outputSpecBucket ?? inputSpecAsset.bucket;
// We'll output the prepared spec in the same asset bucket
const preparedSpecOutputKeyPrefix = `${inputSpecAsset.s3ObjectKey}-prepared`;
const stack = Stack.of(this);
// Lambda name prefix is truncated to 48 characters (16 below the max of 64)
const lambdaNamePrefix = `${PDKNag.getStackPrefix(stack)
.split("/")
.join("-")
.slice(0, 40)}${this.node.addr.slice(-8).toUpperCase()}`;
const prepareSpecLambdaName = `${lambdaNamePrefix}PrepSpec`;
const prepareSpecRole = new Role(this, "PrepareSpecRole", {
assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
inlinePolicies: {
logs: new PolicyDocument({
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
resources: [
`arn:aws:logs:${stack.region}:${stack.account}:log-group:/aws/lambda/${prepareSpecLambdaName}`,
`arn:aws:logs:${stack.region}:${stack.account}:log-group:/aws/lambda/${prepareSpecLambdaName}:*`,
],
}),
],
}),
s3: new PolicyDocument({
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["s3:getObject"],
resources: [
inputSpecAsset.bucket.arnForObjects(inputSpecAsset.s3ObjectKey),
],
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["s3:putObject"],
resources: [
// The output file will include a hash of the prepared spec, which is not known until deploy time since
// tokens must be resolved
prepareSpecOutputBucket.arnForObjects(
`${preparedSpecOutputKeyPrefix}/*`
),
],
}),
],
}),
},
});
["AwsSolutions-IAM5", "AwsPrototyping-IAMNoWildcardPermissions"].forEach(
(RuleId) => {
NagSuppressions.addResourceSuppressions(
prepareSpecRole,
[
{
id: RuleId,
reason:
"Cloudwatch resources have been scoped down to the LogGroup level, however * is still needed as stream names are created just in time.",
appliesTo: [
{
regex: `/^Resource::arn:aws:logs:${PDKNag.getStackRegionRegex(
stack
)}:${PDKNag.getStackAccountRegex(
stack
)}:log-group:/aws/lambda/${prepareSpecLambdaName}:\*/g`,
},
],
},
{
id: RuleId,
reason:
"S3 resources have been scoped down to the appropriate prefix in the CDK asset bucket, however * is still needed as since the prepared spec hash is not known until deploy time.",
appliesTo: [
{
regex: `/^Resource::arn:${PDKNag.getStackPartitionRegex(
stack
)}:s3:.*/${preparedSpecOutputKeyPrefix}/\*/g`,
},
],
},
],
true
);
}
);
// Create a custom resource for preparing the spec for deployment (adding integrations, authorizers, etc)
const prepareSpec = new LambdaFunction(this, "PrepareSpecHandler", {
handler: "index.handler",
runtime: Runtime.NODEJS_18_X,
code: Code.fromAsset(
path.join(__dirname, "./prepare-spec-event-handler")
),
timeout: Duration.seconds(30),
role: prepareSpecRole,
functionName: prepareSpecLambdaName,
});
const providerFunctionName = `${lambdaNamePrefix}PrepSpecProvider`;
const providerRole = new Role(this, "PrepareSpecProviderRole", {
assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
inlinePolicies: {
logs: new PolicyDocument({
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
resources: [
`arn:aws:logs:${stack.region}:${stack.account}:log-group:/aws/lambda/${providerFunctionName}`,
`arn:aws:logs:${stack.region}:${stack.account}:log-group:/aws/lambda/${providerFunctionName}:*`,
],
}),
],
}),
},
});
const provider = new Provider(this, "PrepareSpecProvider", {
onEventHandler: prepareSpec,
role: providerRole,
providerFunctionName,
});
["AwsSolutions-IAM5", "AwsPrototyping-IAMNoWildcardPermissions"].forEach(
(RuleId) => {
NagSuppressions.addResourceSuppressions(
providerRole,
[
{
id: RuleId,
reason:
"Cloudwatch resources have been scoped down to the LogGroup level, however * is still needed as stream names are created just in time.",
},
],
true
);
}
);
["AwsSolutions-L1", "AwsPrototyping-LambdaLatestVersion"].forEach(
(RuleId) => {
NagSuppressions.addResourceSuppressions(
provider,
[
{
id: RuleId,
reason:
"Latest runtime cannot be configured. CDK will need to upgrade the Provider construct accordingly.",
},
],
true
);
}
);
const serializedCorsOptions: SerializedCorsOptions | undefined =
corsOptions && {
allowHeaders: corsOptions.allowHeaders || [
...Cors.DEFAULT_HEADERS,
"x-amz-content-sha256",
],
allowMethods: corsOptions.allowMethods || Cors.ALL_METHODS,
allowOrigins: corsOptions.allowOrigins,
statusCode: corsOptions.statusCode || 204,
};
const prepareSpecOptions: PrepareApiSpecOptions = {
defaultAuthorizerReference:
serializeAsAuthorizerReference(defaultAuthorizer),
integrations: Object.fromEntries(
Object.entries(integrations).map(([operationId, integration]) => [
operationId,
{
integration: integration.integration.render({
operationId,
scope: this,
...operationLookup[operationId],
corsOptions: serializedCorsOptions,
operationLookup,
}),
methodAuthorizer: serializeAsAuthorizerReference(
integration.authorizer
),
options: integration.options,
},
])
),
securitySchemes: prepareSecuritySchemes(
this,
integrations,
defaultAuthorizer,
options.apiKeyOptions
),
corsOptions: serializedCorsOptions,
operationLookup,
apiKeyOptions: options.apiKeyOptions,
};
// Spec preparation will happen in a custom resource lambda so that references to lambda integrations etc can be
// resolved. However, we also prepare inline to perform some additional validation at synth time.
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
this.extendedApiSpecification = prepareApiSpec(spec, prepareSpecOptions);
const prepareApiSpecCustomResourceProperties: PrepareApiSpecCustomResourceProperties =
{
inputSpecLocation: {
bucket: inputSpecAsset.bucket.bucketName,
key: inputSpecAsset.s3ObjectKey,
},
outputSpecLocation: {
bucket: prepareSpecOutputBucket.bucketName,
key: preparedSpecOutputKeyPrefix,
},
...prepareSpecOptions,
};
const prepareSpecCustomResource = new CustomResource(
this,
"PrepareSpecCustomResource",
{
serviceToken: provider.serviceToken,
properties: {
options: prepareApiSpecCustomResourceProperties,
},
}
);
// Create the api gateway resources from the spec, augmenting the spec with the properties specific to api gateway
// such as integrations or auth types
this.api = new SpecRestApi(this, id, {
apiDefinition: this.node.tryGetContext("type-safe-api-local")
? ApiDefinition.fromInline(this.extendedApiSpecification)
: ApiDefinition.fromBucket(
prepareSpecOutputBucket,
prepareSpecCustomResource.getAttString("outputSpecKey")
),
deployOptions: {
accessLogDestination: new LogGroupLogDestination(
new LogGroup(this, `AccessLogs`)
),
accessLogFormat: AccessLogFormat.clf(),
loggingLevel: MethodLoggingLevel.INFO,
},
...options,
});
this.api.node.addDependency(prepareSpecCustomResource);
// While the api will be updated when the output path from the custom resource changes, CDK still needs to know when
// to redeploy the api. This is achieved by including a hash of the spec in the logical id (internalised in the
// addToLogicalId method since this is how changes of individual resources/methods etc trigger redeployments in CDK)
this.api.latestDeployment?.addToLogicalId(this.extendedApiSpecification);
// Grant API Gateway permission to invoke the integrations
Object.keys(integrations).forEach((operationId) => {
integrations[operationId].integration.grant({
operationId,
scope: this,
api: this.api,
...operationLookup[operationId],
operationLookup,
});
});
// Grant API Gateway permission to invoke each custom authorizer lambda (if any)
getAuthorizerFunctions(props).forEach(({ label, function: lambda }) => {
new CfnPermission(this, `LambdaPermission-${label}`, {
action: "lambda:InvokeFunction",
principal: "apigateway.amazonaws.com",
functionName: lambda.functionArn,
sourceArn: stack.formatArn({
service: "execute-api",
resource: this.api.restApiId,
resourceName: "*/*",
}),
});
});
// Create and associate the web acl if not disabled
if (!props.webAclOptions?.disable) {
const acl = new OpenApiGatewayWebAcl(this, `${id}-Acl`, {
...props.webAclOptions,
apiDeploymentStageArn: this.api.deploymentStage.stageArn,
});
this.webAcl = acl.webAcl;
this.ipSet = acl.ipSet;
this.webAclAssociation = acl.webAclAssociation;
}
["AwsSolutions-IAM4", "AwsPrototyping-IAMNoManagedPolicies"].forEach(
(RuleId) => {
NagSuppressions.addResourceSuppressions(
this,
[
{
id: RuleId,
reason:
"Cloudwatch Role requires access to create/read groups at the root level.",
appliesTo: [
{
regex: `/^Policy::arn:${PDKNag.getStackPartitionRegex(
stack
)}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g`,
},
],
},
],
true
);
}
);
["AwsSolutions-APIG2", "AwsPrototyping-APIGWRequestValidation"].forEach(
(RuleId) => {
NagSuppressions.addResourceSuppressions(
this,
[
{
id: RuleId,
reason:
"This construct implements fine grained validation via OpenApi.",
},
],
true
);
}
);
}