diff --git a/integration/resources/expected/single/graphqlapi-configuration.json b/integration/resources/expected/single/graphqlapi-configuration.json new file mode 100644 index 000000000..771f3bdd6 --- /dev/null +++ b/integration/resources/expected/single/graphqlapi-configuration.json @@ -0,0 +1,58 @@ +[ + { + "LogicalResourceId": "SuperCoolAPI", + "ResourceType": "AWS::AppSync::GraphQLApi" + }, + { + "LogicalResourceId": "SuperCoolAPICloudWatchRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "SuperCoolAPISchema", + "ResourceType": "AWS::AppSync::GraphQLSchema" + }, + { + "LogicalResourceId": "SuperCoolAPIQuerygetBook", + "ResourceType": "AWS::AppSync::Resolver" + }, + { + "LogicalResourceId": "SuperCoolAPINoneDataSource", + "ResourceType": "AWS::AppSync::DataSource" + }, + { + "LogicalResourceId": "SuperCoolAPIprocessQuery", + "ResourceType": "AWS::AppSync::FunctionConfiguration" + }, + { + "LogicalResourceId": "SuperCoolAPIMyApiKey", + "ResourceType": "AWS::AppSync::ApiKey" + }, + { + "LogicalResourceId": "IntrospectionDisableSuperCoolAPI", + "ResourceType": "AWS::AppSync::GraphQLApi" + }, + { + "LogicalResourceId": "IntrospectionDisableSuperCoolAPICloudWatchRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "IntrospectionDisableSuperCoolAPISchema", + "ResourceType": "AWS::AppSync::GraphQLSchema" + }, + { + "LogicalResourceId": "IntrospectionDisableSuperCoolAPIQuerygetBook", + "ResourceType": "AWS::AppSync::Resolver" + }, + { + "LogicalResourceId": "IntrospectionDisableSuperCoolAPINoneDataSource", + "ResourceType": "AWS::AppSync::DataSource" + }, + { + "LogicalResourceId": "IntrospectionDisableSuperCoolAPIprocessQuery", + "ResourceType": "AWS::AppSync::FunctionConfiguration" + }, + { + "LogicalResourceId": "IntrospectionDisableSuperCoolAPIMyApiKey", + "ResourceType": "AWS::AppSync::ApiKey" + } +] diff --git a/integration/resources/templates/single/graphqlapi-configuration.yaml b/integration/resources/templates/single/graphqlapi-configuration.yaml new file mode 100644 index 000000000..5afb5529a --- /dev/null +++ b/integration/resources/templates/single/graphqlapi-configuration.yaml @@ -0,0 +1,94 @@ +Transform: AWS::Serverless-2016-10-31 +Resources: + SuperCoolAPI: + Type: AWS::Serverless::GraphQLApi + Properties: + SchemaInline: | + type Book { + bookName: String + id: ID + } + type Query { getBook(bookName: String): Book } + OwnerContact: blah-blah + Auth: + Type: API_KEY + ApiKeys: + MyApiKey: {} + Functions: + processQuery: + Runtime: + Name: APPSYNC_JS + Version: 1.0.0 + DataSource: NONE + InlineCode: | + import { util } from '@aws-appsync/utils'; + + export function request(ctx) { + const id = util.autoId(); + return { payload: { ...ctx.args, id } }; + } + + export function response(ctx) { + return ctx.result; + } + Resolvers: + Query: + getBook: + Pipeline: + - processQuery + + IntrospectionDisableSuperCoolAPI: + Type: AWS::Serverless::GraphQLApi + Properties: + SchemaInline: | + type Book { + bookName: String + id: ID + } + type Query { getBook(bookName: String): Book } + OwnerContact: blah-blah + IntrospectionConfig: DISABLED + QueryDepthLimit: 10 + ResolverCountLimit: 100 + Auth: + Type: API_KEY + ApiKeys: + MyApiKey: {} + Functions: + processQuery: + Runtime: + Name: APPSYNC_JS + Version: 1.0.0 + DataSource: NONE + InlineCode: | + import { util } from '@aws-appsync/utils'; + + export function request(ctx) { + const id = util.autoId(); + return { payload: { ...ctx.args, id } }; + } + + export function response(ctx) { + return ctx.result; + } + Resolvers: + Query: + getBook: + Pipeline: + - processQuery +Outputs: + SuperCoolAPI: + Description: AppSync API + Value: !GetAtt SuperCoolAPI.GraphQLUrl + MyApiKey: + Description: API Id + Value: !GetAtt SuperCoolAPIMyApiKey.ApiKey + IntrospectionDisableSuperCoolAPI: + Description: AppSync API + Value: !GetAtt IntrospectionDisableSuperCoolAPI.GraphQLUrl + IntrospectionDisableSuperCoolAPIMyApiKey: + Description: API Id + Value: !GetAtt IntrospectionDisableSuperCoolAPIMyApiKey.ApiKey + +Metadata: + SamTransformTest: true diff --git a/integration/single/test_graphqlapi_configuration.py b/integration/single/test_graphqlapi_configuration.py new file mode 100644 index 000000000..6cddf1809 --- /dev/null +++ b/integration/single/test_graphqlapi_configuration.py @@ -0,0 +1,85 @@ +import json +from unittest.case import skipIf + +import pytest +import requests + +from integration.config.service_names import APP_SYNC +from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support + + +def execute_and_verify_appsync_query(url, api_key, query): + """ + Executes a query to an AppSync GraphQLApi. + + Also checks that the response is 200 and does not contain errors before returning. + """ + headers = { + "Content-Type": "application/json", + "x-api-key": api_key, + } + payload = {"query": query} + + response = requests.post(url, json=payload, headers=headers) + response.raise_for_status() + data = response.json() + if "errors" in data: + raise Exception(json.dumps(data["errors"])) + + return data + + +@skipIf(current_region_does_not_support([APP_SYNC]), "AppSync is not supported in this testing region") +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +class TestGraphQLApiConfiguration(BaseTest): + def test_api(self): + file_name = "single/graphqlapi-configuration" + self.create_and_verify_stack(file_name) + + outputs = self.get_stack_outputs() + + url = outputs["SuperCoolAPI"] + api_key = outputs["MyApiKey"] + + introspection_disable_api_url = outputs["IntrospectionDisableSuperCoolAPI"] + introspection_disable_api_key = outputs["IntrospectionDisableSuperCoolAPIMyApiKey"] + + book_name = "GoodBook" + query = f""" + query MyQuery {{ + getBook( + bookName: "{book_name}" + ) {{ + id + bookName + }} + }} + """ + + response = execute_and_verify_appsync_query(url, api_key, query) + self.assertEqual(response["data"]["getBook"]["bookName"], book_name) + + introspection_disable_query_response = execute_and_verify_appsync_query( + introspection_disable_api_url, introspection_disable_api_key, query + ) + self.assertEqual(introspection_disable_query_response["data"]["getBook"]["bookName"], book_name) + + query_introsepction = """ + query myQuery { + __schema { + types { + name + } + } + } + """ + + introspection_query_response = execute_and_verify_appsync_query(url, api_key, query_introsepction) + self.assertIsNotNone(introspection_query_response["data"]["__schema"]) + + # sending introspection query and expecting error as introspection is DISABLED for this API using template file + with self.assertRaises(Exception): + execute_and_verify_appsync_query( + introspection_disable_api_url, introspection_disable_api_key, query_introsepction + ) diff --git a/pytest.ini b/pytest.ini index cd1ec10a7..a83694b93 100644 --- a/pytest.ini +++ b/pytest.ini @@ -20,3 +20,5 @@ filterwarnings = ignore::pytest.PytestUnraisableExceptionWarning # https://github.com/urllib3/urllib3/blob/main/src/urllib3/poolmanager.py#L313 ignore::DeprecationWarning:urllib3.*: + # https://github.com/boto/boto3/issues/3889 + ignore:datetime.datetime.utcnow diff --git a/samtranslator/internal/model/appsync.py b/samtranslator/internal/model/appsync.py index de81660c5..45bbf9f3f 100644 --- a/samtranslator/internal/model/appsync.py +++ b/samtranslator/internal/model/appsync.py @@ -115,6 +115,9 @@ class GraphQLApi(Resource): "AdditionalAuthenticationProviders": GeneratedProperty(), "Visibility": GeneratedProperty(), "OwnerContact": GeneratedProperty(), + "IntrospectionConfig": GeneratedProperty(), + "QueryDepthLimit": GeneratedProperty(), + "ResolverCountLimit": GeneratedProperty(), } Name: str @@ -128,6 +131,9 @@ class GraphQLApi(Resource): LogConfig: Optional[LogConfigType] Visibility: Optional[str] OwnerContact: Optional[str] + IntrospectionConfig: Optional[str] + QueryDepthLimit: Optional[int] + ResolverCountLimit: Optional[int] runtime_attrs = {"api_id": lambda self: fnGetAtt(self.logical_id, "ApiId")} diff --git a/samtranslator/internal/schema_source/aws_serverless_graphqlapi.py b/samtranslator/internal/schema_source/aws_serverless_graphqlapi.py index b731abee7..ff6bd017c 100644 --- a/samtranslator/internal/schema_source/aws_serverless_graphqlapi.py +++ b/samtranslator/internal/schema_source/aws_serverless_graphqlapi.py @@ -164,6 +164,9 @@ class Properties(BaseModel): Cache: Optional[Cache] Visibility: Optional[PassThroughProp] OwnerContact: Optional[PassThroughProp] + IntrospectionConfig: Optional[PassThroughProp] + QueryDepthLimit: Optional[PassThroughProp] + ResolverCountLimit: Optional[PassThroughProp] class Resource(BaseModel): diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 37a6deb06..8f3b6492f 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -2226,6 +2226,9 @@ class SamGraphQLApi(SamResourceMacro): "Cache": Property(False, IS_DICT), "Visibility": PassThroughProperty(False), "OwnerContact": PassThroughProperty(False), + "IntrospectionConfig": PassThroughProperty(False), + "QueryDepthLimit": PassThroughProperty(False), + "ResolverCountLimit": PassThroughProperty(False), } Auth: List[Dict[str, Any]] @@ -2243,6 +2246,9 @@ class SamGraphQLApi(SamResourceMacro): Cache: Optional[Dict[str, Any]] Visibility: Optional[PassThrough] OwnerContact: Optional[PassThrough] + IntrospectionConfig: Optional[PassThrough] + QueryDepthLimit: Optional[PassThrough] + ResolverCountLimit: Optional[PassThrough] # stop validation so we can use class variables for tracking state validate_setattr = False @@ -2322,6 +2328,13 @@ def _construct_appsync_api_resources( api.OwnerContact = passthrough_value(model.OwnerContact) api.XrayEnabled = model.XrayEnabled + if model.IntrospectionConfig: + api.IntrospectionConfig = passthrough_value(model.IntrospectionConfig) + if model.QueryDepthLimit: + api.QueryDepthLimit = passthrough_value(model.QueryDepthLimit) + if model.ResolverCountLimit: + api.ResolverCountLimit = passthrough_value(model.ResolverCountLimit) + lambda_auth_arns = self._parse_and_set_auth_properties(api, model.Auth) auth_connectors = [ self._construct_lambda_auth_connector(api, arn, i) for i, arn in enumerate(lambda_auth_arns, 1) diff --git a/samtranslator/schema/schema.json b/samtranslator/schema/schema.json index 556c36c6d..063ae65b2 100644 --- a/samtranslator/schema/schema.json +++ b/samtranslator/schema/schema.json @@ -279729,6 +279729,9 @@ "title": "Functions", "type": "object" }, + "IntrospectionConfig": { + "$ref": "#/definitions/PassThroughProp" + }, "Logging": { "anyOf": [ { @@ -279746,6 +279749,12 @@ "OwnerContact": { "$ref": "#/definitions/PassThroughProp" }, + "QueryDepthLimit": { + "$ref": "#/definitions/PassThroughProp" + }, + "ResolverCountLimit": { + "$ref": "#/definitions/PassThroughProp" + }, "Resolvers": { "additionalProperties": { "additionalProperties": { diff --git a/schema_source/sam.schema.json b/schema_source/sam.schema.json index 9900d9de1..60c1746ee 100644 --- a/schema_source/sam.schema.json +++ b/schema_source/sam.schema.json @@ -6659,6 +6659,9 @@ "title": "Functions", "type": "object" }, + "IntrospectionConfig": { + "$ref": "#/definitions/PassThroughProp" + }, "Logging": { "anyOf": [ { @@ -6676,6 +6679,12 @@ "OwnerContact": { "$ref": "#/definitions/PassThroughProp" }, + "QueryDepthLimit": { + "$ref": "#/definitions/PassThroughProp" + }, + "ResolverCountLimit": { + "$ref": "#/definitions/PassThroughProp" + }, "Resolvers": { "additionalProperties": { "additionalProperties": { diff --git a/tests/translator/input/graphqlapi_introspection_query_resolver_limits.yaml b/tests/translator/input/graphqlapi_introspection_query_resolver_limits.yaml new file mode 100644 index 000000000..6df2fd9cc --- /dev/null +++ b/tests/translator/input/graphqlapi_introspection_query_resolver_limits.yaml @@ -0,0 +1,17 @@ +Transform: AWS::Serverless-2016-10-31 +Resources: + SuperCoolAPI: + Type: AWS::Serverless::GraphQLApi + Properties: + SchemaInline: | + type Book { + bookName: String + } + type Query { getBook(bookName: String): Book } + Visibility: PRIVATE + OwnerContact: blah-blah + IntrospectionConfig: DISABLED + QueryDepthLimit: 10 + ResolverCountLimit: 100 + Auth: + Type: AWS_IAM diff --git a/tests/translator/output/aws-cn/graphqlapi_introspection_query_resolver_limits.json b/tests/translator/output/aws-cn/graphqlapi_introspection_query_resolver_limits.json new file mode 100644 index 000000000..fa83ad16d --- /dev/null +++ b/tests/translator/output/aws-cn/graphqlapi_introspection_query_resolver_limits.json @@ -0,0 +1,69 @@ +{ + "Resources": { + "SuperCoolAPI": { + "Properties": { + "AuthenticationType": "AWS_IAM", + "IntrospectionConfig": "DISABLED", + "LogConfig": { + "CloudWatchLogsRoleArn": { + "Fn::GetAtt": [ + "SuperCoolAPICloudWatchRole", + "Arn" + ] + }, + "FieldLogLevel": "ALL" + }, + "Name": "SuperCoolAPI", + "OwnerContact": "blah-blah", + "QueryDepthLimit": 10, + "ResolverCountLimit": 100, + "Tags": [ + { + "Key": "graphqlapi:createdBy", + "Value": "SAM" + } + ], + "Visibility": "PRIVATE" + }, + "Type": "AWS::AppSync::GraphQLApi" + }, + "SuperCoolAPICloudWatchRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "appsync.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "SuperCoolAPISchema": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "SuperCoolAPI", + "ApiId" + ] + }, + "Definition": "type Book {\n bookName: String\n} \ntype Query { getBook(bookName: String): Book }\n" + }, + "Type": "AWS::AppSync::GraphQLSchema" + } + } +} diff --git a/tests/translator/output/aws-us-gov/graphqlapi_introspection_query_resolver_limits.json b/tests/translator/output/aws-us-gov/graphqlapi_introspection_query_resolver_limits.json new file mode 100644 index 000000000..fa83ad16d --- /dev/null +++ b/tests/translator/output/aws-us-gov/graphqlapi_introspection_query_resolver_limits.json @@ -0,0 +1,69 @@ +{ + "Resources": { + "SuperCoolAPI": { + "Properties": { + "AuthenticationType": "AWS_IAM", + "IntrospectionConfig": "DISABLED", + "LogConfig": { + "CloudWatchLogsRoleArn": { + "Fn::GetAtt": [ + "SuperCoolAPICloudWatchRole", + "Arn" + ] + }, + "FieldLogLevel": "ALL" + }, + "Name": "SuperCoolAPI", + "OwnerContact": "blah-blah", + "QueryDepthLimit": 10, + "ResolverCountLimit": 100, + "Tags": [ + { + "Key": "graphqlapi:createdBy", + "Value": "SAM" + } + ], + "Visibility": "PRIVATE" + }, + "Type": "AWS::AppSync::GraphQLApi" + }, + "SuperCoolAPICloudWatchRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "appsync.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "SuperCoolAPISchema": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "SuperCoolAPI", + "ApiId" + ] + }, + "Definition": "type Book {\n bookName: String\n} \ntype Query { getBook(bookName: String): Book }\n" + }, + "Type": "AWS::AppSync::GraphQLSchema" + } + } +} diff --git a/tests/translator/output/graphqlapi_introspection_query_resolver_limits.json b/tests/translator/output/graphqlapi_introspection_query_resolver_limits.json new file mode 100644 index 000000000..fa83ad16d --- /dev/null +++ b/tests/translator/output/graphqlapi_introspection_query_resolver_limits.json @@ -0,0 +1,69 @@ +{ + "Resources": { + "SuperCoolAPI": { + "Properties": { + "AuthenticationType": "AWS_IAM", + "IntrospectionConfig": "DISABLED", + "LogConfig": { + "CloudWatchLogsRoleArn": { + "Fn::GetAtt": [ + "SuperCoolAPICloudWatchRole", + "Arn" + ] + }, + "FieldLogLevel": "ALL" + }, + "Name": "SuperCoolAPI", + "OwnerContact": "blah-blah", + "QueryDepthLimit": 10, + "ResolverCountLimit": 100, + "Tags": [ + { + "Key": "graphqlapi:createdBy", + "Value": "SAM" + } + ], + "Visibility": "PRIVATE" + }, + "Type": "AWS::AppSync::GraphQLApi" + }, + "SuperCoolAPICloudWatchRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "appsync.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "SuperCoolAPISchema": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "SuperCoolAPI", + "ApiId" + ] + }, + "Definition": "type Book {\n bookName: String\n} \ntype Query { getBook(bookName: String): Book }\n" + }, + "Type": "AWS::AppSync::GraphQLSchema" + } + } +}