Skip to content

Conversation

harrisonhjones
Copy link
Contributor

@harrisonhjones harrisonhjones commented Jan 2, 2021

This is a work in progress and does not yet contain tests. I am sending it out at this stage to get feedback on my approach. This is an alternate approach from the other PR I sent out, #1876. The main difference between that PR and this one is that this code change adds a new special Authorizer built in called AWS_IAM that cannot be defined in the Authorizers section of an HTTP API.

Issue #, if available: #1731

Description of changes:

This code change does one thing:

  1. Adds a new built-in authorizer, AWS_IAM, to be used by HTTP APIs.

Description of how you validated changes:

I created the following test template:

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
  #######
  # Serverless functions that use a manually-created AWS::Serverless::HttpApi with the default auth set to the IAM authorizer.
  #######
  # Should have IAM auth set because the default authorizer is the IAM authorizer.
  HttpApiFunctionCustomApiWithDefaultIamAuthDefaultAuth:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /custom-api-with-default-iam-auth/default-auth
            Method: GET
            ApiId: !Ref CustomServerlessHttpApiWithDefaultIamAuth
      Handler: index.handler
      InlineCode:
        "def handler(event, context):\n    return {'body':
        'HttpApiFunctionCustomApiWithDefaultIamAuthDefaultAuth', 'statusCode':
        200}\n"
      Runtime: python3.8
  # Should have no auth.
  HttpApiFunctionCustomApiWithDefaultIamAuthNoneAuth:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /custom-api-with-default-iam-auth/none-auth
            Method: GET
            ApiId: !Ref CustomServerlessHttpApiWithDefaultIamAuth
            Auth:
              Authorizer: NONE
      Handler: index.handler
      InlineCode:
        "def handler(event, context):\n    return {'body':
        'HttpApiFunctionCustomApiWithDefaultIamAuthNoneAuth', 'statusCode':
        200}\n"
      Runtime: python3.8
  # Should have IAM auth set because the authorizer is set to the IAM authorizer.
  HttpApiFunctionCustomApiWithDefaultIamAuthIamAuth:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /custom-api-with-default-iam-auth/iam-auth
            Method: GET
            ApiId: !Ref CustomServerlessHttpApiWithDefaultIamAuth
            Auth:
              Authorizer: AWS_IAM
      Handler: index.handler
      InlineCode:
        "def handler(event, context):\n    return {'body':
        'HttpApiFunctionCustomApiWithDefaultIamAuthIamAuth', 'statusCode':
        200}\n"
      Runtime: python3.8
  # Serverless Http Api with an IAM authorizer (default) and a None authorizer.
  CustomServerlessHttpApiWithDefaultIamAuth:
    Type: AWS::Serverless::HttpApi
    Properties:
      FailOnWarnings: true
      Auth:
        DefaultAuthorizer: AWS_IAM

  #######
  # Serverless functions that use a manually-created AWS::Serverless::HttpApi with the default auth unset.
  #######
  # Should have no auth set because the default authorizer is not set.
  HttpApiFunctionCustomApiWithNoDefaultAuthDefaultAuth:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /custom-api-with-no-default-auth/default-auth
            Method: GET
            ApiId: !Ref CustomServerlessHttpApiWithNoDefault
      Handler: index.handler
      InlineCode:
        "def handler(event, context):\n    return {'body':
        'HttpApiFunctionCustomApiWithNoDefaultAuthDefaultAuth', 'statusCode':
        200}\n"
      Runtime: python3.8
  # Should have IAM auth set because the authorizer is set to the IAM authorizer.
  HttpApiFunctionCustomApiWithNoDefaultIamAuth:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /custom-api-with-no-default-auth/iam-auth
            Method: GET
            ApiId: !Ref CustomServerlessHttpApiWithNoDefault
            Auth:
              Authorizer: AWS_IAM
      Handler: index.handler
      InlineCode:
        "def handler(event, context):\n    return {'body':
        'HttpApiFunctionCustomApiWithNoDefaultIamAuth', 'statusCode': 200}\n"
      Runtime: python3.8
  # Serverless Http Api with an IAM authorizer and a None authorizer.
  CustomServerlessHttpApiWithNoDefault:
    Type: AWS::Serverless::HttpApi
    Properties:
      FailOnWarnings: true

I then compiled it using:

python ./bin/sam-translate.py --template-file=../project/template.yml

I then manually deployed it using CloudFormation in my personal AWS account.

