constructor()

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
        );
      }
    );
  }