Code Monkey home page Code Monkey logo

aws-apigateway-lambda-authorizer-blueprints's Introduction

Amazon API Gateway - Custom Authorizer Blueprints for AWS Lambda

We've added blueprints and examples in 3 languages for Lambda-based custom Authorizers for use in API Gateway.

Java

Not available in the Lambda console. Use the AuthPolicy object to generate and serialize IAM policies for your custom authorizer. See javadoc comments for more details.

NodeJS

Also available in the Lambda console, the NodeJS blueprint makes it easy to generate IAM policies, including Conditions.

Python

Also available in the Lambda console, the Python blueprint includes the AuthPolicy class, which makes generating IAM policies simple and easy to understand.

Go

Not available in the Lambda console. Use the AuthorizerResponse object to generate IAM policies for your custom authorizer. See comments for more details.

Rust

Not available in the Lambda console. Using awslabs/aws-lambda-rust-runtime. Use the APIGatewayPolicyBuilder object to generate IAM policies for your custom authorizer. See comments for more details.

Docs

For more details, see public documentation for:

aws-apigateway-lambda-authorizer-blueprints's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

aws-apigateway-lambda-authorizer-blueprints's Issues

policy error

Resource should not be [] it is string , that causes user not access the resource
{
"principalId": "user",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Deny",
"Resource": "arn:aws:execute-api:us-west-2:123456789012:ymy8tbxw7b/*/GET/"
}
]
}
}

Rust blueprint serialization issue

With the rust code, when building a policy the add_method uses this code to serialize the Method enum

serde_json::to_string(&method).unwrap()

this creates a policy with double quotes in the method ARN
"arn:aws:execute-api:ap-southeast-2:XXXXXXXXXX:YYYYYYYYYY/ESTestInvoke-stage/\"*\"/*"

Consider using something like this instead ?

json!(&method).as_str().unwrap()

Stage is null when test in API Gateway -> Authorizers

When I test in AWS console -> API Gateway -> Authorizers -> my-authorizer with correct token, I receive following policy document:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "execute-api:Invoke",
      "Effect": "Allow",
      "Resource": [
        "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/null/*/*"
      ]
    },
    {
      "Action": "execute-api:Invoke",
      "Effect": "Deny",
      "Resource": [
        "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/null/DELETE/locations",
        "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/null/DELETE/locations/*",
        "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/null/POST/locations",
        "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/null/POST/locations/*"
      ]
    }
  ]
}

As one can see the apiGatewayArnPartials arrives as "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/null//", so,

String stage = apiGatewayArnPartials[1];

parses to null, and all resources receive null value, e.g. "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/null/POST/locations"
I even tried to work around by doing this:

stage = ((stage == null) ||(stage.equals("")))? "*":stage;

but it changed nothing.
When I test same authorizer, but from Lambda (AWS console -> Lambda -> Functions -> my-authorizer), everything looks fine (what makes me think that the issue is API Gateway specific):

{
  "principalId": "68372295-99bf-4259-b168-b8f702f4fb10",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Allow",
        "Resource": [
          "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/*/*/*"
        ]
      },
      {
        "Action": "execute-api:Invoke",
        "Effect": "Deny",
        "Resource": [
          "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/*/DELETE/locations",
          "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/*/DELETE/locations/*",
          "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/*/POST/locations",
          "arn:aws:execute-api:us-west-2:hidden:3y5h5l33u1/*/POST/locations/*"
        ]
      }
    ]
  }
}

This may not be an issue of the blueprints and rather AWS API Gateway internal issue. Please refer me to the right place where I can register it or address it internally to the team if so.

Upgrade dotnet core to use netcoreapp3.1

Since dotnet 1.0 is not officially supported (based on what the console shows), it would help future users of this if the code is upgraded to use netcoreapp3.1. I have the 3.1 version of this app deployed and working, Can I send a pull request with that version?

image

Failed invocations when returning Unauthorized

Hello, thank you for the blueprint.
I'm using the nodejs example.

When I want to return a 401 e.g. for expired tokens
callback("Unauthorized", null); will end the lambda with a Failed Invocation.
We have Alarms on all our Lambdas to get notified on Failed Invocations, and now we get notified on all 401.

How do you guys monitor your Lambda Authorizer ?

