Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 32 additions & 18 deletions samtranslator/model/api/http_api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,34 +397,45 @@ def _construct_alias_target(self, domain):

def _add_auth(self):
"""
Add Auth configuration to the OAS file, if necessary
Add Auth configuration to the OAS file. In order to support the built-in AWS_IAM authorizer it is always added.
"""
if not self.auth:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this changes behavior. That is the self.definition_body = open_api_editor.openapi is set, which will risk breaking existing customers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My goal here was to allow customers to specify the IAM_AUTH authorizer even if they didn't have any auth defined in their serverless http api. If you look at my first example, in the converstaion tab, I illustrated turning on IAM auth for a serverless function using the default serverless http api:

AWSTemplateFormatVersion: "2010-09-09"
Description: AWS SAM template with a simple API definition
Transform: AWS::Serverless-2016-10-31
Resources:
  #######
  # Serverless functions that use the automatically-created AWS::Serverless::HttpApi called "ServerlessHttpApi".
  #######
  # Should have no auth set.
  HttpApiFunctionDefaultApiDefaultAuth:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /default-api/default-auth
            Method: GET
      Handler: index.handler
      InlineCode:
        "def handler(event, context):\n    return {'body':
        'HttpApiFunctionDefaultApiDefaultAuth', 'statusCode': 200}\n"
      Runtime: python3.8
  # Should have IAM auth set.
  HttpApiFunctionDefaultApiIAMAuth:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /default-api/iam-auth
            Method: GET
            Auth:
              Authorizer: AWS_IAM
      Handler: index.handler
      InlineCode:
        "def handler(event, context):\n    return {'body':
        'HttpApiFunctionDefaultApiIAMAuth', 'statusCode': 200}\n"
      Runtime: python3.8

Now, we could keep the if not self.auth checks. If we did that customers that wanted to use the IAM authorizer would need to define any other authorizer and potentially leave it unused to gain access to the IAM authorizer. That seems fairly hacky to me which I'd like to avoid.

return

if self.auth and not self.definition_body:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Why remove self.auth check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my response above.

if not self.definition_body:
raise InvalidResourceException(
self.logical_id, "Auth works only with inline OpenApi specified in the 'DefinitionBody' property."
)

# Make sure keys in the dict are recognized
if not all(key in AuthProperties._fields for key in self.auth.keys()):
raise InvalidResourceException(self.logical_id, "Invalid value for 'Auth' property")

if not OpenApiEditor.is_valid(self.definition_body):
raise InvalidResourceException(
self.logical_id,
"Unable to add Auth configuration because 'DefinitionBody' does not contain a valid OpenApi definition.",
)

open_api_editor = OpenApiEditor(self.definition_body)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking back at this line I'm now wondering if this line populates the open_api_editor with the self.definition_body. If so then my new code, specifically the open_api_editor.security_schemes["AWS_IAM"] = { line could still clobber an existing definition which I'd like to avoid. Likely we'll need to add something like:

if not "AWS_IAM" in open_api_editor.security_schemes {
   open_api_editor.security_schemes["AWS_IAM"] = {
      ...
   }
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether this information helps or not - but in my pursuit of trying to work how to enable AWS_IAM via OpenAPI this morning, (obviously I did not succeed (edit: I did later)), I did the following.

  • Created an API (HttpApi) with no auth, via an OpenAPI file.
  • Added IAM auth to an endpoint
  • Exported the OpenAPI template

Under security schemes, it generated this-

  securitySchemes:
    sigv4:
      type: "apiKey"
      name: "Authorization"
      in: "header"
      x-amazon-apigateway-authtype: "awsSigv4"

The affected path now looked like this

  /mypath:
    post:
      operationId: "MyPath"
      responses:
        default:
          description: "Default response for POST /mypath"
      security:
      - sigv4: []
      x-amazon-apigateway-integration:
        payloadFormatVersion: "2.0"
        type: "aws_proxy"
        httpMethod: "POST"
        uri: <redacted>"
        connectionType: "INTERNET"

Similarly I got errors when I tried to recreate using the exported template. An Authorizer cannot be used with AuthorizationType AWS_IAM. Ignoring.

So it seems like by default it calls it an IAM authorizer in the scheme as sigv4, with the awsSigv4 type.

EDIT: I just tried attaching an IAM authorizer via OpenAPI and it worked - so it likes like you can configure an IAM authorizer by using Open API. I'm not sure what I did differently to this morning....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: I just tried attaching an IAM authorizer via OpenAPI and it worked - so it likes like you can configure an IAM authorizer by using Open API. I'm not sure what I did differently to this morning....

Yep, you can do it that way. This PR is proposing a way to do it without having to touch OpenAPI.

auth_properties = AuthProperties(**self.auth)
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer)

