private createAPI()

in packages/constructs/L3/utility/m2m-api-l3-construct/lib/m2m-api-l3-construct.ts [355:661]


  private createAPI(m2mUserPool: IUserPool, apiScope: ResourceServerScope, kmsKey: IKey): void {
    const stageName = this.props.m2mApiProps.stageName || 'prod';

    const integrationLambdaRole = this.props.m2mApiProps.integrationLambdaRoleArn
      ? MdaaLambdaRole.fromRoleArn(this, 'imported-integration-role', this.props.m2mApiProps.integrationLambdaRoleArn)
      : new MdaaLambdaRole(this, 'url-gen-lambda-role', {
          description: 'Lambda Role for presigned S3 URL generation Logger function',
          roleName: 'url-gen-lambda-role',
          naming: this.props.naming,
          logGroupNames: [this.props.naming.resourceName('signed-s3-url-gen')],
          createParams: false,
          createOutputs: false,
        });

    // creates lambda function to generate presigned URL
    const s3UrlGenLambda = new MdaaLambdaFunction(this, 's3-url-gen-lambda', {
      runtime: Runtime.PYTHON_3_13,
      handler: 's3_url.handler',
      functionName: 'signed-s3-url-gen',
      role: integrationLambdaRole,
      naming: this.props.naming,
      code: Code.fromAsset(`${__dirname}/../src/lambda/s3_url`),
      environment: {
        EXPIRY_TIME_SECONDS: '600',
        TARGET_BUCKET: this.props.m2mApiProps.targetBucketName,
        TARGET_PREFIX: this.props.m2mApiProps.targetPrefix,
        METADATA_TARGET_PREFIX: this.props.m2mApiProps.metadataTargetPrefix || this.props.m2mApiProps.targetPrefix,
        EVENT_METADATA_MAPPINGS: JSON.stringify(this.props.m2mApiProps.eventMetadataMappings || {}),
        LOG_LEVEL: 'INFO',
      },
      reservedConcurrentExecutions: this.props.m2mApiProps.concurrencyLimit,
    });

    MdaaNagSuppressions.addCodeResourceSuppressions(
      s3UrlGenLambda,
      [
        {
          id: 'NIST.800.53.R5-LambdaDLQ',
          reason:
            'Function is API implementation and will be invoked syncronously. Error handling is handled by API spec. DLQ not required.',
        },
        { id: 'NIST.800.53.R5-LambdaInsideVPC', reason: 'Function is API implementation behind API gateway.' },
        {
          id: 'HIPAA.Security-LambdaDLQ',
          reason:
            'Function is API implementation and will be invoked syncronously. Error handling is handled by API spec. DLQ not required.',
        },
        {
          id: 'PCI.DSS.321-LambdaDLQ',
          reason:
            'Function is API implementation and will be invoked syncronously. Error handling is handled by API spec. DLQ not required.',
        },
        { id: 'HIPAA.Security-LambdaInsideVPC', reason: 'Function is API implementation behind API gateway.' },
        { id: 'PCI.DSS.321-LambdaInsideVPC', reason: 'Function is API implementation behind API gateway.' },
      ],
      true,
    );

    //create API and components

    const apiResourcePolicy = new PolicyDocument({
      statements: [
        new PolicyStatement({
          effect: Effect.ALLOW,
          actions: ['execute-api:Invoke'],
          principals: [new AnyPrincipal()],
          resources: [`execute-api:/${stageName}/GET/upload*`],
        }),
        new PolicyStatement({
          effect: Effect.DENY,
          principals: [new AnyPrincipal()],
          actions: ['execute-api:Invoke'],
          resources: [`execute-api:/${stageName}/GET/upload*`],
          conditions: {
            NotIpAddress: {
              'aws:SourceIp': this.props.m2mApiProps.allowedCidrs,
            },
          },
        }),
      ],
    });

    const accessLogGroupProps: MdaaLogGroupProps = {
      logGroupName: 'access-logs',
      encryptionKey: kmsKey,
      logGroupNamePathPrefix: '',
      retention: RetentionDays.INFINITE,
      naming: this.props.naming,
    };

    const accessLogGroup = new MdaaLogGroup(this, 'access-log-group', accessLogGroupProps);

    const restApi = new RestApi(this, 'rest-api', {
      restApiName: this.props.naming.resourceName(undefined, 128),
      description: 'REST API to endpoint to proxy an S3 Signed URL generation Lambda',
      policy: apiResourcePolicy,
      cloudWatchRole: false, //Will be created below
      deployOptions: {
        stageName: stageName,
        accessLogDestination: new LogGroupLogDestination(accessLogGroup),
        accessLogFormat: AccessLogFormat.jsonWithStandardFields(),
        tracingEnabled: true,
        methodOptions: {
          '/*/*': {
            loggingLevel: MethodLoggingLevel.INFO,
            cachingEnabled: false,
            cacheDataEncrypted: false,
          },
        },
      },
    });

    MdaaNagSuppressions.addCodeResourceSuppressions(
      restApi,
      [
        {
          id: 'NIST.800.53.R5-APIGWSSLEnabled',
          reason: 'Integrations/backend are Lambda functions. Backend client certificate not required.',
        },
        {
          id: 'HIPAA.Security-APIGWSSLEnabled',
          reason: 'Integrations/backend are Lambda functions. Backend client certificate not required.',
        },
        {
          id: 'PCI.DSS.321-APIGWSSLEnabled',
          reason: 'Integrations/backend are Lambda functions. Backend client certificate not required.',
        },
        { id: 'NIST.800.53.R5-APIGWCacheEnabledAndEncrypted', reason: 'Caching intentionally disabled.' },
        { id: 'HIPAA.Security-APIGWCacheEnabledAndEncrypted', reason: 'Caching intentionally disabled.' },
        { id: 'PCI.DSS.321-APIGWCacheEnabledAndEncrypted', reason: 'Caching intentionally disabled.' },
      ],
      true,
    );

    if (this.props.m2mApiProps.setAccountCloudWatchRole ?? false) {
      const cloudwatchRole = new MdaaRole(this, 'cloudwatch-role', {
        roleName: 'cloudwatch',
        naming: this.props.naming,
        assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
      });
      cloudwatchRole.addManagedPolicy(
        MdaaManagedPolicy.fromAwsManagedPolicyNameWithPartition(
          this,
          'service-role/AmazonAPIGatewayPushToCloudWatchLogs',
        ),
      );

      MdaaNagSuppressions.addCodeResourceSuppressions(
        cloudwatchRole,
        [
          {
            id: 'AwsSolutions-IAM4',
            reason:
              'AmazonAPIGatewayPushToCloudWatchLogs provides the minimum required permissions for API Gateway logging to Cloudwatch: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html',
          },
        ],
        true,
      );
      const account = new CfnAccount(this, 'api-gw-account', {
        cloudWatchRoleArn: cloudwatchRole.roleArn,
      });
      restApi.node.addDependency(account);
    }

    const ipAllowSet = new CfnIPSet(this, 'ip-allow-set', {
      addresses: this.props.m2mApiProps.allowedCidrs,
      ipAddressVersion: 'IPV4',
      scope: 'REGIONAL',
      name: this.props.naming.resourceName('ip-allow-set', 255),
    });

    const ipAllowRuleProps: CfnWebACL.RuleProperty = {
      name: 'ipAllow',
      priority: 0,
      visibilityConfig: {
        cloudWatchMetricsEnabled: false,
        metricName: this.props.naming.resourceName('ip-allow', 255),
        sampledRequestsEnabled: false,
      },
      statement: {
        ipSetReferenceStatement: {
          arn: ipAllowSet.attrArn,
        },
      },
      action: {
        allow: {},
      },
    };

    const defaultWafProps: CfnWebACLProps = {
      name: this.props.naming.resourceName('default-waf', 128),
      defaultAction: {
        block: {},
      },
      scope: 'REGIONAL',
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: this.props.naming.resourceName(undefined, 255),
        sampledRequestsEnabled: false,
      },
      rules: [ipAllowRuleProps],
    };

    const defaultWaf = new CfnWebACL(this, 'default-waf', defaultWafProps);

    const defaultWafLogGroupProps: MdaaLogGroupProps = {
      logGroupName: 'default-waf',
      encryptionKey: kmsKey,
      // WAF log group destination names must start with aws-waf-logs-
      // https://docs.aws.amazon.com/waf/latest/developerguide/logging-cw-logs.html
      logGroupNamePathPrefix: 'aws-waf-logs-',
      retention: RetentionDays.INFINITE,
      naming: this.props.naming,
    };

    const defaultWafLogGroup = new MdaaLogGroup(this, 'default-waf-log-group', defaultWafLogGroupProps);

    new CfnLoggingConfiguration(this, 'default-waf-logging-config', {
      logDestinationConfigs: [defaultWafLogGroup.logGroupArn],
      resourceArn: defaultWaf.attrArn,
    });

    new CfnWebACLAssociation(this, `default-waf-association`, {
      resourceArn: restApi.deploymentStage.stageArn,
      webAclArn: defaultWaf.attrArn,
    });

    Object.entries(this.props.m2mApiProps.wafArns || {}).forEach(wafEntry => {
      new CfnWebACLAssociation(this, `waf-association-${wafEntry[0]}`, {
        resourceArn: restApi.deploymentStage.stageArn,
        webAclArn: wafEntry[1],
      });
    });

    const cognitoAuthorizer = new CognitoUserPoolsAuthorizer(this, 'cognito-authorizer', {
      authorizerName: this.props.naming.resourceName(),
      resultsCacheTtl: Duration.seconds(0),
      cognitoUserPools: [m2mUserPool],
    });

    const restApiRole = new MdaaRole(this, `integration-role`, {
      roleName: `integration`,
      assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
      naming: this.props.naming,
    });

    restApiRole.addToPolicy(
      new PolicyStatement({
        resources: [s3UrlGenLambda.functionArn],
        actions: ['lambda:InvokeFunction'],
        effect: Effect.ALLOW,
      }),
    );

    MdaaNagSuppressions.addCodeResourceSuppressions(
      restApiRole,
      [
        { id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Inline policy is specific to this role and function.' },
        { id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Inline policy is specific to this role and function.' },
        { id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Inline policy is specific to this role and function.' },
      ],
      true,
    );

    const integrationRequestParamers = Object.fromEntries(
      Object.keys(this.props.m2mApiProps.requestParameters || {}).map(param => {
        return [`integration.request.querystring.${param}`, `method.request.querystring.${param}`];
      }),
    );

    const integration = new LambdaIntegration(s3UrlGenLambda, {
      credentialsRole: restApiRole,
      requestParameters: integrationRequestParamers,
    });

    const uploadResource = restApi.root.addResource('upload');

    const proxyResource = uploadResource.addResource('{proxy+}');

    const methodRequestParamers = Object.fromEntries(
      Object.entries(this.props.m2mApiProps.requestParameters || {}).map(entry => {
        return [`method.request.querystring.${entry[0]}`, entry[1]];
      }),
    );

    proxyResource.addMethod('GET', integration, {
      authorizationType: AuthorizationType.COGNITO,
      authorizer: cognitoAuthorizer,
      authorizationScopes: [`${M2MApiL3Construct.identifier}/${apiScope.scopeName}`],
      requestParameters: methodRequestParamers,
      requestValidatorOptions: {
        validateRequestParameters: true,
        validateRequestBody: true,
      },
    });

    const apistagePath = `/${stageName}`;
    new MdaaParamAndOutput(this, {
      ...{
        resourceType: 'rest-api-url',
        resourceId: 'rest-api-upload-url',
        name: 'rest-api-end-point-stage-url',
        value: restApi.urlForPath(apistagePath),
      },
      naming: this.props.naming,
    });
  }