Thanks

Unit tests

Can we have some unit tests? The logic has quite a lot of conditions.

Error in nodejs blueprint?

Hi,

I just tried the nodejs blueprint and I think I've come across an error.

When parsing event.methodArn, the apiGatewayArnTmp is assigned via:
tmp[5].split('/')

Later on (on line 44), when getting the resource, you take apiGatewayArnTmp[3].
If the resource has a slash in it (which it is likely to do since it's a path), you'll only get the first part of the path:

ARN: arn:aws:execute-api:eu-west-1:1234567890:abc123/*/GET/test/deep/path
Yeilds: resource: /test when it should be /test/deep/path

Changing line 43-45 to the following should fix this issue:

if (apiGatewayArnTmp[3]) {
    resource += apiGatewayArnTmp.slice(3, apiGatewayArnTmp.length).join('/');
}

This will correctly yield resource: /test/deep/path

Using IAM client to retrieve policy document?

In Java blue print,
public Map<String, Object> getPolicyDocument() {}

I am wondering if retrieving policy document using IAM sdk would be easy option? why is it not preferred here?
For example AmazonIdentityManagementClient.GetRolePolicyResult() .getPolicyDocument()

Python blueprint doesn't work for China regions

I'm an AWS SA based in China. My customer used the python blueprint to try out authorizer in Beijing region and it didn't work (it is indeed listed as Lambda blueprint in China region as well). It turns out the resourceArn in the returned policy should be in the format of 'arn:aws-cn:execute-api...' for China regions. So the code in question is below:
resourceArn = 'arn:aws:execute-api:{}:{}:{}/{}/{}/{}'.format(self.region, self.awsAccountId, self.restApiId, self.stage, verb, resource)
Besides, the input methodArn in China region is in the following format, which contains 'aws-cn':
{ "type": "TOKEN", "methodArn": "arn:aws-cn:execute-api:cn-north-1:182470276418:{rest_api_id}/...", "authorizationToken": "..." }

[RUST] Test via API Gateway always unauthorized

Hello and thank you for the blueprints.

I'm using the Rust blueprint and I'm trying to authorize all requests (for testing) by using allow_all_methods() but it's always returning unauthorized.

#![allow(dead_code)]
#[cfg_attr(test, macro_use)] 
extern crate lambda_runtime as lambda;
#[cfg_attr(test, macro_use)] 
extern crate serde_derive;
#[macro_use]
extern crate log;
extern crate simple_logger;
use lambda::error::HandlerError;

use serde_json::json;
use serde::{Serialize, Deserialize};

use lambda_runtime::lambda;
use std::error::Error;

static POLICY_VERSION: &str = "2012-10-17"; // override if necessary

fn handler(
    event: APIGatewayCustomAuthorizerRequest,
    _ctx: lambda::Context,
) -> Result<APIGatewayCustomAuthorizerResponse, HandlerError> {
    info!("Client token: {}", event.authorization_token);
    info!("Method ARN: {}", event.method_arn);

    // validate the incoming token
    // and produce the principal user identifier associated with the token

    // this could be accomplished in a number of ways:
    // 1. Call out to OAuth provider
    // 2. Decode a JWT token inline
    // 3. Lookup in a self-managed DB
    let principal_id = "user|a1b2c3d4";

    // you can send a 401 Unauthorized response to the client by failing like so:
    // Err(HandlerError{ msg: "Unauthorized".to_string(), backtrace: None });

    // if the token is valid, a policy must be generated which will allow or deny access to the client

    // if access is denied, the client will recieve a 403 Access Denied response
    // if access is allowed, API Gateway will proceed with the backend integration configured on the method that was called

    // this function must generate a policy that is associated with the recognized principal user identifier.
    // depending on your use case, you might store policies in a DB, or generate them on the fly

    // keep in mind, the policy is cached for 5 minutes by default (TTL is configurable in the authorizer)
    // and will apply to subsequent calls to any method/resource in the RestApi
    // made with the same token

    //the example policy below denies access to all resources in the RestApi
    let tmp: Vec<&str> = event.method_arn.split(":").collect();
    let api_gateway_arn_tmp: Vec<&str> = tmp[5].split("/").collect();
    let aws_account_id = tmp[4];
    let region = tmp[3];
    let rest_api_id = api_gateway_arn_tmp[0];
    let stage = api_gateway_arn_tmp[1];

    let policy = APIGatewayPolicyBuilder::new(region, aws_account_id, rest_api_id, stage)
        .allow_all_methods()
        .build();

    // new! -- add additional key-value pairs associated with the authenticated principal
    // these are made available by APIGW like so: $context.authorizer.<key>
    // additional context is cached
    Ok(APIGatewayCustomAuthorizerResponse {
        principal_id: principal_id.to_string(),
        policy_document: policy,
        context: json!({
        "stringKey": "stringval",
        "numberKey": 123,
        "booleanKey": true
        }),
    })
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct APIGatewayCustomAuthorizerRequest {
    #[serde(rename = "type")]
    _type: String,
    authorization_token: String,
    method_arn: String,
}

#[derive(Serialize, Deserialize)]
#[allow(non_snake_case)]
struct APIGatewayCustomAuthorizerPolicy {
    Version: String,
    Statement: Vec<IAMPolicyStatement>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct APIGatewayCustomAuthorizerResponse {
    principal_id: String,
    policy_document: APIGatewayCustomAuthorizerPolicy,
    context: serde_json::Value,
}

#[derive(Serialize, Deserialize)]
#[allow(non_snake_case)]
struct IAMPolicyStatement {
    Action: Vec<String>,
    Effect: Effect,
    Resource: Vec<String>,
}

struct APIGatewayPolicyBuilder {
    region: String,
    aws_account_id: String,
    rest_api_id: String,
    stage: String,
    policy: APIGatewayCustomAuthorizerPolicy,
}

#[derive(Serialize, Deserialize)]
enum Method {
    #[serde(rename = "GET")]
    Get,
    #[serde(rename = "POST")]
    Post,
    #[serde(rename = "*PUT")]
    Put,
    #[serde(rename = "DELETE")]
    Delete,
    #[serde(rename = "PATCH")]
    Patch,
    #[serde(rename = "HEAD")]
    Head,
    #[serde(rename = "OPTIONS")]
    Options,
    #[serde(rename = "*")]
    All,
}

#[derive(Serialize, Deserialize)]
enum Effect {
    Allow,
    Deny,
}

impl APIGatewayPolicyBuilder {
    pub fn new(
        region: &str,
        account_id: &str,
        api_id: &str,
        stage: &str,
    ) -> APIGatewayPolicyBuilder {
        Self {
            region: region.to_string(),
            aws_account_id: account_id.to_string(),
            rest_api_id: api_id.to_string(),
            stage: stage.to_string(),
            policy: APIGatewayCustomAuthorizerPolicy {
                Version: POLICY_VERSION.to_string(),
                Statement: vec![],
            },
        }
    }

    pub fn add_method<T: Into<String>>(
        mut self,
        effect: Effect,
        method: Method,
        resource: T,
    ) -> Self {
        let resource_arn = format!(
            "arn:aws:execute-api:{}:{}:{}/{}/{}/{}",
            &self.region,
            &self.aws_account_id,
            &self.rest_api_id,
            &self.stage,
            serde_json::to_string(&method).unwrap(),
            resource.into().trim_start_matches("/")
        );

        let stmt = IAMPolicyStatement {
            Effect: effect,
            Action: vec!["execute-api:Invoke".to_string()],
            Resource: vec![resource_arn],
        };

        self.policy.Statement.push(stmt);
        self
    }

    pub fn allow_all_methods(self) -> Self {
        self.add_method(Effect::Allow, Method::All, "*")
    }

    pub fn deny_all_methods(self) -> Self {
        self.add_method(Effect::Deny, Method::All, "*")
    }

    pub fn allow_method(self, method: Method, resource: String) -> Self {
        self.add_method(Effect::Allow, method, resource)
    }

    pub fn deny_method(self, method: Method, resource: String) -> Self {
        self.add_method(Effect::Deny, method, resource)
    }

    // Creates and executes a new child thread.
    pub fn build(self) -> APIGatewayCustomAuthorizerPolicy {
        self.policy
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    simple_logger::init_with_level(log::Level::Debug)?;
    lambda!(handler);

    Ok(())
}

Calling getPolicyDocument in AuthPolicy.java causing multiple statement duplicates

Calling getPolicyDocument in AuthPolicy.java causing multiple statement duplicates in the returned map. If I call it more than one time (e.g. to log some of policyDocument info or to make multiple assertions in tests) the returned Map (and list of statements of PolicyDocument) contains multiple entries of same statements. This is because getStatement() doesn't just get, it actually adds allow and deny statements on every get call.

make a blueprint for typescript

Would be nice to have a typescript blueprint.
I even took the time and made something from the node.js blueprint.
Note it should probably need to be reviewed and maybe tested first, I hope I nailed the types correctly, not sure at all about the conditions type:

import {
  APIGatewayAuthorizerEvent,
  APIGatewayAuthorizerCallback,
  APIGatewayEventDefaultAuthorizerContext,
  PolicyDocument, ConditionBlock,
  AuthResponse,
} from 'aws-lambda';

type Method = {
  resourceArn: string;
  conditions: ConditionBlock[] | null;
};

type ApiOptions = { restApiId?: any; region?: any; stage?: any; }

export const handler = (event: APIGatewayAuthorizerEvent, _context: APIGatewayEventDefaultAuthorizerContext, callback: APIGatewayAuthorizerCallback) => {
  // Do not print the auth token unless absolutely necessary
  // console.log('Client token: ' + event.authorizationToken);
  console.log('Method ARN: ' + event.methodArn);

  // validate the incoming token
  // and produce the principal user identifier associated with the token

  // this could be accomplished in a number of ways:
  // 1. Call out to OAuth provider
  // 2. Decode a JWT token inline
  // 3. Lookup in a self-managed DB
  const principalId = 'user|a1b2c3d4';

  // you can send a 401 Unauthorized response to the client by failing like so:
  // callback("Unauthorized", null);

  // if the token is valid, a policy must be generated which will allow or deny access to the client

  // if access is denied, the client will receive a 403 Access Denied response
  // if access is allowed, API Gateway will proceed with the backend integration configured on the method that was called

  // build apiOptions for the AuthPolicy
  const apiOptions:ApiOptions = {};
  const tmp = event.methodArn.split(':');
  const apiGatewayArnTmp = tmp[5].split('/');
  const awsAccountId = tmp[4];
  apiOptions.region = tmp[3];
  apiOptions.restApiId = apiGatewayArnTmp[0];
  apiOptions.stage = apiGatewayArnTmp[1];
  const method = apiGatewayArnTmp[2];
  const resource = '/'; // root resource
  if (apiGatewayArnTmp[3]) {
    resource += apiGatewayArnTmp.slice(3, apiGatewayArnTmp.length).join('/');
  }

  // this function must generate a policy that is associated with the recognized principal user identifier.
  // depending on your use case, you might store policies in a DB, or generate them on the fly

  // keep in mind, the policy is cached for 5 minutes by default (TTL is configurable in the authorizer)
  // and will apply to subsequent calls to any method/resource in the RestApi
  // made with the same token

  // the example policy below denies access to all resources in the RestApi
  const policy = new AuthPolicy(principalId, awsAccountId, apiOptions);
  policy.denyAllMethods();
  // policy.allowMethod(AuthPolicy.HttpVerb.GET, "/users/username");

  // finally, build the policy
  const authResponse = policy.build();

  // new! -- add additional key-value pairs
  // these are made available by APIGW like so: $context.authorizer.<key>
  // additional context is cached
  authResponse.context = {
    key : 'value', // $context.authorizer.key -> value
    number : 1,
    bool: true
  };
  // authResponse.context.arr = ['foo']; <- this is invalid, APIGW will not accept it
  // authResponse.context.obj = {'foo':'bar'}; <- also invalid

  callback(null, authResponse);
};

/**
 * AuthPolicy receives a set of allowed and denied methods and generates a valid
 * AWS policy for the API Gateway authorizer. The constructor receives the calling
 * user principal, the AWS account ID of the API owner, and an apiOptions object.
 * The apiOptions can contain an API Gateway RestApi Id, a region for the RestApi, and a
 * stage that calls should be allowed/denied for. For example
 * {
 *   restApiId: "xxxxxxxxxx",
 *   region: "us-east-1",
 *   stage: "dev"
 * }
 *
 * const testPolicy = new AuthPolicy("[principal user identifier]", "[AWS account id]", apiOptions);
 * testPolicy.allowMethod(AuthPolicy.HttpVerb.GET, "/users/username");
 * testPolicy.denyMethod(AuthPolicy.HttpVerb.POST, "/pets");
 * context.succeed(testPolicy.build());
 *
 * @class AuthPolicy
 * @constructor
 */
class AuthPolicy {
  public readonly awsAccountId: string;
  public readonly principalId: string;
  public readonly version: string;
  public readonly pathRegex: RegExp;
  public readonly allowMethods: Method[];
  public readonly denyMethods: Method[];
  public readonly restApiId: string;
  public readonly region: string;
  public readonly stage: string;

  private static HttpVerb: { HEAD: string; ALL: string; DELETE: string; POST: string; GET: string; OPTIONS: string; PUT: string; PATCH: string };
  /**
   * A set of existing HTTP verbs supported by API Gateway. This property is here
   * only to avoid spelling mistakes in the policy.
   *
   * @property HttpVerb
   * @type {Object}
   */
  HttpVerb = {
    GET     : "GET",
    POST    : "POST",
    PUT     : "PUT",
    PATCH   : "PATCH",
    HEAD    : "HEAD",
    DELETE  : "DELETE",
    OPTIONS : "OPTIONS",
    ALL     : "*"
  };

  constructor(principal: string, awsAccountId: string, apiOptions: ApiOptions) {
    /**
     * The AWS account id the policy will be generated for. This is used to create
     * the method ARNs.
     *
     * @property awsAccountId
     * @type {String}
     */
    this.awsAccountId = awsAccountId;

    /**
     * The principal used for the policy, this should be a unique identifier for
     * the end user.
     *
     * @property principalId
     * @type {String}
     */
    this.principalId = principal;

    /**
     * The policy version used for the evaluation. This should always be "2012-10-17"
     *
     * @property version
     * @type {String}
     * @default "2012-10-17"
     */
    this.version = "2012-10-17";

    /**
     * The regular expression used to validate resource paths for the policy
     *
     * @property pathRegex
     * @type {RegExp}
     * @default '^\/[/.a-zA-Z0-9-\*]+$'
     */
    this.pathRegex = new RegExp('^[/.a-zA-Z0-9-\*]+$');

    // these are the internal lists of allowed and denied methods. These are lists
    // of objects and each object has 2 properties: A resource ARN and a nullable
    // conditions statement.
    // the build method processes these lists and generates the appropriate
    // statements for the final policy
    this.allowMethods = [];
    this.denyMethods = [];

    if (!apiOptions || !apiOptions.restApiId) {
      // Replace the placeholder value with a default API Gateway API id to be used in the policy.
      // Beware of using '*' since it will not simply mean any API Gateway API id, because stars will greedily expand over '/' or other separators.
      // See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_resource.html for more details.
      this.restApiId = "<<restApiId>>";
    } else {
      this.restApiId = apiOptions.restApiId;
    }
    if (!apiOptions || !apiOptions.region) {
      // Replace the placeholder value with a default region to be used in the policy.
      // Beware of using '*' since it will not simply mean any region, because stars will greedily expand over '/' or other separators.
      // See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_resource.html for more details.
      this.region = "<<region>>";
    } else {
      this.region = apiOptions.region;
    }
    if (!apiOptions || !apiOptions.stage) {
      // Replace the placeholder value with a default stage to be used in the policy.
      // Beware of using '*' since it will not simply mean any stage, because stars will greedily expand over '/' or other separators.
      // See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_resource.html for more details.
      this.stage = "<<stage>>";
    } else {
      this.stage = apiOptions.stage;
    }
  }

  /**
   * Adds a method to the internal lists of allowed or denied methods. Each object in
   * the internal list contains a resource ARN and a condition statement. The condition
   * statement can be null.
   *
   * @method addMethod
   * @param {String} effect: The effect for the policy. This can only be "Allow" or "Deny".
   * @param {String} verb: The HTTP verb for the method, this should ideally come from the
   *                 AuthPolicy.HttpVerb object to avoid spelling mistakes
   * @param {String} resource: The resource path. For example "/pets"
   * @param {Object} conditions: The conditions object in the format specified by the AWS docs.
   * @return {void}
   */
  addMethod = (effect:string, verb:string, resource:string, conditions:ConditionBlock[] | null):void => {
    if (verb != "*" && !AuthPolicy.HttpVerb.hasOwnProperty(verb)) {
      throw new Error("Invalid HTTP verb " + verb + ". Allowed verbs in AuthPolicy.HttpVerb");
    }

    if (!this.pathRegex.test(resource)) {
      throw new Error("Invalid resource path: " + resource + ". Path should match " + this.pathRegex);
    }

    let cleanedResource = resource;
    if (resource.substring(0, 1) == "/") {
      cleanedResource = resource.substring(1, resource.length);
    }
    const resourceArn = "arn:aws:execute-api:" +
      this.region + ":" +
      this.awsAccountId + ":" +
      this.restApiId + "/" +
      this.stage + "/" +
      verb + "/" +
      cleanedResource;

    if (effect.toLowerCase() == "allow") {
      this.allowMethods.push({ resourceArn, conditions });
    } else if (effect.toLowerCase() == "deny") {
      this.denyMethods.push({ resourceArn, conditions });
    }
  }

  /**
   * Returns an empty statement object prepopulated with the correct action and the
   * desired effect.
   *
   * @method getEmptyStatement
   * @param {String} effect: The effect of the statement, this can be "Allow" or "Deny"
   * @return {Object} An empty statement object with the Action, Effect, and Resource
   *                  properties prepopulated.
   */
  getEmptyStatement = (effect:string):any => {
    return {
      Action: "execute-api:Invoke",
      Effect: effect.substring(0, 1).toUpperCase() + effect.substring(1, effect.length).toLowerCase(),
      Resource: [],
    };
  };

  /**
   * This function loops over an array of objects containing a resourceArn and
   * conditions statement and generates the array of statements for the policy.
   *
   * @method getStatementsForEffect
   * @param {String} effect: The desired effect. This can be "Allow" or "Deny"
   * @param {Array} methods: An array of method objects containing the ARN of the resource
   *                and the conditions for the policy
   * @return {Array} an array of formatted statements for the policy.
   */
  getStatementsForEffect = (effect:string, methods:Method[]):any[] => {
    const statements = [];

    if (methods.length > 0) {
      const statement = this.getEmptyStatement(effect);

      for (let i = 0; i < methods.length; i++) {
        const curMethod = methods[i];
        if (curMethod.conditions === null || curMethod.conditions.length === 0) {
          statement.Resource.push(curMethod.resourceArn);
        } else {
          const conditionalStatement = this.getEmptyStatement(effect);
          conditionalStatement.Resource.push(curMethod.resourceArn);
          conditionalStatement.Condition = curMethod.conditions;
          statements.push(conditionalStatement);
        }
      }

      if (statement.Resource !== null && statement.Resource.length > 0) {
        statements.push(statement);
      }
    }

    return statements;
  };

  /**
   * Adds an allow "*" statement to the policy.
   *
   * @method allowAllMethods
   */
  allowAllMethods = ():void => {
    this.addMethod.call(this, "allow", "*", "*", null);
  }

  /**
   * Adds a deny "*" statement to the policy.
   *
   * @method denyAllMethods
   */
  denyAllMethods = ():void => {
    this.addMethod.call(this, "deny", "*", "*", null);
  }

  /**
   * Adds an API Gateway method (Http verb + Resource path) to the list of allowed
   * methods for the policy
   *
   * @method allowMethod
   * @param {String} verb: The HTTP verb for the method, this should ideally come from the
   *                 AuthPolicy.HttpVerb object to avoid spelling mistakes
   * @param {string} resource: The resource path. For example "/pets"
   * @return {void}
   */
  allowMethod = (verb:string, resource:string):void => {
    this.addMethod.call(this, "allow", verb, resource, null);
  }

  /**
   * Adds an API Gateway method (Http verb + Resource path) to the list of denied
   * methods for the policy
   *
   * @method denyMethod
   * @param {String} verb: The HTTP verb for the method, this should ideally come from the
   *                 AuthPolicy.HttpVerb object to avoid spelling mistakes
   * @param {string} resource: The resource path. For example "/pets"
   * @return {void}
   */
  denyMethod = (verb:string, resource:string):void => {
    this.addMethod.call(this, "deny", verb, resource, null);
  }

  /**
   * Adds an API Gateway method (Http verb + Resource path) to the list of allowed
   * methods and includes a condition for the policy statement. More on AWS policy
   * conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition
   *
   * @method allowMethodWithConditions
   * @param {String} verb: The HTTP verb for the method, this should ideally come from the
   *                 AuthPolicy.HttpVerb object to avoid spelling mistakes
   * @param {string} resource: The resource path. For example "/pets"
   * @param {Object} conditions: The conditions object in the format specified by the AWS docs
   * @return {void}
   */
  allowMethodWithConditions = (verb:string, resource:string, conditions:ConditionBlock[]):void => {
    this.addMethod.call(this, "allow", verb, resource, conditions);
  }

  /**
   * Adds an API Gateway method (Http verb + Resource path) to the list of denied
   * methods and includes a condition for the policy statement. More on AWS policy
   * conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition
   *
   * @method denyMethodWithConditions
   * @param {String} verb: The HTTP verb for the method, this should ideally come from the
   *                 AuthPolicy.HttpVerb object to avoid spelling mistakes
   * @param {string} resource: The resource path. For example "/pets"
   * @param {Object} conditions: The conditions object in the format specified by the AWS docs
   * @return {void}
   */
  denyMethodWithConditions = (verb:string, resource:string, conditions:ConditionBlock[]):void => {
    this.addMethod.call(this, "deny", verb, resource, conditions);
  }

  /**
   * Generates the policy document based on the internal lists of allowed and denied
   * conditions. This will generate a policy with two main statements for the effect:
   * one statement for Allow and one statement for Deny.
   * Methods that includes conditions will have their own statement in the policy.
   *
   * @method build
   * @return {Object} The policy object that can be serialized to JSON.
   */
  build = ():AuthResponse => {
    if ((!this.allowMethods || this.allowMethods.length === 0) &&
      (!this.denyMethods || this.denyMethods.length === 0)) {
      throw new Error("No statements defined for the policy");
    }
    
    const doc:PolicyDocument = {
      Statement: [],
      Version: this.version,
    };

    doc.Statement = doc.Statement.concat(this.getStatementsForEffect.call(this, "Allow", this.allowMethods));
    doc.Statement = doc.Statement.concat(this.getStatementsForEffect.call(this, "Deny", this.denyMethods));

    return {
      principalId: this.principalId,
      policyDocument: doc,
    };
  }
}

Unit testing an authorizer

After creating an authorizer, you do of course want to unit test that the authorizer is producing policy documents that reflect the methods/resources that should allowed/denied based on the input.

I can't seem to find any tools that can be used locally to evaluate an IAM policy document given an action on a particular resource (or merely a request to an API endpoint).

Yes, you could treat the output of the authorizer as merely a JSON document, and test its content, but this does not provide for testing the semantics of policy evaluation.

Are there any tools that would allow an IAM policy document to be evaluated locally for local testing?

Add context key-value HashMap to AuthPolicy constructor

Accordingly with the aws documentation : https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-custom-authorizer-output.html

The custom authorizer's Lambda function returns an output that must include the principal identifier (principalId) and a policy document (policyDocument) containing a list of policy statements. The output can also include a context map containing key-value pairs.

The context map containing key-value pairs is missing in the Java implementation. Is it possibile to add it in order to retrive the key-value map in the lamba function that handles the response ?

The authPolicy constructor should be something like:
AuthPolicy(String principalId, PolicyDocument policyDocumentObject, HashMap<String, String> context).

This would be really useful in case of JWT Custom authorizer that need to pass some token extracted information to the lambda.

Update nodejs example to ES7 and Node v8.10 runtime

Hi there, I'm a fellow Amazon developer, I'm wondering if you're open to pull requests to update the nodejs example to modern JavaScript? I just spent a few hours completing a ES7 rewrite of the module that's supported in the Node.js v8.10 runtime. I feel like it may better lay out some of the core concepts this example library is trying to convey, and update some of the functional foundations around syntax like let and const as well as arrow functions and other JS features that ES7 and AWS Lambda now support.

If you are, just let me know and I'll put a PR up. Cheers

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.