|
| 1 | +from collections import namedtuple |
| 2 | +from six import string_types |
| 3 | +from samtranslator.model.intrinsics import ref |
| 4 | +from samtranslator.model.apigatewayv2 import ApiGatewayV2HttpApi, ApiGatewayV2Stage, ApiGatewayV2Authorizer |
| 5 | +from samtranslator.model.exceptions import InvalidResourceException |
| 6 | +from samtranslator.model.s3_utils.uri_parser import parse_s3_uri |
| 7 | +from samtranslator.open_api.open_api import OpenApiEditor |
| 8 | +from samtranslator.translator import logical_id_generator |
| 9 | +from samtranslator.model.tags.resource_tagging import get_tag_list |
| 10 | + |
| 11 | +AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer"]) |
| 12 | +AuthProperties.__new__.__defaults__ = (None, None) |
| 13 | +DefaultStageName = "$default" |
| 14 | + |
| 15 | + |
| 16 | +class HttpApiGenerator(object): |
| 17 | + |
| 18 | + def __init__(self, logical_id, stage_variables, depends_on, definition_body, definition_uri, |
| 19 | + stage_name, tags=None, auth=None, access_log_settings=None, |
| 20 | + resource_attributes=None, passthrough_resource_attributes=None): |
| 21 | + """Constructs an API Generator class that generates API Gateway resources |
| 22 | +
|
| 23 | + :param logical_id: Logical id of the SAM API Resource |
| 24 | + :param stage_variables: API Gateway Variables |
| 25 | + :param depends_on: Any resources that need to be depended on |
| 26 | + :param definition_body: API definition |
| 27 | + :param definition_uri: URI to API definition |
| 28 | + :param name: Name of the API Gateway resource |
| 29 | + :param stage_name: Name of the Stage |
| 30 | + :param tags: Stage and API Tags |
| 31 | + :param access_log_settings: Whether to send access logs and where for Stage |
| 32 | + :param resource_attributes: Resource attributes to add to API resources |
| 33 | + :param passthrough_resource_attributes: Attributes such as `Condition` that are added to derived resources |
| 34 | + """ |
| 35 | + self.logical_id = logical_id |
| 36 | + self.stage_variables = stage_variables |
| 37 | + self.depends_on = depends_on |
| 38 | + self.definition_body = definition_body |
| 39 | + self.definition_uri = definition_uri |
| 40 | + self.stage_name = stage_name |
| 41 | + if not self.stage_name: |
| 42 | + self.stage_name = DefaultStageName |
| 43 | + self.auth = auth |
| 44 | + self.tags = tags |
| 45 | + self.access_log_settings = access_log_settings |
| 46 | + self.resource_attributes = resource_attributes |
| 47 | + self.passthrough_resource_attributes = passthrough_resource_attributes |
| 48 | + |
| 49 | + def _construct_http_api(self): |
| 50 | + """Constructs and returns the ApiGatewayV2 HttpApi. |
| 51 | +
|
| 52 | + :returns: the HttpApi to which this SAM Api corresponds |
| 53 | + :rtype: model.apigatewayv2.ApiGatewayHttpApi |
| 54 | + """ |
| 55 | + http_api = ApiGatewayV2HttpApi(self.logical_id, depends_on=self.depends_on, attributes=self.resource_attributes) |
| 56 | + |
| 57 | + if self.definition_uri and self.definition_body: |
| 58 | + raise InvalidResourceException(self.logical_id, |
| 59 | + "Specify either 'DefinitionUri' or 'DefinitionBody' property and not both") |
| 60 | + |
| 61 | + self._add_auth() |
| 62 | + |
| 63 | + if self.definition_uri: |
| 64 | + http_api.BodyS3Location = self._construct_body_s3_dict() |
| 65 | + elif self.definition_body: |
| 66 | + http_api.Body = self.definition_body |
| 67 | + else: |
| 68 | + raise InvalidResourceException(self.logical_id, |
| 69 | + "'DefinitionUri' or 'DefinitionBody' are required properties of an " |
| 70 | + "'AWS::Serverless::HttpApi'. Add a value for one of these properties or " |
| 71 | + "add a 'HttpApi' event to an 'AWS::Serverless::Function'") |
| 72 | + |
| 73 | + if self.tags is not None: |
| 74 | + http_api.Tags = get_tag_list(self.tags) |
| 75 | + |
| 76 | + return http_api |
| 77 | + |
| 78 | + def _add_auth(self): |
| 79 | + """ |
| 80 | + Add Auth configuration to the OAS file, if necessary |
| 81 | + """ |
| 82 | + if not self.auth: |
| 83 | + return |
| 84 | + |
| 85 | + if self.auth and not self.definition_body: |
| 86 | + raise InvalidResourceException(self.logical_id, |
| 87 | + "Auth works only with inline Swagger specified in " |
| 88 | + "'DefinitionBody' property") |
| 89 | + |
| 90 | + # Make sure keys in the dict are recognized |
| 91 | + if not all(key in AuthProperties._fields for key in self.auth.keys()): |
| 92 | + raise InvalidResourceException( |
| 93 | + self.logical_id, "Invalid value for 'Auth' property") |
| 94 | + |
| 95 | + if not OpenApiEditor.is_valid(self.definition_body): |
| 96 | + raise InvalidResourceException(self.logical_id, "Unable to add Auth configuration because " |
| 97 | + "'DefinitionBody' does not contain a valid Swagger") |
| 98 | + open_api_editor = OpenApiEditor(self.definition_body) |
| 99 | + auth_properties = AuthProperties(**self.auth) |
| 100 | + authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer) |
| 101 | + |
| 102 | + # authorizers is guaranteed to return a value or raise an exception |
| 103 | + open_api_editor.add_authorizers_security_definitions(authorizers) |
| 104 | + self._set_default_authorizer(open_api_editor, authorizers, auth_properties.DefaultAuthorizer, |
| 105 | + auth_properties.Authorizers) |
| 106 | + self.definition_body = open_api_editor.openapi |
| 107 | + |
| 108 | + def _set_default_authorizer(self, open_api_editor, authorizers, default_authorizer, api_authorizers): |
| 109 | + """ |
| 110 | + Sets the default authorizer if one is given in the template |
| 111 | + :param open_api_editor: editor object that contains the OpenApi definition |
| 112 | + :param authorizers: authorizer definitions converted from the API auth section |
| 113 | + :param default_authorizer: name of the default authorizer |
| 114 | + :param api_authorizers: API auth section authorizer defintions |
| 115 | + """ |
| 116 | + if not default_authorizer: |
| 117 | + return |
| 118 | + |
| 119 | + if not authorizers.get(default_authorizer): |
| 120 | + raise InvalidResourceException(self.logical_id, "Unable to set DefaultAuthorizer because '" + |
| 121 | + default_authorizer + "' was not defined in 'Authorizers'") |
| 122 | + |
| 123 | + for path in open_api_editor.iter_on_path(): |
| 124 | + open_api_editor.set_path_default_authorizer(path, default_authorizer, authorizers=authorizers, |
| 125 | + api_authorizers=api_authorizers) |
| 126 | + |
| 127 | + def _get_authorizers(self, authorizers_config, default_authorizer=None): |
| 128 | + """ |
| 129 | + Returns all authorizers for an API as an ApiGatewayV2Authorizer object |
| 130 | + :param authorizers_config: authorizer configuration from the API Auth section |
| 131 | + :param default_authorizer: name of the default authorizer |
| 132 | + """ |
| 133 | + authorizers = {} |
| 134 | + |
| 135 | + if not isinstance(authorizers_config, dict): |
| 136 | + raise InvalidResourceException(self.logical_id, |
| 137 | + "Authorizers must be a dictionary") |
| 138 | + |
| 139 | + for authorizer_name, authorizer in authorizers_config.items(): |
| 140 | + if not isinstance(authorizer, dict): |
| 141 | + raise InvalidResourceException(self.logical_id, |
| 142 | + "Authorizer %s must be a dictionary." % (authorizer_name)) |
| 143 | + |
| 144 | + authorizers[authorizer_name] = ApiGatewayV2Authorizer( |
| 145 | + api_logical_id=self.logical_id, |
| 146 | + name=authorizer_name, |
| 147 | + open_id_connect_url=authorizer.get('OpenIdConnectUrl'), |
| 148 | + authorization_scopes=authorizer.get('AuthorizationScopes'), |
| 149 | + jwt_configuration=authorizer.get('JwtConfiguration'), |
| 150 | + id_source=authorizer.get('IdentitySource') |
| 151 | + ) |
| 152 | + return authorizers |
| 153 | + |
| 154 | + def _construct_body_s3_dict(self): |
| 155 | + """ |
| 156 | + Constructs the HttpApi's `BodyS3Location property`, from the SAM Api's DefinitionUri property. |
| 157 | + :returns: a BodyS3Location dict, containing the S3 Bucket, Key, and Version of the OpenApi definition |
| 158 | + :rtype: dict |
| 159 | + """ |
| 160 | + if isinstance(self.definition_uri, dict): |
| 161 | + if not self.definition_uri.get("Bucket", None) or not self.definition_uri.get("Key", None): |
| 162 | + # DefinitionUri is a dictionary but does not contain Bucket or Key property |
| 163 | + raise InvalidResourceException(self.logical_id, |
| 164 | + "'DefinitionUri' requires Bucket and Key properties to be specified") |
| 165 | + s3_pointer = self.definition_uri |
| 166 | + |
| 167 | + else: |
| 168 | + # DefinitionUri is a string |
| 169 | + s3_pointer = parse_s3_uri(self.definition_uri) |
| 170 | + if s3_pointer is None: |
| 171 | + raise InvalidResourceException(self.logical_id, |
| 172 | + '\'DefinitionUri\' is not a valid S3 Uri of the form ' |
| 173 | + '"s3://bucket/key" with optional versionId query parameter.') |
| 174 | + |
| 175 | + body_s3 = { |
| 176 | + 'Bucket': s3_pointer['Bucket'], |
| 177 | + 'Key': s3_pointer['Key'] |
| 178 | + } |
| 179 | + if 'Version' in s3_pointer: |
| 180 | + body_s3['Version'] = s3_pointer['Version'] |
| 181 | + return body_s3 |
| 182 | + |
| 183 | + def _construct_stage(self): |
| 184 | + """Constructs and returns the ApiGatewayV2 Stage. |
| 185 | +
|
| 186 | + :returns: the Stage to which this SAM Api corresponds |
| 187 | + :rtype: model.apigatewayv2.ApiGatewayV2Stage |
| 188 | + """ |
| 189 | + |
| 190 | + # If there are no special configurations, don't create a stage and use the default |
| 191 | + if not self.stage_name and not self.stage_variables and not self.access_log_settings: |
| 192 | + return |
| 193 | + |
| 194 | + # If StageName is some intrinsic function, then don't prefix the Stage's logical ID |
| 195 | + # This will NOT create duplicates because we allow only ONE stage per API resource |
| 196 | + stage_name_prefix = self.stage_name if isinstance(self.stage_name, string_types) else "" |
| 197 | + if stage_name_prefix.isalnum(): |
| 198 | + stage_logical_id = self.logical_id + stage_name_prefix + "Stage" |
| 199 | + elif stage_name_prefix == DefaultStageName: |
| 200 | + stage_logical_id = self.logical_id + "ApiGatewayDefaultStage" |
| 201 | + else: |
| 202 | + generator = logical_id_generator.LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix) |
| 203 | + stage_logical_id = generator.gen() |
| 204 | + stage = ApiGatewayV2Stage(stage_logical_id, |
| 205 | + attributes=self.passthrough_resource_attributes) |
| 206 | + stage.ApiId = ref(self.logical_id) |
| 207 | + stage.StageName = self.stage_name |
| 208 | + stage.StageVariables = self.stage_variables |
| 209 | + stage.AccessLogSettings = self.access_log_settings |
| 210 | + stage.AutoDeploy = True |
| 211 | + |
| 212 | + if self.tags is not None: |
| 213 | + stage.Tags = get_tag_list(self.tags) |
| 214 | + |
| 215 | + return stage |
| 216 | + |
| 217 | + def to_cloudformation(self): |
| 218 | + """Generates CloudFormation resources from a SAM API resource |
| 219 | +
|
| 220 | + :returns: a tuple containing the HttpApi and Stage for an empty Api. |
| 221 | + :rtype: tuple |
| 222 | + """ |
| 223 | + http_api = self._construct_http_api() |
| 224 | + |
| 225 | + stage = self._construct_stage() |
| 226 | + |
| 227 | + return http_api, stage |
0 commit comments