# authorizers is guaranteed to return a value or raise an exception
open_api_editor.add_authorizers_security_definitions(authorizers)
self._set_default_authorizer(
open_api_editor, authorizers, auth_properties.DefaultAuthorizer, auth_properties.Authorizers
)
# To remain backwards compatible add the built-in "AWS_IAM" security scheme _before_ adding the authorizers defined
# in the template so that if the template already has an authorized named "AWS_IAM" it will override the built-in one.
Comment on lines +415 to +416
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wouldn't we add this only if a customer wants AWS_IAM auth?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal here is that if a customer somehow already has an authorizer called AWS_IAM we wouldn't want to clobber it.

open_api_editor.security_schemes["AWS_IAM"] = {
"type": "apiKey",
"name": "Authorization",
"in": "header",
"x-amazon-apigateway-authtype": "awsSigv4",
}

# If auth is defined in the template validate it, set the default authorizer, and add any specified authorizers.
if self.auth:
# Make sure keys in the dict are recognized
if not all(key in AuthProperties._fields for key in self.auth.keys()):
raise InvalidResourceException(self.logical_id, "Invalid value for 'Auth' property")

auth_properties = AuthProperties(**self.auth)
# authorizers is guaranteed to return a value or raise an exception
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer)

open_api_editor.add_authorizers_security_definitions(authorizers)
self._set_default_authorizer(
open_api_editor, authorizers, auth_properties.DefaultAuthorizer, auth_properties.Authorizers
)

self.definition_body = open_api_editor.openapi

def _add_tags(self):
Expand Down Expand Up @@ -468,7 +479,8 @@ def _set_default_authorizer(self, open_api_editor, authorizers, default_authoriz
if not default_authorizer:
return

if not authorizers.get(default_authorizer):
# The AWS_IAM authorizer is built-in and does not need to be defined in the template as an authorizer.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We require this for all other Auths. Why should AWS_IAM be any different?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because AWS_IAM is a "built in" auth type. It should not be customized and there only ever needs to be a single one of them. That said I did take the approach you are suggesting in my alternate implementation: #1876

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we already have a precedent for another built-in authorizer: NONE. The NONE authorizer could have been implemented as some kind of authorizer you had to define but which did nothing, which would make it like all the other authorizers, but instead it was defined as a special built-in authorizer. I implemented something similar with the AWS_IAM authorizer.

if not authorizers.get(default_authorizer) and default_authorizer != "AWS_IAM":
raise InvalidResourceException(
self.logical_id,
"Unable to set DefaultAuthorizer because '"
Expand All @@ -488,8 +500,10 @@ def _get_authorizers(self, authorizers_config, default_authorizer=None):
:param default_authorizer: name of the default authorizer
"""
authorizers = {}

if not isinstance(authorizers_config, dict):
# The AWS_IAM authorizer is built-in and does not need to be defined in the template as an authorizer.
if default_authorizer == "AWS_IAM":
return authorizers
raise InvalidResourceException(self.logical_id, "Authorizers must be a dictionary.")

for authorizer_name, authorizer in authorizers_config.items():
Expand Down
9 changes: 7 additions & 2 deletions samtranslator/model/eventsources/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,8 @@ def _add_auth_to_openapi_integration(self, api, editor):
# Default auth should already be applied, so apply any other auth here or scope override to default
api_authorizers = api_auth and api_auth.get("Authorizers")

if method_authorizer != "NONE" and not api_authorizers:
# "NONE" and "AWS_IAM" are placeholder authorizer values so they can be safely skipped.
if method_authorizer != "NONE" and method_authorizer != "AWS_IAM" and not api_authorizers:
raise InvalidEventException(
self.relative_id,
"Unable to set Authorizer [{authorizer}] on API method [{method}] for path [{path}] "
Expand All @@ -1176,7 +1177,11 @@ def _add_auth_to_openapi_integration(self, api, editor):
),
)

if method_authorizer != "NONE" and not api_authorizers.get(method_authorizer):
if (
method_authorizer != "NONE"
and method_authorizer != "AWS_IAM"
and not api_authorizers.get(method_authorizer)
):
raise InvalidEventException(
self.relative_id,
"Unable to set Authorizer [{authorizer}] on API method [{method}] for path [{path}] "
Expand Down
2 changes: 1 addition & 1 deletion samtranslator/open_api/open_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers
security_dict = dict()
security_dict[authorizer_name] = []

if authorizer_name != "NONE":
if authorizer_name != "NONE" and authorizer_name != "AWS_IAM":
method_authorization_scopes = authorizers[authorizer_name].get("AuthorizationScopes")
if authorization_scopes:
method_authorization_scopes = authorization_scopes
Expand Down