From da3c8823f8404d57e7c488931a8e44972d97b28b Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 8 Feb 2025 17:02:22 -0500 Subject: [PATCH] rename to resource rules and add api endpoints --- server/auth/actions.ts | 17 ++- server/db/schema.ts | 4 +- server/routers/badger/verifySession.ts | 12 +- server/routers/external.ts | 21 +++ server/routers/resource/createResourceRule.ts | 79 +++++++++++ server/routers/resource/deleteResourceRule.ts | 67 +++++++++ server/routers/resource/index.ts | 3 + server/routers/resource/listResourceRules.ts | 133 ++++++++++++++++++ 8 files changed, 321 insertions(+), 15 deletions(-) create mode 100644 server/routers/resource/createResourceRule.ts create mode 100644 server/routers/resource/deleteResourceRule.ts create mode 100644 server/routers/resource/listResourceRules.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 8bc7b823..7c0f5d28 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -51,13 +51,16 @@ export enum ActionsEnum { // removeUserAction = "removeUserAction", // removeUserSite = "removeUserSite", getOrgUser = "getOrgUser", - "setResourcePassword" = "setResourcePassword", - "setResourcePincode" = "setResourcePincode", - "setResourceWhitelist" = "setResourceWhitelist", - "getResourceWhitelist" = "getResourceWhitelist", - "generateAccessToken" = "generateAccessToken", - "deleteAcessToken" = "deleteAcessToken", - "listAccessTokens" = "listAccessTokens" + setResourcePassword = "setResourcePassword", + setResourcePincode = "setResourcePincode", + setResourceWhitelist = "setResourceWhitelist", + getResourceWhitelist = "getResourceWhitelist", + generateAccessToken = "generateAccessToken", + deleteAcessToken = "deleteAcessToken", + listAccessTokens = "listAccessTokens", + createResourceRule = "createResourceRule", + deleteResourceRule = "deleteResourceRule", + listResourceRules = "listResourceRules" } export async function checkUserActionPermission( diff --git a/server/db/schema.ts b/server/db/schema.ts index 7c8c2739..cf5ad531 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -372,7 +372,7 @@ export const versionMigrations = sqliteTable("versionMigrations", { executedAt: integer("executedAt").notNull() }); -export const badgerRules = sqliteTable("badgerRules", { +export const resourceRules = sqliteTable("resourceRules", { ruleId: integer("ruleId").primaryKey({ autoIncrement: true }), resourceId: integer("resourceId") .notNull() @@ -414,4 +414,4 @@ export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; export type ResourceWhitelist = InferSelectModel; export type VersionMigration = InferSelectModel; -export type BadgerRule = InferSelectModel; \ No newline at end of file +export type ResourceRule = InferSelectModel; \ No newline at end of file diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index a40cfd21..12058825 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -6,8 +6,7 @@ import { fromError } from "zod-validation-error"; import { response } from "@server/lib/response"; import db from "@server/db"; import { - BadgerRule, - badgerRules, + resourceRules, ResourceAccessToken, ResourcePassword, resourcePassword, @@ -16,7 +15,8 @@ import { resources, sessions, userOrgs, - users + users, + ResourceRule } from "@server/db/schema"; import { and, eq } from "drizzle-orm"; import config from "@server/lib/config"; @@ -459,13 +459,13 @@ async function checkRules( ): Promise { const ruleCacheKey = `rules:${resourceId}`; - let rules: BadgerRule[] | undefined = cache.get(ruleCacheKey); + let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey); if (!rules) { rules = await db .select() - .from(badgerRules) - .where(eq(badgerRules.resourceId, resourceId)); + .from(resourceRules) + .where(eq(resourceRules.resourceId, resourceId)); cache.set(ruleCacheKey, rules); } diff --git a/server/routers/external.ts b/server/routers/external.ts index e74e0472..73e32911 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -186,6 +186,26 @@ authenticated.get( verifyUserHasAction(ActionsEnum.listTargets), target.listTargets ); + +authenticated.put( + "/resource/:resourceId/:ruleId", + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.createResourceRule), + resource.createResourceRule +); +authenticated.get( + "/resource/:resourceId/rules", + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.listResourceRules), + resource.listResourceRules +); +authenticated.delete( + "/resource/:resourceId/:ruleId", + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.deleteResourceRule), + resource.deleteResourceRule +); + authenticated.get( "/target/:targetId", verifyTargetAccess, @@ -205,6 +225,7 @@ authenticated.delete( target.deleteTarget ); + authenticated.put( "/org/:orgId/role", verifyOrgAccess, diff --git a/server/routers/resource/createResourceRule.ts b/server/routers/resource/createResourceRule.ts new file mode 100644 index 00000000..68578da2 --- /dev/null +++ b/server/routers/resource/createResourceRule.ts @@ -0,0 +1,79 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resourceRules, resources } from "@server/db/schema"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; + +const createResourceRuleSchema = z + .object({ + resourceId: z.number().int().positive(), + action: z.enum(["ACCEPT", "DROP"]), + match: z.enum(["CIDR", "PATH"]), + value: z.string().min(1) + }) + .strict(); + +export async function createResourceRule( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = createResourceRuleSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { resourceId, action, match, value } = parsedBody.data; + + // Verify that the referenced resource exists + const [resource] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + + if (!resource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found` + ) + ); + } + + // Create the new resource rule + const [newRule] = await db + .insert(resourceRules) + .values({ + resourceId, + action, + match, + value + }) + .returning(); + + return response(res, { + data: newRule, + success: true, + error: false, + message: "Resource rule created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} \ No newline at end of file diff --git a/server/routers/resource/deleteResourceRule.ts b/server/routers/resource/deleteResourceRule.ts new file mode 100644 index 00000000..9c19ff04 --- /dev/null +++ b/server/routers/resource/deleteResourceRule.ts @@ -0,0 +1,67 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resourceRules, resources } from "@server/db/schema"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; + +const deleteResourceRuleSchema = z + .object({ + ruleId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); + +export async function deleteResourceRule( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = deleteResourceRuleSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { ruleId } = parsedParams.data; + + // Delete the rule and return the deleted record + const [deletedRule] = await db + .delete(resourceRules) + .where(eq(resourceRules.ruleId, ruleId)) + .returning(); + + if (!deletedRule) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource rule with ID ${ruleId} not found` + ) + ); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Resource rule deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} \ No newline at end of file diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 187d23fe..b3e8016f 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -18,3 +18,6 @@ export * from "./authWithWhitelist"; export * from "./authWithAccessToken"; export * from "./transferResource"; export * from "./getExchangeToken"; +export * from "./createResourceRule"; +export * from "./deleteResourceRule"; +export * from "./listResourceRules"; \ No newline at end of file diff --git a/server/routers/resource/listResourceRules.ts b/server/routers/resource/listResourceRules.ts new file mode 100644 index 00000000..02c774a6 --- /dev/null +++ b/server/routers/resource/listResourceRules.ts @@ -0,0 +1,133 @@ +import { db } from "@server/db"; +import { resourceRules, resources } from "@server/db/schema"; +import HttpCode from "@server/types/HttpCode"; +import response from "@server/lib/response"; +import { eq, sql } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; + +const listResourceRulesParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); + +const listResourceRulesSchema = z.object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().positive()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) +}); + +function queryResourceRules(resourceId: number) { + let baseQuery = db + .select({ + ruleId: resourceRules.ruleId, + resourceId: resourceRules.resourceId, + action: resourceRules.action, + match: resourceRules.match, + value: resourceRules.value, + resourceName: resources.name, + }) + .from(resourceRules) + .leftJoin(resources, eq(resourceRules.resourceId, resources.resourceId)) + .where(eq(resourceRules.resourceId, resourceId)); + + return baseQuery; +} + +export type ListResourceRulesResponse = { + rules: Awaited>; + pagination: { total: number; limit: number; offset: number }; +}; + +export async function listResourceRules( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = listResourceRulesSchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error) + ) + ); + } + const { limit, offset } = parsedQuery.data; + + const parsedParams = listResourceRulesParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error) + ) + ); + } + const { resourceId } = parsedParams.data; + + // Verify the resource exists + const [resource] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + + if (!resource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found` + ) + ); + } + + const baseQuery = queryResourceRules(resourceId); + + let countQuery = db + .select({ count: sql`cast(count(*) as integer)` }) + .from(resourceRules) + .where(eq(resourceRules.resourceId, resourceId)); + + const rulesList = await baseQuery.limit(limit).offset(offset); + const totalCountResult = await countQuery; + const totalCount = totalCountResult[0].count; + + return response(res, { + data: { + rules: rulesList, + pagination: { + total: totalCount, + limit, + offset + } + }, + success: true, + error: false, + message: "Resource rules retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} \ No newline at end of file