From cc3a5b3d5e74eae71f264f0489896c7b9ff31363 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 16:34:14 -0500 Subject: [PATCH 1/4] enhance request with IP and location --- src/api/index.ts | 77 +++++++++++++++++++++------------ src/api/lambda.ts | 1 + src/api/package.json | 1 + src/api/plugins/errorHandler.ts | 4 +- src/api/plugins/location.ts | 27 ++++++++++++ src/api/routes/apiKey.ts | 6 +-- src/api/types.d.ts | 10 +++++ yarn.lock | 7 +++ 8 files changed, 101 insertions(+), 32 deletions(-) create mode 100644 src/api/plugins/location.ts diff --git a/src/api/index.ts b/src/api/index.ts index f425854c..04009710 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,41 +1,21 @@ -/* eslint import/no-nodejs-modules: ["error", {"allow": ["crypto"]}] */ +/* eslint import/no-nodejs-modules: ["error", {"allow": ["crypto", "path", "url"]}] */ import { randomUUID } from "crypto"; +import { fileURLToPath } from "url"; +import path from "path"; import fastify, { FastifyInstance } from "fastify"; -import FastifyAuthProvider from "@fastify/auth"; -import fastifyStatic from "@fastify/static"; -import fastifyAuthPlugin, { getSecretValue } from "./plugins/auth.js"; -import protectedRoute from "./routes/protected.js"; -import errorHandlerPlugin from "./plugins/errorHandler.js"; import { RunEnvironment, runEnvironments } from "../common/roles.js"; import { InternalServerError } from "../common/errors/index.js"; -import eventsPlugin from "./routes/events.js"; -import cors from "@fastify/cors"; import { environmentConfig, genericConfig, SecretConfig, } from "../common/config.js"; -import organizationsPlugin from "./routes/organizations.js"; -import authorizeFromSchemaPlugin from "./plugins/authorizeFromSchema.js"; -import evaluatePoliciesPlugin from "./plugins/evaluatePolicies.js"; -import icalPlugin from "./routes/ics.js"; -import vendingPlugin from "./routes/vending.js"; import * as dotenv from "dotenv"; -import iamRoutes from "./routes/iam.js"; -import ticketsPlugin from "./routes/tickets.js"; -import linkryRoutes from "./routes/linkry.js"; import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts"; import NodeCache from "node-cache"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; -import mobileWalletRoute from "./routes/mobileWallet.js"; -import stripeRoutes from "./routes/stripe.js"; -import membershipPlugin from "./routes/membership.js"; -import path from "path"; // eslint-disable-line import/no-nodejs-modules -import roomRequestRoutes from "./routes/roomRequests.js"; -import logsPlugin from "./routes/logs.js"; - import { fastifyZodOpenApiPlugin, fastifyZodOpenApiTransform, @@ -43,22 +23,53 @@ import { serializerCompiler, validatorCompiler, } from "fastify-zod-openapi"; -import { ZodOpenApiVersion } from "zod-openapi"; +import { type ZodOpenApiVersion } from "zod-openapi"; import { withTags } from "./components/index.js"; +import RedisModule from "ioredis"; + +/** BEGIN EXTERNAL PLUGINS */ +import { FastifyIP } from "fastify-ip"; +import cors from "@fastify/cors"; +import FastifyAuthProvider from "@fastify/auth"; +import fastifyStatic from "@fastify/static"; +/** END EXTERNAL PLUGINS */ + +/** BEGIN INTERNAL PLUGINS */ +import locationPlugin from "./plugins/location.js"; +import fastifyAuthPlugin, { getSecretValue } from "./plugins/auth.js"; +import errorHandlerPlugin from "./plugins/errorHandler.js"; +import authorizeFromSchemaPlugin from "./plugins/authorizeFromSchema.js"; +import evaluatePoliciesPlugin from "./plugins/evaluatePolicies.js"; +/** END INTERNAL PLUGINS */ + +/** BEGIN ROUTES */ +import organizationsPlugin from "./routes/organizations.js"; +import icalPlugin from "./routes/ics.js"; +import vendingPlugin from "./routes/vending.js"; +import iamRoutes from "./routes/iam.js"; +import ticketsPlugin from "./routes/tickets.js"; +import linkryRoutes from "./routes/linkry.js"; +import mobileWalletRoute from "./routes/mobileWallet.js"; +import stripeRoutes from "./routes/stripe.js"; +import membershipPlugin from "./routes/membership.js"; +import roomRequestRoutes from "./routes/roomRequests.js"; +import logsPlugin from "./routes/logs.js"; import apiKeyRoute from "./routes/apiKey.js"; import clearSessionRoute from "./routes/clearSession.js"; -import RedisModule from "ioredis"; -import { fileURLToPath } from "url"; // eslint-disable-line import/no-nodejs-modules +import protectedRoute from "./routes/protected.js"; +import eventsPlugin from "./routes/events.js"; +/** END ROUTES */ + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); dotenv.config(); const now = () => Date.now(); +const isRunningInLambda = + process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME; async function init(prettyPrint: boolean = false, initClients: boolean = true) { - const isRunningInLambda = - process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME; let isSwaggerServer = false; const transport = prettyPrint ? { @@ -95,6 +106,7 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) { await app.register(evaluatePoliciesPlugin); await app.register(errorHandlerPlugin); await app.register(fastifyZodOpenApiPlugin); + await app.register(locationPlugin); if (!isRunningInLambda) { try { const fastifySwagger = import("@fastify/swagger"); @@ -278,6 +290,14 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) { await app.refreshSecretConfig(); app.redisClient = new RedisModule.default(app.secretConfig.redis_url); } + if (isRunningInLambda) { + app.register(FastifyIP, { + order: ["x-forwarded-for"], + strict: true, + isAWS: false, + }); + } + app.addHook("onRequest", (req, _, done) => { req.startTime = now(); const hostname = req.hostname; @@ -337,6 +357,7 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) { origin: app.environmentConfig.ValidCorsOrigins, methods: ["GET", "HEAD", "POST", "PATCH", "DELETE"], }); + app.addHook("onSend", async (request, reply) => { reply.header("X-Request-Id", request.id); }); diff --git a/src/api/lambda.ts b/src/api/lambda.ts index f07d7b76..c31226b1 100644 --- a/src/api/lambda.ts +++ b/src/api/lambda.ts @@ -29,6 +29,7 @@ const handler = async (event: APIGatewayEvent, context: Context) => { isBase64Encoded: false, }; } + delete event.headers["x-origin-verify"]; } // else proceed with handler logic return await realHandler(event, context).catch((e) => { diff --git a/src/api/package.json b/src/api/package.json index 2f213c3f..9c682b05 100644 --- a/src/api/package.json +++ b/src/api/package.json @@ -40,6 +40,7 @@ "dotenv": "^16.5.0", "esbuild": "^0.25.3", "fastify": "^5.3.2", + "fastify-ip": "^1.2.0", "fastify-plugin": "^5.0.1", "fastify-raw-body": "^5.0.0", "fastify-zod-openapi": "^5.0.1", diff --git a/src/api/plugins/errorHandler.ts b/src/api/plugins/errorHandler.ts index 53b7c6e5..13ba857a 100644 --- a/src/api/plugins/errorHandler.ts +++ b/src/api/plugins/errorHandler.ts @@ -47,7 +47,9 @@ const errorHandlerPlugin = fp(async (fastify) => { }, ); fastify.setNotFoundHandler((request: FastifyRequest) => { - throw new NotFoundError({ endpointName: request.url }); + throw new NotFoundError({ + endpointName: `${request.method} ${request.url}`, + }); }); }); diff --git a/src/api/plugins/location.ts b/src/api/plugins/location.ts new file mode 100644 index 00000000..35b745d6 --- /dev/null +++ b/src/api/plugins/location.ts @@ -0,0 +1,27 @@ +import fp from "fastify-plugin"; + +const locationPlugin = fp(async (fastify, opts) => { + const processHeader = (headerValue: string | string[] | undefined) => { + if (Array.isArray(headerValue)) { + return headerValue.join(","); + } + return headerValue; + }; + + fastify.decorateRequest("location", { + getter() { + return { + country: processHeader(this.headers["cloudfront-viewer-country"]), + city: processHeader(this.headers["cloudfront-viewer-city"]), + region: processHeader(this.headers["cloudfront-viewer-country-region"]), + latitude: processHeader(this.headers["cloudfront-viewer-latitude"]), + longitude: processHeader(this.headers["cloudfront-viewer-longitude"]), + postalCode: processHeader( + this.headers["cloudfront-viewer-postal-code"], + ), + }; + }, + }); +}); + +export default locationPlugin; diff --git a/src/api/routes/apiKey.ts b/src/api/routes/apiKey.ts index 15b012c1..c33e90a7 100644 --- a/src/api/routes/apiKey.ts +++ b/src/api/routes/apiKey.ts @@ -105,8 +105,8 @@ Key ID: acmuiuc_${keyId} Key Description: ${description} -IP address: ${request.ip}. - +IP address: ${request.ip} +${request.location.city && request.location.region && request.location.country ? `\nLocation: ${request.location.city}, ${request.location.region}, ${request.location.country}\n` : ""} Roles: ${roles.join(", ")}. If you did not create this API key, please secure your account and notify the ACM Infrastructure team. @@ -210,7 +210,7 @@ This email confirms that an API key for the Core API has been deleted from your Key ID: acmuiuc_${keyId} IP address: ${request.ip}. - +${request.location.city && request.location.region && request.location.country ? `\nLocation: ${request.location.city}, ${request.location.region}, ${request.location.country}\n` : ""} If you did not delete this API key, please secure your account and notify the ACM Infrastructure team. `, callToActionButton: { diff --git a/src/api/types.d.ts b/src/api/types.d.ts index e8a6f246..a447e7f3 100644 --- a/src/api/types.d.ts +++ b/src/api/types.d.ts @@ -12,6 +12,15 @@ import { AvailableAuthorizationPolicy } from "common/policies/definition.js"; import type RedisModule from "ioredis"; type Redis = RedisModule.default; +interface CloudfrontLocation { + country: string | undefined; + city: string | undefined; + region: string | undefined; + latitude: string | undefined; + longitude: string | undefined; + postalCode: string | undefined; +} + declare module "fastify" { interface FastifyInstance { authenticate: ( @@ -45,6 +54,7 @@ declare module "fastify" { userRoles?: Set; tokenPayload?: AadToken; policyRestrictions?: AvailableAuthorizationPolicy[]; + location: CloudfrontLocation; } } diff --git a/yarn.lock b/yarn.lock index 09a1bd23..a114b71f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5633,6 +5633,13 @@ fastest-levenshtein@^1.0.16: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== +fastify-ip@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fastify-ip/-/fastify-ip-1.2.0.tgz#bd65121e843f407870da11f1a721afe5083f44a1" + integrity sha512-n7BqGlEMZmaG/zEdrp7/fShBUNWfgT6EKXfC3FERTzuSPZSo8gxfq0kjD8PhAjD1I1o7oqo5Wc/m7jr+Fn+HRg== + dependencies: + fastify-plugin "^5.0.1" + fastify-plugin@^5.0.0, fastify-plugin@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-5.0.1.tgz#82d44e6fe34d1420bb5a4f7bee434d501e41939f" From 25b47d09edf4f9d0cce1d9820c20bf9f4f57060e Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 16:37:45 -0500 Subject: [PATCH 2/4] fix import --- src/api/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 04009710..d3c6a94e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -28,7 +28,7 @@ import { withTags } from "./components/index.js"; import RedisModule from "ioredis"; /** BEGIN EXTERNAL PLUGINS */ -import { FastifyIP } from "fastify-ip"; +import fastifyIp from "fastify-ip"; import cors from "@fastify/cors"; import FastifyAuthProvider from "@fastify/auth"; import fastifyStatic from "@fastify/static"; @@ -291,7 +291,7 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) { app.redisClient = new RedisModule.default(app.secretConfig.redis_url); } if (isRunningInLambda) { - app.register(FastifyIP, { + app.register(fastifyIp.FastifyIP, { order: ["x-forwarded-for"], strict: true, isAWS: false, From c5b2c22cc68154f85babef565070e6cbb8845204 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 16:49:54 -0500 Subject: [PATCH 3/4] add await --- src/api/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/index.ts b/src/api/index.ts index d3c6a94e..8779dbb1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -291,7 +291,7 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) { app.redisClient = new RedisModule.default(app.secretConfig.redis_url); } if (isRunningInLambda) { - app.register(fastifyIp.FastifyIP, { + await app.register(fastifyIp.FastifyIP, { order: ["x-forwarded-for"], strict: true, isAWS: false, From 320a6e8acd4f02a50c0e76a084725bd0f0f8c6aa Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 17:02:29 -0500 Subject: [PATCH 4/4] fix fastify ip import --- src/api/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/index.ts b/src/api/index.ts index 8779dbb1..e9104fe3 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -291,7 +291,7 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) { app.redisClient = new RedisModule.default(app.secretConfig.redis_url); } if (isRunningInLambda) { - await app.register(fastifyIp.FastifyIP, { + await app.register(fastifyIp.default, { order: ["x-forwarded-for"], strict: true, isAWS: false,