rename to resource rules and add api endpoints

This commit is contained in:
Owen 2025-02-08 17:02:22 -05:00
parent 3cd20cab55
commit da3c8823f8
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
8 changed files with 321 additions and 15 deletions

View file

@ -51,13 +51,16 @@ export enum ActionsEnum {
// removeUserAction = "removeUserAction", // removeUserAction = "removeUserAction",
// removeUserSite = "removeUserSite", // removeUserSite = "removeUserSite",
getOrgUser = "getOrgUser", getOrgUser = "getOrgUser",
"setResourcePassword" = "setResourcePassword", setResourcePassword = "setResourcePassword",
"setResourcePincode" = "setResourcePincode", setResourcePincode = "setResourcePincode",
"setResourceWhitelist" = "setResourceWhitelist", setResourceWhitelist = "setResourceWhitelist",
"getResourceWhitelist" = "getResourceWhitelist", getResourceWhitelist = "getResourceWhitelist",
"generateAccessToken" = "generateAccessToken", generateAccessToken = "generateAccessToken",
"deleteAcessToken" = "deleteAcessToken", deleteAcessToken = "deleteAcessToken",
"listAccessTokens" = "listAccessTokens" listAccessTokens = "listAccessTokens",
createResourceRule = "createResourceRule",
deleteResourceRule = "deleteResourceRule",
listResourceRules = "listResourceRules"
} }
export async function checkUserActionPermission( export async function checkUserActionPermission(

View file

@ -372,7 +372,7 @@ export const versionMigrations = sqliteTable("versionMigrations", {
executedAt: integer("executedAt").notNull() executedAt: integer("executedAt").notNull()
}); });
export const badgerRules = sqliteTable("badgerRules", { export const resourceRules = sqliteTable("resourceRules", {
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }), ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
@ -414,4 +414,4 @@ export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>; export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>; export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
export type VersionMigration = InferSelectModel<typeof versionMigrations>; export type VersionMigration = InferSelectModel<typeof versionMigrations>;
export type BadgerRule = InferSelectModel<typeof badgerRules>; export type ResourceRule = InferSelectModel<typeof resourceRules>;

View file

@ -6,8 +6,7 @@ import { fromError } from "zod-validation-error";
import { response } from "@server/lib/response"; import { response } from "@server/lib/response";
import db from "@server/db"; import db from "@server/db";
import { import {
BadgerRule, resourceRules,
badgerRules,
ResourceAccessToken, ResourceAccessToken,
ResourcePassword, ResourcePassword,
resourcePassword, resourcePassword,
@ -16,7 +15,8 @@ import {
resources, resources,
sessions, sessions,
userOrgs, userOrgs,
users users,
ResourceRule
} from "@server/db/schema"; } from "@server/db/schema";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
@ -459,13 +459,13 @@ async function checkRules(
): Promise<boolean> { ): Promise<boolean> {
const ruleCacheKey = `rules:${resourceId}`; const ruleCacheKey = `rules:${resourceId}`;
let rules: BadgerRule[] | undefined = cache.get(ruleCacheKey); let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey);
if (!rules) { if (!rules) {
rules = await db rules = await db
.select() .select()
.from(badgerRules) .from(resourceRules)
.where(eq(badgerRules.resourceId, resourceId)); .where(eq(resourceRules.resourceId, resourceId));
cache.set(ruleCacheKey, rules); cache.set(ruleCacheKey, rules);
} }

View file

@ -186,6 +186,26 @@ authenticated.get(
verifyUserHasAction(ActionsEnum.listTargets), verifyUserHasAction(ActionsEnum.listTargets),
target.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( authenticated.get(
"/target/:targetId", "/target/:targetId",
verifyTargetAccess, verifyTargetAccess,
@ -205,6 +225,7 @@ authenticated.delete(
target.deleteTarget target.deleteTarget
); );
authenticated.put( authenticated.put(
"/org/:orgId/role", "/org/:orgId/role",
verifyOrgAccess, verifyOrgAccess,

View file

@ -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<any> {
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")
);
}
}

View file

@ -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<any> {
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")
);
}
}

View file

@ -18,3 +18,6 @@ export * from "./authWithWhitelist";
export * from "./authWithAccessToken"; export * from "./authWithAccessToken";
export * from "./transferResource"; export * from "./transferResource";
export * from "./getExchangeToken"; export * from "./getExchangeToken";
export * from "./createResourceRule";
export * from "./deleteResourceRule";
export * from "./listResourceRules";

View file

@ -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<ReturnType<typeof queryResourceRules>>;
pagination: { total: number; limit: number; offset: number };
};
export async function listResourceRules(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
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<number>`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<ListResourceRulesResponse>(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")
);
}
}