From 002aac7203e1b53bded746c46692131bea08dc2e Mon Sep 17 00:00:00 2001 From: Jordan Sim-Smith Date: Wed, 18 Sep 2024 16:24:14 +1200 Subject: [PATCH] Implement basic api authorizer --- MODULE.bazel | 1 + MODULE.bazel.lock | 3 +- immersion_tracker_api/BUILD.bazel | 21 +++++ immersion_tracker_api/infra/main.tf | 45 +++++++++-- .../immersiontracker/AuthHandler.java | 76 +++++++++++++++++++ 5 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/AuthHandler.java diff --git a/MODULE.bazel b/MODULE.bazel index f45dcec..ec47cbc 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -31,6 +31,7 @@ maven.install( "software.amazon.awssdk:bom:2.26.19", "software.amazon.awssdk:dynamodb:2.26.19", "software.amazon.awssdk:dynamodb-enhanced:2.26.19", + "software.amazon.awssdk:iam-policy-builder:2.28.3", "com.amazonaws:aws-lambda-java-core:1.2.3", "com.amazonaws:aws-lambda-java-events:3.13.0", "com.google.guava:guava:33.2.1-jre", diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 55085b5..b59a6ba 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1290,7 +1290,7 @@ "@@rules_jvm_external~//:extensions.bzl%maven": { "general": { "bzlTransitiveDigest": "Pq3YnJEflPAALx/UMykUnN3aA5bve0fNm/cuLvt/zvI=", - "usagesDigest": "R9TQHsVrDEiwyYxr65jAYIne26o/qUsTPqy1anaL6us=", + "usagesDigest": "x+RlRhMg0A94uZTWRSaJYVGl/nlARDBdvCWdjU04acY=", "recordedFileInputs": { "@@rules_jvm_external~//rules_jvm_external_deps_install.json": "0bfbc915d9155df44d7a3b216e8f3c1fbcd110e358dd07637dc393583a5227e8", "@@contrib_rules_jvm~//contrib_rules_jvm_deps_install.json": "c65d8722c471d3b75be39f94e3c4a6ce19c4e377e96cb9b28746ac559f6267de" @@ -6670,6 +6670,7 @@ "{ \"group\": \"software.amazon.awssdk\", \"artifact\": \"bom\", \"version\": \"2.26.19\" }", "{ \"group\": \"software.amazon.awssdk\", \"artifact\": \"dynamodb\", \"version\": \"2.26.19\" }", "{ \"group\": \"software.amazon.awssdk\", \"artifact\": \"dynamodb-enhanced\", \"version\": \"2.26.19\" }", + "{ \"group\": \"software.amazon.awssdk\", \"artifact\": \"iam-policy-builder\", \"version\": \"2.28.3\" }", "{ \"group\": \"com.amazonaws\", \"artifact\": \"aws-lambda-java-core\", \"version\": \"1.2.3\" }", "{ \"group\": \"com.amazonaws\", \"artifact\": \"aws-lambda-java-events\", \"version\": \"3.13.0\" }", "{ \"group\": \"com.google.guava\", \"artifact\": \"guava\", \"version\": \"33.2.1-jre\" }", diff --git a/immersion_tracker_api/BUILD.bazel b/immersion_tracker_api/BUILD.bazel index e2cc55e..463b80f 100644 --- a/immersion_tracker_api/BUILD.bazel +++ b/immersion_tracker_api/BUILD.bazel @@ -22,6 +22,27 @@ java_library( ], ) +java_binary( + name = "auth-handler", + srcs = glob(["src/main/java/com/jordansimsmith/immersiontracker/AuthHandler.java"]), + create_executable = False, + resources = glob([ + "src/main/resources/logback.xml", + ]), + deps = [ + ":lib", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:ch_qos_logback_logback_core", + "@maven//:com_amazonaws_aws_lambda_java_core", + "@maven//:com_amazonaws_aws_lambda_java_events", + "@maven//:com_fasterxml_jackson_core_jackson_core", + "@maven//:com_fasterxml_jackson_core_jackson_databind", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_guava_guava", + "@maven//:software_amazon_awssdk_iam_policy_builder", + ], +) + java_binary( name = "get-progress-handler", srcs = glob(["src/main/java/com/jordansimsmith/immersiontracker/GetProgressHandler.java"]), diff --git a/immersion_tracker_api/infra/main.tf b/immersion_tracker_api/infra/main.tf index 75f1ace..1247153 100644 --- a/immersion_tracker_api/infra/main.tf +++ b/immersion_tracker_api/infra/main.tf @@ -54,7 +54,7 @@ resource "aws_dynamodb_table" "immersion_tracker_table" { resource "aws_secretsmanager_secret" "users" { name = "${local.application_id}_users" recovery_window_in_days = 0 - tags = local.tags + tags = local.tags } data "aws_iam_policy_document" "lambda_sts_allow_policy_document" { @@ -154,6 +154,30 @@ resource "aws_iam_role_policy_attachment" "lambda_secretsmanager" { policy_arn = aws_iam_policy.lambda_secretsmanager.arn } +data "external" "auth_handler_location" { + program = ["bash", "${path.module}/resolve_location.sh"] + + query = { + target = "//immersion_tracker_api:auth-handler_deploy.jar" + } +} + +data "local_file" "auth_handler_file" { + filename = data.external.auth_handler_location.result.location +} + +resource "aws_lambda_function" "auth" { + filename = data.local_file.auth_handler_file.filename + function_name = "${local.application_id}_auth" + role = aws_iam_role.lambda_role.arn + source_code_hash = data.local_file.auth_handler_file.content_base64sha256 + handler = "com.jordansimsmith.immersiontracker.AuthHandler" + runtime = "java17" + memory_size = 512 + timeout = 10 + tags = local.tags +} + data "external" "get_progress_handler_location" { program = ["bash", "${path.module}/resolve_location.sh"] @@ -179,7 +203,7 @@ resource "aws_lambda_function" "get_progress" { } resource "aws_lambda_permission" "api_gateway" { - for_each = toset([aws_lambda_function.get_progress.function_name]) + for_each = toset([aws_lambda_function.auth.function_name, aws_lambda_function.get_progress.function_name]) statement_id = "AllowAPIGatewayInvoke" action = "lambda:InvokeFunction" @@ -193,6 +217,13 @@ resource "aws_api_gateway_rest_api" "immersion_tracker" { tags = local.tags } +resource "aws_api_gateway_authorizer" "immersion_tracker" { + name = "${local.application_id}_authorizer" + rest_api_id = aws_api_gateway_rest_api.immersion_tracker.id + authorizer_uri = aws_lambda_function.auth.invoke_arn + type = "TOKEN" +} + resource "aws_api_gateway_resource" "get_progress" { rest_api_id = aws_api_gateway_rest_api.immersion_tracker.id parent_id = aws_api_gateway_rest_api.immersion_tracker.root_resource_id @@ -203,7 +234,8 @@ resource "aws_api_gateway_method" "get_progress" { rest_api_id = aws_api_gateway_rest_api.immersion_tracker.id resource_id = aws_api_gateway_resource.get_progress.id http_method = "GET" - authorization = "NONE" + authorization = "CUSTOM" + authorizer_id = aws_api_gateway_authorizer.immersion_tracker.id } resource "aws_api_gateway_integration" "get_progress" { @@ -220,9 +252,10 @@ resource "aws_api_gateway_deployment" "immersion_tracker" { triggers = { redeployment = sha1(jsonencode([ - aws_api_gateway_resource.get_progress.id, - aws_api_gateway_method.get_progress.id, - aws_api_gateway_integration.get_progress.id, + aws_api_gateway_authorizer.immersion_tracker, + aws_api_gateway_resource.get_progress, + aws_api_gateway_method.get_progress, + aws_api_gateway_integration.get_progress, ])) } diff --git a/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/AuthHandler.java b/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/AuthHandler.java new file mode 100644 index 0000000..4f2e9e6 --- /dev/null +++ b/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/AuthHandler.java @@ -0,0 +1,76 @@ +package com.jordansimsmith.immersiontracker; + +import static com.jordansimsmith.immersiontracker.AuthHandler.AuthorizerEvent; +import static com.jordansimsmith.immersiontracker.AuthHandler.AuthorizerResponse; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import java.util.LinkedHashMap; +import software.amazon.awssdk.policybuilder.iam.IamEffect; +import software.amazon.awssdk.policybuilder.iam.IamPolicy; +import software.amazon.awssdk.policybuilder.iam.IamStatement; + +public class AuthHandler implements RequestHandler { + private final ObjectMapper objectMapper; + + public record AuthorizerEvent(String authorizationToken, String methodArn) {} + + public record AuthorizerResponse(String principalId, Object policyDocument) {} + + public AuthHandler() { + this(ImmersionTrackerFactory.create()); + } + + @VisibleForTesting + AuthHandler(ImmersionTrackerFactory factory) { + this.objectMapper = factory.objectMapper(); + } + + @Override + public AuthorizerResponse handleRequest(AuthorizerEvent event, Context context) { + try { + return doHandleRequest(event, context); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public AuthorizerResponse doHandleRequest(AuthorizerEvent event, Context context) + throws Exception { + var token = event.authorizationToken.replace("Bearer ", ""); + + if (Strings.isNullOrEmpty(token)) { + throw new RuntimeException("Unauthorized"); + } + + if ("jordan".equals(token)) { + return response(token, IamEffect.ALLOW, event.methodArn()); + } + + return response(token, IamEffect.DENY, event.methodArn()); + } + + private AuthorizerResponse response(String principal, IamEffect effect, String resource) + throws JsonProcessingException { + var policy = + IamPolicy.builder() + .version("2012-10-17") + .addStatement( + IamStatement.builder() + .addAction("execute-api:Invoke") + .effect(effect) + .addResource(resource) + .build()) + .build() + .toJson(); + var policyMap = objectMapper.readValue(policy, LinkedHashMap.class); + + return new AuthorizerResponse(principal, policyMap); + } +}