Once deployed I validated all the routes had the expected auth type:

  1. https://8gs6je9808.execute-api.us-east-1.amazonaws.com/custom-api-with-no-default-auth/default-auth - has no auth
  2. https://8gs6je9808.execute-api.us-east-1.amazonaws.com/custom-api-with-no-default-auth/iam-auth - has IAM auth
  3. https://e65p0kxj50.execute-api.us-east-1.amazonaws.com/default-api/default-auth - has no auth
  4. https://e65p0kxj50.execute-api.us-east-1.amazonaws.com/default-api/iam-auth - has IAM auth
  5. https://ykor2hq2i0.execute-api.us-east-1.amazonaws.com/custom-api-with-default-iam-auth/default-auth - has IAM auth
  6. https://ykor2hq2i0.execute-api.us-east-1.amazonaws.com/custom-api-with-default-iam-auth/iam-auth - has IAM auth
  7. https://ykor2hq2i0.execute-api.us-east-1.amazonaws.com/custom-api-with-default-iam-auth/none-auth - has no auth

Checklist:

  • Write/update tests
  • make pr passes
  • Update documentation
  • Verify transformed template deploys and application functions as expected

Examples?

Please reach out in the comments, if you want to add an example. Examples will be
added to sam init through https://github.com/awslabs/aws-sam-cli-app-templates/

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Copy link
Contributor

@jfuss jfuss left a comment

Choose a reason for hiding this comment

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

@harrisonhjones So I am having a hard time parsing what is going on here and how it fits into what we already have. Can you explain what you are trying to add here? I know it is IAM Auth for HttpApi, but it's hard to understand how this works at a high level, why this is different than our other Auth's, what defaults (if any) are changing, etc. The issue doesn't seem to have any approach explained and bouncing between two different PRs to try to understand what is different/being added makes things difficult to understand. I don't think we have a design template within SAM, like we do in SAM CLI, but thinking it could be worth it for features that are more involved or require some context.

Comment on lines +415 to +416
# 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.
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.

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.

if not self.auth:
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.

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.

Copy link
Contributor Author

@harrisonhjones harrisonhjones left a comment

Choose a reason for hiding this comment

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

@harrisonhjones So I am having a hard time parsing what is going on here and how it fits into what we already have. Can you explain what you are trying to add here? I know it is IAM Auth for HttpApi, but it's hard to understand how this works at a high level, why this is different than our other Auth's, what defaults (if any) are changing, etc. The issue doesn't seem to have any approach explained and bouncing between two different PRs to try to understand what is different/being added makes things difficult to understand. I don't think we have a design template within SAM, like we do in SAM CLI, but thinking it could be worth it for features that are more involved or require some context.

Sure thing. The goal of this and PR #1876 is to add AWS IAM authorizer support to Serverless functions and HTTP APIs. By "support" I mean customers will be able to configure their individual Serverless functions to use AWS IAM auth and their Serverless HTTP APIs to use AWS IAM auth by default. The two PRs do it in two different ways:

PR 1876

#1876

PR 1876 accomplishes the goal by defining a new authorizer property, is_aws_iam_authorizer which is set using the new IamAuthorizer key. If set on an authorizer, regardless of any other properties, the authorizer is setup as an IAM authorizer. Customers have to define the authorizer themselves if they are using a custom Serverless HTTP (not the default on). They can call it whatever they want and to use it they need to reference it in their Serverless functions. For example:

  HttpApiFunctionCustomApiWithDefaultIamAuthIamAuth:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /custom-api-with-default-iam-auth/iam-auth
            Method: GET
            ApiId: !Ref CustomServerlessHttpApiWithDefaultIamAuth
            Auth:
              Authorizer: CustomIamAuthorizer
      ... other properties ...
  CustomServerlessHttpApiWithDefaultIamAuth:
    Type: AWS::Serverless::HttpApi
    Properties:
      Auth:
        DefaultAuthorizer: CustomIamAuthorizer
        Authorizers:
          CustomIamAuthorizer:
            IamAuthorizer: True

The PR then goes a bit further to add an IAM authorizer to the default Serverless HTTP API (called the ImplicitHttpApiResource) so that customers that use that with their Serverless functions can enable IAM auth without having to define their own Serverless HTTP API.

PR 1878 (this one)

#1878

PR 1878 works differently. It always adds a AWS_IAM security scheme to all generated Serverless HTTP APIs. Customers do not need to define any authorizers to use IAM auth, they simply need to reference AWS_IAM in their Serverless function definitions or as the default auth for their Serverless HTTP API definitions.

Closing

I hope that helps explain my approaches. If it would help move things quicker I'm happy to jump on a call to discuss.

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 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.

if not self.auth:
return

if self.auth and not 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.

See my response above.

Comment on lines +415 to +416
# 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.
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.

"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.

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 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

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 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.

@jfuss
Copy link
Contributor

jfuss commented Mar 30, 2021

Closing in favor of #1924

@jfuss jfuss closed this Mar 30, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants