From 372f13bba6eb8661e1debb69bf0cc365a63bdc47 Mon Sep 17 00:00:00 2001 From: Marc Gavanier Date: Fri, 29 Sep 2023 14:31:47 +0200 Subject: [PATCH] feat: add api keys authorizer --- .github/workflows/terraform.yml | 3 ++ .gitignore | 5 +- api-gateway.tf | 18 +++++-- api-keys.tf | 84 ++++++++++++++++++++++++++++++++ api-lambda.tf | 2 +- cloud-watch.tf | 17 +++++++ database.tf | 21 ++++---- global.tf | 7 +-- import-from-s3-lambda.tf | 21 +------- src/api-keys-authorizer/index.ts | 9 ++++ vite.config.ts | 16 ++++-- 11 files changed, 157 insertions(+), 46 deletions(-) create mode 100644 api-keys.tf create mode 100644 cloud-watch.tf create mode 100644 src/api-keys-authorizer/index.ts diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 4a34cd8..a814f3a 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -89,6 +89,9 @@ jobs: mv "import-from-s3/index.mjs" "dist/index.mjs" (cd dist && zip "import-from-s3.zip" "index.mjs") rm "dist/index.mjs" + mv "api-keys-authorizer/index.mjs" "dist/index.mjs" + (cd dist && zip "api-keys-authorizer.zip" "index.mjs") + rm "dist/index.mjs" - name: Publish on S3 uses: jakejarvis/s3-sync-action@master with: diff --git a/.gitignore b/.gitignore index 0559cf3..7509c49 100644 --- a/.gitignore +++ b/.gitignore @@ -116,5 +116,6 @@ terraform.rc node_modules/ # Build -dist -import-from-s3 +dist/ +api-keys-authorizer/ +import-from-s3/ diff --git a/api-gateway.tf b/api-gateway.tf index 5e2576f..75bcb03 100644 --- a/api-gateway.tf +++ b/api-gateway.tf @@ -1,5 +1,5 @@ resource "aws_apigatewayv2_api" "cartographie_nationale" { - name = "${local.product_information.context.project}-${local.product_information.context.service}" + name = local.name_prefix tags = local.tags protocol_type = "HTTP" @@ -35,6 +35,15 @@ resource "aws_apigatewayv2_stage" "cartographie_nationale" { } } +resource "aws_apigatewayv2_authorizer" "api_key_authorizer" { + api_id = aws_apigatewayv2_api.cartographie_nationale.id + authorizer_type = "REQUEST" + authorizer_uri = aws_lambda_function.import_from_s3.invoke_arn + identity_sources = ["$request.header.Authorization"] + name = "${local.name_prefix}.key-authorizer" + authorizer_payload_format_version = "2.0" +} + resource "aws_apigatewayv2_integration" "api_integrations" { for_each = { for object in data.aws_s3_object.s3_objects : @@ -55,9 +64,10 @@ resource "aws_apigatewayv2_route" "api_route" { if object.content_type == "application/zip" } - api_id = aws_apigatewayv2_api.cartographie_nationale.id - route_key = "GET /${aws_lambda_function.api_routes[each.key].function_name}" - target = "integrations/${aws_apigatewayv2_integration.api_integrations[each.key].id}" + api_id = aws_apigatewayv2_api.cartographie_nationale.id + route_key = "${upper(element(split(".", each.key), length(split(".", each.key)) - 2))} /${aws_lambda_function.api_routes[each.key].function_name}" + target = "integrations/${aws_apigatewayv2_integration.api_integrations[each.key].id}" + authorizer_id = element(split(".", each.key), length(split(".", each.key)) - 2) != "get" ? aws_apigatewayv2_authorizer.api_key_authorizer.id : null } resource "aws_cloudwatch_log_group" "api_cartographie_nationale" { diff --git a/api-keys.tf b/api-keys.tf new file mode 100644 index 0000000..f2241e1 --- /dev/null +++ b/api-keys.tf @@ -0,0 +1,84 @@ +resource "aws_dynamodb_table" "api_keys_table" { + name = "${local.product_information.context.project}.api-keys" + billing_mode = "PAY_PER_REQUEST" + hash_key = "name" + + attribute { + name = "name" + type = "S" + } + + tags = local.tags +} + +data "aws_iam_policy_document" "api_keys_table_policy_document" { + statement { + effect = "Allow" + actions = [ + "dynamodb:GetItem", + "dynamodb:Query", + "dynamodb:Scan" + ] + resources = [aws_dynamodb_table.api_keys_table.arn] + } +} + +resource "aws_iam_policy" "api_keys_table_policy" { + name = "${local.name_prefix}.keys-table-policy" + description = "IAM policy to allow DynamoDB Table api-keys access" + policy = data.aws_iam_policy_document.api_keys_table_policy_document.json +} + +data "aws_s3_object" "api_keys_authorizer_archive" { + count = fileexists("${path.module}/api-keys-authorizer/index.mjs") ? 0 : 1 + bucket = aws_s3_bucket.api.id + key = "utilities/api-keys-authorizer.zip" +} + +data "archive_file" "api_keys_authorizer_archive" { + count = fileexists("${path.module}/api-keys-authorizer/index.mjs") ? 1 : 0 + type = "zip" + source_dir = "${path.module}/api-keys-authorizer" + output_path = "${path.module}/api-keys-authorizer.zip" +} + +resource "aws_lambda_function" "api_keys_authorizer" { + filename = fileexists("${path.module}/api-keys-authorizer/index.mjs") ? "api-keys-authorizer.zip" : null + s3_bucket = fileexists("${path.module}/api-keys-authorizer/index.mjs") ? null : aws_s3_bucket.api.id + s3_key = fileexists("${path.module}/api-keys-authorizer/index.mjs") ? null : "utilities/api-keys-authorizer.zip" + function_name = "api-keys-authorizer" + role = aws_iam_role.api_keys_authorizer_execution_role.arn + handler = "index.handler" + runtime = "nodejs18.x" + source_code_hash = fileexists("${path.module}/api-keys-authorizer/index.mjs") ? data.archive_file.import_from_s3_archive[0].output_base64sha256 : null +} + +resource "aws_cloudwatch_log_group" "api_keys_authorizer_cloudwatch_log_group" { + name = "/aws/lambda/${aws_lambda_function.api_keys_authorizer.function_name}" + retention_in_days = 14 +} + +data "aws_iam_policy_document" "api_keys_authorizer_execution_policy" { + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + + principals { + identifiers = ["lambda.amazonaws.com"] + type = "Service" + } + } +} + +resource "aws_iam_role" "api_keys_authorizer_execution_role" { + name = "${local.name_prefix}.keys-authorizer-execution-role" + description = "Authentication iam role references a policy document that can assume role for api keys authorizer execution" + tags = local.tags + assume_role_policy = data.aws_iam_policy_document.api_keys_authorizer_execution_policy.json +} + +resource "aws_iam_role_policy" "api_keys_table_role_policy" { + name = "${local.name_prefix}.keys-table-role-policy" + role = aws_iam_role.api_keys_authorizer_execution_role.id + policy = data.aws_iam_policy_document.api_keys_table_policy_document.json +} diff --git a/api-lambda.tf b/api-lambda.tf index 7bb8229..220ee19 100644 --- a/api-lambda.tf +++ b/api-lambda.tf @@ -55,7 +55,7 @@ resource "aws_iam_role" "api_routes_roles" { resource "aws_iam_policy_attachment" "sources_table_policy_attachment" { name = "sources_table_policy_attachment" - policy_arn = aws_iam_policy.sources_table_table_policy.arn + policy_arn = aws_iam_policy.sources_table_policy.arn roles = [aws_iam_role.api_routes_roles.name] } diff --git a/cloud-watch.tf b/cloud-watch.tf new file mode 100644 index 0000000..37aaa0a --- /dev/null +++ b/cloud-watch.tf @@ -0,0 +1,17 @@ +data "aws_iam_policy_document" "cloud_watch_role_policy_document" { + statement { + effect = "Allow" + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + resources = ["arn:aws:logs:*:*:*"] + } +} + +resource "aws_iam_role_policy" "cloud_watch_role_policy" { + name = "${local.name_prefix}.cloud-watch-role-policy" + role = aws_iam_role.api_keys_authorizer_execution_role.id + policy = data.aws_iam_policy_document.cloud_watch_role_policy_document.json +} diff --git a/database.tf b/database.tf index 03d8ec5..bebda9c 100644 --- a/database.tf +++ b/database.tf @@ -1,5 +1,5 @@ resource "aws_dynamodb_table" "sources_table" { - name = "Sources" + name = "${local.product_information.context.project}.sources" billing_mode = "PAY_PER_REQUEST" hash_key = "name" @@ -11,7 +11,7 @@ resource "aws_dynamodb_table" "sources_table" { tags = local.tags } -data "aws_iam_policy_document" "sources_table_table_policy_document" { +data "aws_iam_policy_document" "sources_table_policy_document" { statement { effect = "Allow" actions = [ @@ -26,14 +26,14 @@ data "aws_iam_policy_document" "sources_table_table_policy_document" { } } -resource "aws_iam_policy" "sources_table_table_policy" { - name = "sources_table_table_policy" +resource "aws_iam_policy" "sources_table_policy" { + name = "${local.product_information.context.project}.sources-table-policy" description = "IAM policy to allow DynamoDB Table sources access" - policy = data.aws_iam_policy_document.sources_table_table_policy_document.json + policy = data.aws_iam_policy_document.sources_table_policy_document.json } resource "aws_dynamodb_table" "lieux_inclusion_numerique_table" { - name = "LieuxInclusionNumerique" + name = "${local.product_information.context.project}.lieux-inclusion-numerique" billing_mode = "PAY_PER_REQUEST" hash_key = "id" @@ -61,12 +61,11 @@ data "aws_iam_policy_document" "lieux_inclusion_numerique_table_policy_document" } resource "aws_iam_policy" "lieux_inclusion_numerique_table_policy" { - name = "lieux_inclusion_numerique_table_policy" + name = "${local.name_prefix}.lieux-inclusion-numerique-table-policy" description = "IAM policy to allow DynamoDB Table Lieux d'inclusion numérique access" policy = data.aws_iam_policy_document.lieux_inclusion_numerique_table_policy_document.json } - resource "aws_s3_bucket" "dynamo_table_import" { bucket = "${replace(local.product_information.context.project, "_", "-")}-dynamo-table-import" force_destroy = true @@ -101,15 +100,13 @@ data "aws_iam_policy_document" "read_from_s3_policy_document" { } resource "aws_iam_role_policy" "read_from_s3_role_policy" { - name = "read-from-s3-role-policy" + name = "${local.name_prefix}.read-from-s3-role-policy" role = aws_iam_role.import_from_s3_role.id policy = data.aws_iam_policy_document.read_from_s3_policy_document.json } - - resource "aws_iam_role_policy" "import_in_dynamo_table_role_policy" { - name = "import-in-dynamo-table-role-policy" + name = "${local.name_prefix}.import-in-dynamo-table-role-policy" role = aws_iam_role.import_from_s3_role.id policy = data.aws_iam_policy_document.lieux_inclusion_numerique_table_policy_document.json } diff --git a/global.tf b/global.tf index 59d0268..026d42f 100644 --- a/global.tf +++ b/global.tf @@ -25,7 +25,8 @@ locals { } locals { - projectTitle = title(replace(local.product_information.context.project, "-", " ")) - layerTitle = title(replace(local.product_information.context.layer, "-", " ")) - serviceTitle = title(replace(local.product_information.context.service, "-", " ")) + name_prefix = "${local.product_information.context.project}-${local.product_information.context.service}" + project_title = title(replace(local.product_information.context.project, "-", " ")) + layer_title = title(replace(local.product_information.context.layer, "-", " ")) + service_title = title(replace(local.product_information.context.service, "-", " ")) } diff --git a/import-from-s3-lambda.tf b/import-from-s3-lambda.tf index d651412..58ac8e2 100644 --- a/import-from-s3-lambda.tf +++ b/import-from-s3-lambda.tf @@ -4,7 +4,6 @@ data "aws_s3_object" "import_from_s3_archive" { key = "utilities/import-from-s3.zip" } - data "archive_file" "import_from_s3_archive" { count = fileexists("${path.module}/import-from-s3/index.mjs") ? 1 : 0 type = "zip" @@ -25,7 +24,7 @@ resource "aws_lambda_function" "import_from_s3" { source_code_hash = fileexists("${path.module}/import-from-s3/index.mjs") ? data.archive_file.import_from_s3_archive[0].output_base64sha256 : null } -resource "aws_cloudwatch_log_group" "example" { +resource "aws_cloudwatch_log_group" "import_from_s3_cloudwatch_log_group" { name = "/aws/lambda/${aws_lambda_function.import_from_s3.function_name}" retention_in_days = 14 } @@ -48,21 +47,3 @@ resource "aws_iam_role" "import_from_s3_role" { tags = local.tags assume_role_policy = data.aws_iam_policy_document.lambda_execution_policy.json } - -data "aws_iam_policy_document" "cloud_watch_role_policy_document" { - statement { - effect = "Allow" - actions = [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ] - resources = ["arn:aws:logs:*:*:*"] - } -} - -resource "aws_iam_role_policy" "cloud_watch_role_policy" { - name = "cloud-watch-role-policy" - role = aws_iam_role.import_from_s3_role.id - policy = data.aws_iam_policy_document.cloud_watch_role_policy_document.json -} diff --git a/src/api-keys-authorizer/index.ts b/src/api-keys-authorizer/index.ts new file mode 100644 index 0000000..2fc94e1 --- /dev/null +++ b/src/api-keys-authorizer/index.ts @@ -0,0 +1,9 @@ +import { APIGatewayProxyResultV2 } from 'aws-lambda'; + +export const handler = async (): Promise> => { + try { + return false; + } catch (err) { + console.log(err); + } +}; diff --git a/vite.config.ts b/vite.config.ts index 448e640..194030d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,17 @@ import * as path from 'path'; import { defineConfig } from 'vite'; -const inputFor = (folder: string) => ({ - [`${folder}/index`]: path.resolve(__dirname, `src/${folder}/index.ts`), -}); +const inputFor = (folders: string[]) => + folders.reduce( + ( + inputs: { [key: string]: string }, + folder: string, + ): { [key: string]: string } => ({ + ...inputs, + [`${folder}/index`]: path.resolve(__dirname, `src/${folder}/index.ts`), + }), + {}, + ); export default defineConfig({ build: { @@ -15,7 +23,7 @@ export default defineConfig({ rollupOptions: { external: /^@aws-sdk/, input: { - ...inputFor('import-from-s3'), + ...inputFor(['api-keys-authorizer', 'import-from-s3']), }, output: { entryFileNames: '[name].mjs',