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