working on request blocker
This commit is contained in:
@@ -74,6 +74,7 @@
|
||||
"p": "ENV_FILE=.env.prod infisical run --env=prod -- bun scripts/dev.ts",
|
||||
"l": "bash ./scripts/dev-local.sh",
|
||||
"setup": "node scripts/setup/setup.js",
|
||||
"setup:s3-admin": "bun scripts/setup/setupS3Admin.ts",
|
||||
"setup:test": "infisical run --env=dev -- bun scripts/setup/setup-test.ts",
|
||||
"migrate-functions": "infisical run --env=dev -- bun scripts/migrations/migrate-functions.ts",
|
||||
"migrate-functions:test": "infisical run --env=test -- bun scripts/migrations/migrate-functions.ts",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"replicate": "bun run db/replicate.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1017.0",
|
||||
"@autumn/shared": "workspace:*",
|
||||
"chalk": "^5.3.0",
|
||||
"dotenv": "^16.5.0",
|
||||
|
||||
203
scripts/setup/setupS3Admin.ts
Normal file
203
scripts/setup/setupS3Admin.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env bun
|
||||
import {
|
||||
BucketAlreadyExists,
|
||||
BucketAlreadyOwnedByYou,
|
||||
type BucketLocationConstraint,
|
||||
CreateBucketCommand,
|
||||
HeadBucketCommand,
|
||||
HeadObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import {
|
||||
ADMIN_REQUEST_BLOCK_CONFIG_KEY as REQUEST_BLOCK_CONFIG_KEY,
|
||||
getAdminS3Config,
|
||||
} from "@server/external/aws/s3/adminS3Config.js";
|
||||
const DEFAULT_REQUEST_BLOCK_CONFIG = {
|
||||
orgs: {},
|
||||
};
|
||||
|
||||
const getTargetFromArgs = () => {
|
||||
const hasDevFlag = process.argv.includes("--dev");
|
||||
const hasProdFlag = process.argv.includes("--prod");
|
||||
|
||||
if (hasDevFlag && hasProdFlag) {
|
||||
throw new Error("Use either --dev or --prod, not both");
|
||||
}
|
||||
|
||||
if (hasDevFlag) return "dev" as const;
|
||||
if (hasProdFlag) return "prod" as const;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const createS3Client = ({ region }: { region: string }) => {
|
||||
return new S3Client({ region });
|
||||
};
|
||||
|
||||
const getHttpStatusCode = ({ error }: { error: unknown }) => {
|
||||
if (!error || typeof error !== "object") return undefined;
|
||||
|
||||
const errorWithMetadata = error as {
|
||||
$metadata?: {
|
||||
httpStatusCode?: number;
|
||||
};
|
||||
};
|
||||
|
||||
return errorWithMetadata.$metadata?.httpStatusCode;
|
||||
};
|
||||
|
||||
const isMissingS3ResourceError = ({ error }: { error: unknown }) => {
|
||||
if (error instanceof Error) {
|
||||
if (error.name === "NotFound" || error.name === "NoSuchBucket") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return getHttpStatusCode({ error }) === 404;
|
||||
};
|
||||
|
||||
const bucketExists = async ({
|
||||
s3Client,
|
||||
bucket,
|
||||
}: {
|
||||
s3Client: S3Client;
|
||||
bucket: string;
|
||||
}) => {
|
||||
try {
|
||||
await s3Client.send(
|
||||
new HeadBucketCommand({
|
||||
Bucket: bucket,
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isMissingS3ResourceError({ error })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const ensureBucketExists = async ({
|
||||
s3Client,
|
||||
bucket,
|
||||
region,
|
||||
}: {
|
||||
s3Client: S3Client;
|
||||
bucket: string;
|
||||
region: string;
|
||||
}) => {
|
||||
const exists = await bucketExists({ s3Client, bucket });
|
||||
if (exists) {
|
||||
console.log(`Bucket already exists: ${bucket}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await s3Client.send(
|
||||
new CreateBucketCommand({
|
||||
Bucket: bucket,
|
||||
...(region === "us-east-1"
|
||||
? {}
|
||||
: {
|
||||
CreateBucketConfiguration: {
|
||||
LocationConstraint: region as BucketLocationConstraint,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
console.log(`Created bucket: ${bucket}`);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BucketAlreadyOwnedByYou ||
|
||||
error instanceof BucketAlreadyExists
|
||||
) {
|
||||
console.log(`Bucket already exists: ${bucket}`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const objectExists = async ({
|
||||
s3Client,
|
||||
bucket,
|
||||
key,
|
||||
}: {
|
||||
s3Client: S3Client;
|
||||
bucket: string;
|
||||
key: string;
|
||||
}) => {
|
||||
try {
|
||||
await s3Client.send(
|
||||
new HeadObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isMissingS3ResourceError({ error })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const ensureRequestBlockConfigExists = async ({
|
||||
s3Client,
|
||||
bucket,
|
||||
}: {
|
||||
s3Client: S3Client;
|
||||
bucket: string;
|
||||
}) => {
|
||||
const exists = await objectExists({
|
||||
s3Client,
|
||||
bucket,
|
||||
key: REQUEST_BLOCK_CONFIG_KEY,
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
console.log(`Admin config already exists: ${REQUEST_BLOCK_CONFIG_KEY}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await s3Client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: REQUEST_BLOCK_CONFIG_KEY,
|
||||
Body: JSON.stringify(DEFAULT_REQUEST_BLOCK_CONFIG, null, 2),
|
||||
ContentType: "application/json",
|
||||
}),
|
||||
);
|
||||
|
||||
console.log(`Created admin config: ${REQUEST_BLOCK_CONFIG_KEY}`);
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const target = getTargetFromArgs();
|
||||
const { bucket, region } = getAdminS3Config({ target });
|
||||
const s3Client = createS3Client({ region });
|
||||
|
||||
console.log(
|
||||
`Initializing S3 admin config for ${target || process.env.NODE_ENV || "default"} -> s3://${bucket}/${REQUEST_BLOCK_CONFIG_KEY}`,
|
||||
);
|
||||
|
||||
await ensureBucketExists({
|
||||
s3Client,
|
||||
bucket,
|
||||
region,
|
||||
});
|
||||
|
||||
await ensureRequestBlockConfigExists({
|
||||
s3Client,
|
||||
bucket,
|
||||
});
|
||||
|
||||
console.log("S3 admin initialization complete.");
|
||||
};
|
||||
|
||||
await main();
|
||||
29
server/src/external/aws/s3/adminS3Config.ts
vendored
Normal file
29
server/src/external/aws/s3/adminS3Config.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
export const ADMIN_REQUEST_BLOCK_CONFIG_KEY = "admin/request-block-config.json";
|
||||
|
||||
type AdminS3Target = "dev" | "prod";
|
||||
|
||||
const isDevTarget = ({ target }: { target?: AdminS3Target }) => {
|
||||
if (target) return target === "dev";
|
||||
return (
|
||||
process.env.NODE_ENV === "dev" ||
|
||||
process.env.NODE_ENV === "development"
|
||||
);
|
||||
};
|
||||
|
||||
export const getAdminS3Config = ({
|
||||
target,
|
||||
}: {
|
||||
target?: AdminS3Target;
|
||||
} = {}) => {
|
||||
if (isDevTarget({ target })) {
|
||||
return {
|
||||
bucket: "autumn-dev-server",
|
||||
region: "eu-west-2",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
bucket: "autumn-prod-server",
|
||||
region: "us-east-2",
|
||||
};
|
||||
};
|
||||
17
server/src/external/aws/s3/initS3.ts
vendored
Normal file
17
server/src/external/aws/s3/initS3.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
import { DEFAULT_AWS_REGION } from "@/external/aws/awsRegionUtils.js";
|
||||
|
||||
const s3ClientsByRegion = new Map<string, S3Client>();
|
||||
|
||||
export const getS3Client = ({
|
||||
region = DEFAULT_AWS_REGION,
|
||||
}: {
|
||||
region?: string;
|
||||
}) => {
|
||||
const existingClient = s3ClientsByRegion.get(region);
|
||||
if (existingClient) return existingClient;
|
||||
|
||||
const s3Client = new S3Client({ region });
|
||||
s3ClientsByRegion.set(region, s3Client);
|
||||
return s3Client;
|
||||
};
|
||||
11
server/src/external/aws/s3/s3Utils.ts
vendored
Normal file
11
server/src/external/aws/s3/s3Utils.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export const getS3BodyAsString = async ({
|
||||
body,
|
||||
}: {
|
||||
body: { transformToString?: () => Promise<string> };
|
||||
}) => {
|
||||
if (typeof body.transformToString === "function") {
|
||||
return await body.transformToString();
|
||||
}
|
||||
|
||||
return await new Response(body as BodyInit).text();
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ErrCode, RecaseError } from "@autumn/shared";
|
||||
import type { Context, Next } from "hono";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import { getRuntimeRequestBlockEntry } from "@/internal/misc/requestBlocks/requestBlockStore.js";
|
||||
import { matchRoute } from "./middlewareUtils.js";
|
||||
import { getRuntimeRequestBlockEntry } from "@/internal/requestBlocks/requestBlockStore.js";
|
||||
|
||||
export const requestBlockMiddleware = async (
|
||||
c: Context<HonoEnv>,
|
||||
@@ -22,14 +22,10 @@ export const requestBlockMiddleware = async (
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Entry:", entry);
|
||||
|
||||
if (entry.blockAll) {
|
||||
ctx.logger.warn("Rejecting blocked /v1 request", {
|
||||
orgId,
|
||||
orgSlug: ctx.org?.slug,
|
||||
method: c.req.method,
|
||||
path: c.req.path,
|
||||
blockAll: true,
|
||||
});
|
||||
ctx.logger.warn("Rejecting blocked /v1 request (block all)");
|
||||
|
||||
throw new RecaseError({
|
||||
message: "API access is temporarily disabled for this organization",
|
||||
@@ -54,13 +50,9 @@ export const requestBlockMiddleware = async (
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.logger.warn("Rejecting endpoint-blocked /v1 request", {
|
||||
orgId,
|
||||
orgSlug: ctx.org?.slug,
|
||||
method: c.req.method,
|
||||
path: c.req.path,
|
||||
rule: matchedRule,
|
||||
});
|
||||
ctx.logger.warn(
|
||||
"Rejecting endpoint-blocked /v1 request (blocked endpoint, matched rule)",
|
||||
);
|
||||
|
||||
throw new RecaseError({
|
||||
message: "This endpoint is temporarily disabled for this organization",
|
||||
|
||||
@@ -14,7 +14,7 @@ import { logger } from "./external/logtail/logtailUtils.js";
|
||||
import {
|
||||
startRequestBlockPolling,
|
||||
stopRequestBlockPolling,
|
||||
} from "./internal/requestBlocks/requestBlockStore.js";
|
||||
} from "./internal/misc/requestBlocks/requestBlockStore.js";
|
||||
import { warmupRegionalRedis } from "./external/redis/initRedis.js";
|
||||
import { createHonoApp } from "./initHono.js";
|
||||
import { otelSdk } from "./instrumentation.js";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import {
|
||||
getOrgRequestBlockFromSource,
|
||||
getRuntimeRequestBlockStatus,
|
||||
} from "@/internal/requestBlocks/requestBlockStore.js";
|
||||
} from "@/internal/misc/requestBlocks/requestBlockStore.js";
|
||||
|
||||
export const handleGetAdminOrgRequestBlock = createRoute({
|
||||
params: z.object({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { member, organizations, user } from "@autumn/shared";
|
||||
import { and, desc, eq, gt, gte, ilike, inArray, lt, or } from "drizzle-orm";
|
||||
import { getRequestBlockConfigFromSource } from "../requestBlocks/requestBlockStore.js";
|
||||
import { getRequestBlockConfigFromSource } from "../misc/requestBlocks/requestBlockStore.js";
|
||||
import { createRoute } from "../../honoMiddlewares/routeHandler";
|
||||
|
||||
export const handleListAdminOrgs = createRoute({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod/v4";
|
||||
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import { updateOrgRequestBlockInSource } from "@/internal/requestBlocks/requestBlockStore.js";
|
||||
import { RequestBlockUpdateSchema } from "@/internal/requestBlocks/requestBlockSchemas.js";
|
||||
import { updateOrgRequestBlockInSource } from "@/internal/misc/requestBlocks/requestBlockStore.js";
|
||||
import { RequestBlockUpdateSchema } from "@/internal/misc/requestBlocks/requestBlockSchemas.js";
|
||||
|
||||
export const handleUpsertAdminOrgRequestBlock = createRoute({
|
||||
params: z.object({
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import {
|
||||
ADMIN_REQUEST_BLOCK_CONFIG_KEY,
|
||||
getAdminS3Config,
|
||||
} from "@/external/aws/s3/adminS3Config.js";
|
||||
import { getS3Client } from "@/external/aws/s3/initS3.js";
|
||||
import { getS3BodyAsString } from "@/external/aws/s3/s3Utils.js";
|
||||
import type { Logger } from "@/external/logtail/logtailUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import { DEFAULT_AWS_REGION } from "@/external/aws/awsRegionUtils.js";
|
||||
import {
|
||||
RequestBlockConfigSchema,
|
||||
type RequestBlockConfig,
|
||||
RequestBlockConfigSchema,
|
||||
type RequestBlockEntry,
|
||||
type RequestBlockUpdate,
|
||||
} from "./requestBlockSchemas.js";
|
||||
@@ -31,10 +36,8 @@ let runtimeStatus: RequestBlockStatus = {
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const getConfigLocation = () => {
|
||||
const bucket = process.env.REQUEST_BLOCK_CONFIG_S3_BUCKET;
|
||||
const key = process.env.REQUEST_BLOCK_CONFIG_S3_KEY;
|
||||
const region =
|
||||
process.env.REQUEST_BLOCK_CONFIG_S3_REGION || DEFAULT_AWS_REGION;
|
||||
const { bucket, region } = getAdminS3Config();
|
||||
const key = ADMIN_REQUEST_BLOCK_CONFIG_KEY;
|
||||
|
||||
return {
|
||||
bucket,
|
||||
@@ -44,27 +47,14 @@ const getConfigLocation = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const getS3Client = () => {
|
||||
const { region } = getConfigLocation();
|
||||
return new S3Client({ region });
|
||||
};
|
||||
|
||||
const streamToString = async (body: { transformToString?: () => Promise<string> }) => {
|
||||
if (typeof body.transformToString === "function") {
|
||||
return await body.transformToString();
|
||||
}
|
||||
|
||||
return await new Response(body as BodyInit).text();
|
||||
};
|
||||
|
||||
const readConfigFromS3 = async (): Promise<RequestBlockConfig> => {
|
||||
const { bucket, key, configured } = getConfigLocation();
|
||||
const { bucket, key, configured, region } = getConfigLocation();
|
||||
|
||||
if (!configured || !bucket || !key) {
|
||||
return emptyConfig();
|
||||
}
|
||||
|
||||
const client = getS3Client();
|
||||
const client = getS3Client({ region });
|
||||
try {
|
||||
const response = await client.send(
|
||||
new GetObjectCommand({
|
||||
@@ -77,7 +67,7 @@ const readConfigFromS3 = async (): Promise<RequestBlockConfig> => {
|
||||
return emptyConfig();
|
||||
}
|
||||
|
||||
const raw = (await streamToString(response.Body)).trim();
|
||||
const raw = (await getS3BodyAsString({ body: response.Body })).trim();
|
||||
if (!raw) {
|
||||
return emptyConfig();
|
||||
}
|
||||
@@ -93,7 +83,7 @@ const readConfigFromS3 = async (): Promise<RequestBlockConfig> => {
|
||||
};
|
||||
|
||||
const writeConfigToS3 = async (config: RequestBlockConfig) => {
|
||||
const { bucket, key, configured } = getConfigLocation();
|
||||
const { bucket, key, configured, region } = getConfigLocation();
|
||||
|
||||
if (!configured || !bucket || !key) {
|
||||
throw new RecaseError({
|
||||
@@ -103,7 +93,7 @@ const writeConfigToS3 = async (config: RequestBlockConfig) => {
|
||||
});
|
||||
}
|
||||
|
||||
const client = getS3Client();
|
||||
const client = getS3Client({ region });
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
@@ -166,7 +156,7 @@ export const refreshRequestBlockConfig = async ({
|
||||
lastSuccessAt: runtimeStatus.lastSuccessAt,
|
||||
error: error instanceof Error ? error.message : "Failed to load config",
|
||||
};
|
||||
logger?.error("Failed to refresh request block config", { error });
|
||||
logger?.warn(`Failed to refresh request block config: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -211,8 +201,7 @@ export const updateOrgRequestBlockInSource = async ({
|
||||
updatedBy?: string;
|
||||
}) => {
|
||||
const config = await readConfigFromS3();
|
||||
const shouldDelete =
|
||||
!update.blockAll && update.blockedEndpoints.length === 0;
|
||||
const shouldDelete = !update.blockAll && update.blockedEndpoints.length === 0;
|
||||
|
||||
if (shouldDelete) {
|
||||
delete config.orgs[orgId];
|
||||
Reference in New Issue
Block a user