Skip to content

Commit

Permalink
JASPER-217: Integrate Authorizer to API Gateway (#69)
Browse files Browse the repository at this point in the history
- Fixed error in publish lambda GHA
- Fixed failing build in Publish Lambda Functions GHA
- Added Authorizer and Rotate Key Lambda Functions
- Update Terraform code to support Authorizer
- Added implementation to restart ECS service when key is rotated
- Updates to TF to allow lambda to update secret and restart ECS service
- Use x-origin-verify header and pass to http client
- Update devcontainer to force .tf files to be formatted by TF extension
- Added APIGW Settings for logging
- Added lambda mem size default to 512M

---------

Co-authored-by: Ronaldo Macapobre <[email protected]>
  • Loading branch information
ronaldo-macapobre and Ronaldo Macapobre authored Nov 15, 2024
1 parent a31f1ea commit 4c637b9
Show file tree
Hide file tree
Showing 31 changed files with 1,404 additions and 287 deletions.
13 changes: 11 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,22 @@
"ms-dotnettools.csdevkit",
"ms-dotnettools.csharp",
"ms-dotnettools.vscode-dotnet-runtime",
"amazonwebservices.aws-toolkit-vscode"
"amazonwebservices.aws-toolkit-vscode",
"hashicorp.terraform"
],
"settings": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"editor.formatOnSave": true
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[csharp]": {
"editor.defaultFormatter": "ms-dotnettools.csharp"
},
"[terraform]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "hashicorp.terraform"
}
}
}
},
Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/actions/deploy-lambda/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ inputs:
app_name:
description: The application name.
required: true
resource:
description: The resource path of the lambda function.
required: true
lambda_name:
description: The lambda function name name.
required: true
Expand Down Expand Up @@ -71,7 +68,7 @@ runs:
id: ecr-check
shell: bash
run: |
IMAGE_TAG=${{ inputs.resource }}${{ inputs.lambda_name }}-${{ inputs.short_sha }}
IMAGE_TAG=${{ inputs.image_name }}-${{ inputs.short_sha }}
REPOSITORY_NAME=${{ inputs.app_name }}-lambda-repo-${{ inputs.environment }}
IMAGE_EXISTS=$(aws ecr describe-images --repository-name $REPOSITORY_NAME --query "imageDetails[?contains(imageTags, '$IMAGE_TAG')]" --output text)
Expand All @@ -97,4 +94,4 @@ runs:
run: |
aws lambda update-function-code \
--function-name ${{ inputs.app_name }}-${{ inputs.lambda_name }}-lambda-${{ inputs.environment }} \
--image-uri ${{ env.full_ecr_repo_url }}:${{ inputs.resource }}.${{ inputs.lambda_name }}-${{ inputs.short_sha }}
--image-uri ${{ steps.vars.outputs.full_ecr_repo_url }}:${{ inputs.image_name }}-${{ inputs.short_sha }}
1 change: 1 addition & 0 deletions .github/workflows/aws-template-terraform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ jobs:
TF_VAR_environment: ${{ vars.ENVIRONMENT_NAME }}
TF_VAR_kms_key_name: ${{ vars.KMS_KEY_NAME }}
TF_VAR_vpc_id: ${{ vars.VPC_ID }}
TF_VAR_lambda_memory_size: ${{ vars.LAMBDA_MEMORY_SIZE }}
needs: [check_changes, scan]
steps:
- name: Checkout repository
Expand Down
14 changes: 8 additions & 6 deletions .github/workflows/publish-lambdas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
lambda_dir_list: ${{ steps.convert.outputs.LAMBDA_DIR_LIST }}

steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -48,9 +49,11 @@ jobs:
LAMBDA_DIR_LIST=$(echo "${LAMBDA_DIRS}" | jq -R 'split(";")' -c)
echo "LAMBDA_DIR_LIST=$LAMBDA_DIR_LIST" >> $GITHUB_OUTPUT
deploy-to-gchr:
deploy2gchr:
needs: get-lambdas
environment: ${{ inputs.environment }}
outputs:
short_sha: ${{ steps.short_sha.outputs.SHORT_SHA }}
permissions:
id-token: write
packages: write
Expand Down Expand Up @@ -82,7 +85,7 @@ jobs:
- name: Get short SHA
id: short_sha
run: |
echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Parse Resource and Lambda Name
id: parse
Expand All @@ -100,7 +103,7 @@ jobs:
images: |
${{ env.GITHUB_IMAGE_REPO }}/${{ env.RESOURCE }}.${{ env.LAMBDA }}
tags: |
type=raw,value=${{ env.SHORT_SHA }}
type=raw,value=${{ steps.short_sha.outputs.SHORT_SHA }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
Expand All @@ -121,7 +124,7 @@ jobs:
deploy2dev:
name: Deploy to DEV
needs: [get-lambdas, deploy-to-gchr]
needs: [get-lambdas, deploy2gchr]
env:
ENVIRONMENT: dev
permissions:
Expand Down Expand Up @@ -157,7 +160,6 @@ jobs:
aws_role_arn: ${{ vars.AWS_ROLE_ARN }}
ghcr_token: ${{ secrets.GITHUB_TOKEN }}
github_image_repo: ${{ env.GITHUB_IMAGE_REPO }}
resource: ${{ env.RESOURCE }}
lambda_name: ${{ env.LAMBDA }}
image_name: ${{ env.RESOURCE }}.${{ env.LAMBDA }}
short_sha: ${{ needs.deploy-to-gchr.outputs.short_sha }}
short_sha: ${{ needs.deploy2gchr.outputs.short_sha }}
10 changes: 8 additions & 2 deletions api/Infrastructure/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ namespace Scv.Api.Infrastructure
{
public static class ServiceCollectionExtensions
{
const string X_APIGW_KEY_HEADER = "x-api-key";
const string X_ORIGIN_VERIFY_HEADER = "x-origin-verify";

public static IServiceCollection AddMapster(this IServiceCollection services, Action<TypeAdapterConfig> options = null)
{
var config = TypeAdapterConfig.GlobalSettings;
Expand All @@ -37,6 +40,9 @@ public static IServiceCollection AddMapster(this IServiceCollection services, Ac

public static IServiceCollection AddHttpClientsAndScvServices(this IServiceCollection services, IConfiguration configuration)
{
var apigwKey = configuration.GetNonEmptyValue("AWS_API_GATEWAY_API_KEY");
var authorizerKey = configuration.GetNonEmptyValue("AuthorizerKey");

services.AddTransient<TimingHandler>();
services.AddHttpClient<FileServicesClient>(client =>
{
Expand All @@ -61,8 +67,8 @@ public static IServiceCollection AddHttpClientsAndScvServices(this IServiceColle
// configuration.GetNonEmptyValue("LocationServicesClient:Username"),
// configuration.GetNonEmptyValue("LocationServicesClient:Password"));
client.BaseAddress = new Uri(configuration.GetNonEmptyValue("LocationServicesClient:Url").EnsureEndingForwardSlash());
var apiKey = configuration.GetNonEmptyValue("AWS_API_GATEWAY_API_KEY");
client.DefaultRequestHeaders.Add("x-api-key", apiKey);
client.DefaultRequestHeaders.Add(X_APIGW_KEY_HEADER, apigwKey);
client.DefaultRequestHeaders.Add(X_ORIGIN_VERIFY_HEADER, authorizerKey);
}).AddHttpMessageHandler<TimingHandler>();

services.AddHttpClient<UserServiceClient>(client =>
Expand Down
3 changes: 3 additions & 0 deletions aws/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"semi": true
}
24 changes: 0 additions & 24 deletions aws/helpers/getSecret.ts

This file was deleted.

105 changes: 105 additions & 0 deletions aws/lambdas/auth/authorizer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Logger } from "@aws-lambda-powertools/logger";
import {
APIGatewayAuthorizerResult,
APIGatewayRequestAuthorizerEvent,
Context,
PolicyDocument,
StatementEffect,
} from "aws-lambda";
import { v4 as uuidv4 } from "uuid";
import SecretsManagerService from "../../../services/secretsManagerService";

const X_ORIGIN_VERIFY_HEADER = "x-origin-verify";

export const handler = async (
event: APIGatewayRequestAuthorizerEvent,
context: Context
): Promise<APIGatewayAuthorizerResult> => {
console.log(`Event: ${JSON.stringify(event, null, 2)}`);
console.log(`Context: ${JSON.stringify(context, null, 2)}`);

const correlationId: string = event.requestContext.requestId || uuidv4();
const logger = new Logger({
serviceName: "auth.authorizer",
});

try {
if (!event.headers) {
logger.error("headers is missing.");
throw new Error("Error: invalid token");
}

// x-verify-origin should be set by the caller
if (!(X_ORIGIN_VERIFY_HEADER in event.headers)) {
logger.error(`${X_ORIGIN_VERIFY_HEADER} not found in headers.`);
throw new Error("Error: invalid token");
}

// Extract the token from the request
const verifyToken = event.headers[X_ORIGIN_VERIFY_HEADER];
const smService = new SecretsManagerService();
const secretStringJson = await smService.getSecret(
process.env.VERIFY_SECRET_NAME!
);

let verifyTokenfromSecretManager = "";
if (secretStringJson) {
verifyTokenfromSecretManager = JSON.parse(secretStringJson).verifyKey;
logger.info(
"Authorization token from secret manager",
verifyTokenfromSecretManager
);
} else {
logger.error("Secret not found in secret manager");
throw new Error("Error: invalid token");
}

if (verifyToken !== verifyTokenfromSecretManager) {
logger.error("Authorization token not valid");
throw new Error("Error: invalid token");
}

const policy = generatePolicy(
correlationId,
"user",
"Allow",
event.methodArn
);

logger.info(JSON.stringify(policy));

return policy;
} catch (error) {
logger.error(error);

throw new Error("Unauthorized");
}
};

const generatePolicy = (
correlationId: string,
principalId: string,
effect: StatementEffect,
resource: string
): APIGatewayAuthorizerResult => {
const policyDocument: PolicyDocument = {
Version: "2012-10-17",
Statement: [
{
Action: "execute-api:Invoke",
Effect: effect,
Resource: resource,
},
],
};

const authResponse: APIGatewayAuthorizerResult = {
principalId,
context: {
correlation_id: correlationId,
},
policyDocument,
};

return authResponse;
};
63 changes: 63 additions & 0 deletions aws/lambdas/auth/rotate-key/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Logger } from "@aws-lambda-powertools/logger";
import { APIGatewayEvent, APIGatewayProxyResult, Context } from "aws-lambda";
import { v4 as uuidv4 } from "uuid";
import ECSService from "../../../services/ecsService";
import SecretsManagerService from "../../../services/secretsManagerService";

export const handler = async (
event: APIGatewayEvent,
context: Context
): Promise<APIGatewayProxyResult> => {
console.log(`Event: ${JSON.stringify(event, null, 2)}`);
console.log(`Context: ${JSON.stringify(context, null, 2)}`);

const logger = new Logger({
serviceName: "auth.rotate-key",
});

try {
logger.info("Rotating verifyKey.");
await updateSecret();
logger.info("Successfully rotated verifyKey");

logger.info("Restarting ECS Services to pickup updated VerifyKey.");
await restartECSServices();
logger.info("Restart completed.");

return {
statusCode: 200,
body: JSON.stringify({
message:
"Successfully rotated the key and restarted the ECS Service/TD",
}),
};
} catch (error) {
logger.error(error);

return {
statusCode: 500,
body: JSON.stringify({
message:
"Something went wrong when updating the key and restarting the ECS Service/TD",
}),
};
}
};

const updateSecret = async () => {
const smService = new SecretsManagerService();
const newGuid = uuidv4();

await smService.updateSecret(
process.env.VERIFY_SECRET_NAME!,
JSON.stringify({ verifyKey: newGuid })
);
};

const restartECSServices = async () => {
const ecsService = new ECSService(process.env.CLUSTER_NAME!);

const services = await ecsService.getECSServices();

await ecsService.restartServices(services);
};
Loading

0 comments on commit 4c637b9

Please sign in to comment.