mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-16 01:46:38 +00:00
Scoped Branch - Rule Templates:
- Add rule templates for reusable access control rules - Support template assignment to resources with automatic rule propagation - Add template management UI - Implement template rule protection on resource rules page
This commit is contained in:
@@ -406,6 +406,8 @@ export const resourceRules = pgTable("resourceRules", {
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
templateRuleId: integer("templateRuleId")
|
||||
.references(() => templateRules.ruleId, { onDelete: "cascade" }),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: varchar("action").notNull(), // ACCEPT, DROP
|
||||
@@ -413,6 +415,40 @@ export const resourceRules = pgTable("resourceRules", {
|
||||
value: varchar("value").notNull()
|
||||
});
|
||||
|
||||
// Rule templates (reusable rule sets)
|
||||
export const ruleTemplates = pgTable("ruleTemplates", {
|
||||
templateId: varchar("templateId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
name: varchar("name").notNull(),
|
||||
description: varchar("description"),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
// Rules within templates
|
||||
export const templateRules = pgTable("templateRules", {
|
||||
ruleId: serial("ruleId").primaryKey(),
|
||||
templateId: varchar("templateId")
|
||||
.notNull()
|
||||
.references(() => ruleTemplates.templateId, { onDelete: "cascade" }),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: varchar("action").notNull(), // ACCEPT, DROP
|
||||
match: varchar("match").notNull(), // CIDR, IP, PATH
|
||||
value: varchar("value").notNull()
|
||||
});
|
||||
|
||||
// Template assignments to resources
|
||||
export const resourceTemplates = pgTable("resourceTemplates", {
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
templateId: varchar("templateId")
|
||||
.notNull()
|
||||
.references(() => ruleTemplates.templateId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const supporterKey = pgTable("supporterKey", {
|
||||
keyId: serial("keyId").primaryKey(),
|
||||
key: varchar("key").notNull(),
|
||||
@@ -637,3 +673,6 @@ export type OlmSession = InferSelectModel<typeof olmSessions>;
|
||||
export type UserClient = InferSelectModel<typeof userClients>;
|
||||
export type RoleClient = InferSelectModel<typeof roleClients>;
|
||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||
export type RuleTemplate = InferSelectModel<typeof ruleTemplates>;
|
||||
export type TemplateRule = InferSelectModel<typeof templateRules>;
|
||||
export type ResourceTemplate = InferSelectModel<typeof resourceTemplates>;
|
||||
|
||||
@@ -534,6 +534,8 @@ export const resourceRules = sqliteTable("resourceRules", {
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
templateRuleId: integer("templateRuleId")
|
||||
.references(() => templateRules.ruleId, { onDelete: "cascade" }),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: text("action").notNull(), // ACCEPT, DROP
|
||||
@@ -541,6 +543,40 @@ export const resourceRules = sqliteTable("resourceRules", {
|
||||
value: text("value").notNull()
|
||||
});
|
||||
|
||||
// Rule templates (reusable rule sets)
|
||||
export const ruleTemplates = sqliteTable("ruleTemplates", {
|
||||
templateId: text("templateId").primaryKey(),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
createdAt: integer("createdAt").notNull()
|
||||
});
|
||||
|
||||
// Rules within templates
|
||||
export const templateRules = sqliteTable("templateRules", {
|
||||
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
|
||||
templateId: text("templateId")
|
||||
.notNull()
|
||||
.references(() => ruleTemplates.templateId, { onDelete: "cascade" }),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: text("action").notNull(), // ACCEPT, DROP
|
||||
match: text("match").notNull(), // CIDR, IP, PATH
|
||||
value: text("value").notNull()
|
||||
});
|
||||
|
||||
// Template assignments to resources
|
||||
export const resourceTemplates = sqliteTable("resourceTemplates", {
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
templateId: text("templateId")
|
||||
.notNull()
|
||||
.references(() => ruleTemplates.templateId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const supporterKey = sqliteTable("supporterKey", {
|
||||
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
||||
key: text("key").notNull(),
|
||||
@@ -679,3 +715,6 @@ export type ApiKey = InferSelectModel<typeof apiKeys>;
|
||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||
export type RuleTemplate = InferSelectModel<typeof ruleTemplates>;
|
||||
export type TemplateRule = InferSelectModel<typeof templateRules>;
|
||||
export type ResourceTemplate = InferSelectModel<typeof resourceTemplates>;
|
||||
|
||||
@@ -11,6 +11,7 @@ export enum OpenAPITags {
|
||||
Invitation = "Invitation",
|
||||
Target = "Target",
|
||||
Rule = "Rule",
|
||||
RuleTemplate = "Rule Template",
|
||||
AccessToken = "Access Token",
|
||||
Idp = "Identity Provider",
|
||||
Client = "Client",
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as accessToken from "./accessToken";
|
||||
import * as idp from "./idp";
|
||||
import * as license from "./license";
|
||||
import * as apiKeys from "./apiKeys";
|
||||
import * as ruleTemplate from "./ruleTemplate";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import {
|
||||
verifyAccessTokenAccess,
|
||||
@@ -339,6 +340,80 @@ authenticated.delete(
|
||||
resource.deleteResourceRule
|
||||
);
|
||||
|
||||
// Rule template routes
|
||||
authenticated.post(
|
||||
"/org/:orgId/rule-templates",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createResourceRule),
|
||||
ruleTemplate.createRuleTemplate
|
||||
);
|
||||
authenticated.get(
|
||||
"/org/:orgId/rule-templates",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.listResourceRules),
|
||||
ruleTemplate.listRuleTemplates
|
||||
);
|
||||
authenticated.get(
|
||||
"/org/:orgId/rule-templates/:templateId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.listResourceRules),
|
||||
ruleTemplate.getRuleTemplate
|
||||
);
|
||||
authenticated.put(
|
||||
"/org/:orgId/rule-templates/:templateId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createResourceRule),
|
||||
ruleTemplate.updateRuleTemplate
|
||||
);
|
||||
authenticated.get(
|
||||
"/org/:orgId/rule-templates/:templateId/rules",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.listResourceRules),
|
||||
ruleTemplate.listTemplateRules
|
||||
);
|
||||
authenticated.post(
|
||||
"/org/:orgId/rule-templates/:templateId/rules",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createResourceRule),
|
||||
ruleTemplate.addTemplateRule
|
||||
);
|
||||
authenticated.put(
|
||||
"/org/:orgId/rule-templates/:templateId/rules/:ruleId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createResourceRule),
|
||||
ruleTemplate.updateTemplateRule
|
||||
);
|
||||
authenticated.delete(
|
||||
"/org/:orgId/rule-templates/:templateId/rules/:ruleId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteResourceRule),
|
||||
ruleTemplate.deleteTemplateRule
|
||||
);
|
||||
authenticated.delete(
|
||||
"/org/:orgId/rule-templates/:templateId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteResourceRule),
|
||||
ruleTemplate.deleteRuleTemplate
|
||||
);
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/templates/:templateId",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.createResourceRule),
|
||||
ruleTemplate.assignTemplateToResource
|
||||
);
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId/templates/:templateId",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteResourceRule),
|
||||
ruleTemplate.unassignTemplateFromResource
|
||||
);
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/templates",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.listResourceRules),
|
||||
ruleTemplate.listResourceTemplates
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/target/:targetId",
|
||||
verifyTargetAccess,
|
||||
|
||||
@@ -39,6 +39,7 @@ function queryResourceRules(resourceId: number) {
|
||||
.select({
|
||||
ruleId: resourceRules.ruleId,
|
||||
resourceId: resourceRules.resourceId,
|
||||
templateRuleId: resourceRules.templateRuleId,
|
||||
action: resourceRules.action,
|
||||
match: resourceRules.match,
|
||||
value: resourceRules.value,
|
||||
|
||||
161
server/routers/ruleTemplate/addTemplateRule.ts
Normal file
161
server/routers/ruleTemplate/addTemplateRule.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { templateRules, ruleTemplates } from "@server/db";
|
||||
import { eq, and } 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";
|
||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "@server/lib/validators";
|
||||
|
||||
const addTemplateRuleParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string().min(1),
|
||||
templateId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
const addTemplateRuleBodySchema = z
|
||||
.object({
|
||||
action: z.enum(["ACCEPT", "DROP"]),
|
||||
match: z.enum(["CIDR", "IP", "PATH"]),
|
||||
value: z.string().min(1),
|
||||
priority: z.number().int().optional(),
|
||||
enabled: z.boolean().optional()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function addTemplateRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = addTemplateRuleParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = addTemplateRuleBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, templateId } = parsedParams.data;
|
||||
const { action, match, value, priority, enabled = true } = parsedBody.data;
|
||||
|
||||
// Check if template exists and belongs to the organization
|
||||
const existingTemplate = await db
|
||||
.select()
|
||||
.from(ruleTemplates)
|
||||
.where(and(eq(ruleTemplates.orgId, orgId), eq(ruleTemplates.templateId, templateId)))
|
||||
.limit(1);
|
||||
|
||||
if (existingTemplate.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Rule template not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Validate the value based on match type
|
||||
if (match === "CIDR" && !isValidCIDR(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid CIDR format"
|
||||
)
|
||||
);
|
||||
}
|
||||
if (match === "IP" && !isValidIP(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid IP address format"
|
||||
)
|
||||
);
|
||||
}
|
||||
if (match === "PATH" && !isValidUrlGlobPattern(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid URL pattern format"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check for duplicate rule
|
||||
const existingRule = await db
|
||||
.select()
|
||||
.from(templateRules)
|
||||
.where(and(
|
||||
eq(templateRules.templateId, templateId),
|
||||
eq(templateRules.action, action),
|
||||
eq(templateRules.match, match),
|
||||
eq(templateRules.value, value)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (existingRule.length > 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
"Rule already exists"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Determine priority if not provided
|
||||
let finalPriority = priority;
|
||||
if (finalPriority === undefined) {
|
||||
const maxPriority = await db
|
||||
.select({ maxPriority: templateRules.priority })
|
||||
.from(templateRules)
|
||||
.where(eq(templateRules.templateId, templateId))
|
||||
.orderBy(templateRules.priority)
|
||||
.limit(1);
|
||||
|
||||
finalPriority = (maxPriority[0]?.maxPriority || 0) + 1;
|
||||
}
|
||||
|
||||
// Add the rule
|
||||
const [newRule] = await db
|
||||
.insert(templateRules)
|
||||
.values({
|
||||
templateId,
|
||||
action,
|
||||
match,
|
||||
value,
|
||||
priority: finalPriority,
|
||||
enabled
|
||||
})
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: newRule,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Template rule added successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
176
server/routers/ruleTemplate/assignTemplateToResource.ts
Normal file
176
server/routers/ruleTemplate/assignTemplateToResource.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resourceTemplates, ruleTemplates, resources, templateRules, resourceRules } from "@server/db";
|
||||
import { eq, and } 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";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const assignTemplateToResourceParamsSchema = z
|
||||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
templateId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource/{resourceId}/templates/{templateId}",
|
||||
description: "Assign a template to a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.RuleTemplate],
|
||||
request: {
|
||||
params: assignTemplateToResourceParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function assignTemplateToResource(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = assignTemplateToResourceParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId, templateId } = parsedParams.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`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the template exists
|
||||
const [template] = await db
|
||||
.select()
|
||||
.from(ruleTemplates)
|
||||
.where(eq(ruleTemplates.templateId, templateId))
|
||||
.limit(1);
|
||||
|
||||
if (!template) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Rule template with ID ${templateId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the template belongs to the same organization as the resource
|
||||
if (template.orgId !== resource.orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Template ${templateId} does not belong to the same organization as resource ${resourceId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the template is already assigned to this resource
|
||||
const [existingAssignment] = await db
|
||||
.select()
|
||||
.from(resourceTemplates)
|
||||
.where(and(eq(resourceTemplates.resourceId, resourceId), eq(resourceTemplates.templateId, templateId)))
|
||||
.limit(1);
|
||||
|
||||
if (existingAssignment) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
`Template ${templateId} is already assigned to resource ${resourceId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Assign the template to the resource
|
||||
await db
|
||||
.insert(resourceTemplates)
|
||||
.values({
|
||||
resourceId,
|
||||
templateId
|
||||
});
|
||||
|
||||
// Automatically sync the template rules to the resource
|
||||
try {
|
||||
// Get all rules from the template
|
||||
const templateRulesList = await db
|
||||
.select()
|
||||
.from(templateRules)
|
||||
.where(eq(templateRules.templateId, templateId))
|
||||
.orderBy(templateRules.priority);
|
||||
|
||||
if (templateRulesList.length > 0) {
|
||||
// Get existing resource rules to calculate the next priority
|
||||
const existingRules = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.resourceId, resourceId))
|
||||
.orderBy(resourceRules.priority);
|
||||
|
||||
// Calculate the starting priority for new template rules
|
||||
// They should come after the highest existing priority
|
||||
const maxExistingPriority = existingRules.length > 0
|
||||
? Math.max(...existingRules.map(r => r.priority))
|
||||
: 0;
|
||||
|
||||
// Create new resource rules from template rules with adjusted priorities
|
||||
const newRules = templateRulesList.map((templateRule, index) => ({
|
||||
resourceId,
|
||||
templateRuleId: templateRule.ruleId, // Link to the template rule
|
||||
action: templateRule.action,
|
||||
match: templateRule.match,
|
||||
value: templateRule.value,
|
||||
priority: maxExistingPriority + index + 1, // Simple sequential ordering
|
||||
enabled: templateRule.enabled
|
||||
}));
|
||||
|
||||
await db
|
||||
.insert(resourceRules)
|
||||
.values(newRules);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error auto-syncing template rules during assignment:", error);
|
||||
// Don't fail the assignment if sync fails, just log it
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: { resourceId, templateId },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Template assigned to resource successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
121
server/routers/ruleTemplate/createRuleTemplate.ts
Normal file
121
server/routers/ruleTemplate/createRuleTemplate.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { ruleTemplates } from "@server/db";
|
||||
import { eq, and } 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";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
|
||||
const createRuleTemplateParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
const createRuleTemplateBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(100).refine(name => name.trim().length > 0, {
|
||||
message: "Template name cannot be empty or just whitespace"
|
||||
}),
|
||||
description: z.string().max(500).optional()
|
||||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}/rule-templates",
|
||||
description: "Create a rule template.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.RuleTemplate],
|
||||
request: {
|
||||
params: createRuleTemplateParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createRuleTemplateBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createRuleTemplate(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = createRuleTemplateParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = createRuleTemplateBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
const { name, description } = parsedBody.data;
|
||||
|
||||
// Check if template with same name already exists
|
||||
const existingTemplate = await db
|
||||
.select()
|
||||
.from(ruleTemplates)
|
||||
.where(and(eq(ruleTemplates.orgId, orgId), eq(ruleTemplates.name, name)))
|
||||
.limit(1);
|
||||
|
||||
if (existingTemplate.length > 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
`A template with the name "${name}" already exists in this organization`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const templateId = generateId(15);
|
||||
const createdAt = Date.now();
|
||||
|
||||
const [newTemplate] = await db
|
||||
.insert(ruleTemplates)
|
||||
.values({
|
||||
templateId,
|
||||
orgId,
|
||||
name,
|
||||
description: description || null,
|
||||
createdAt
|
||||
})
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: newTemplate,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Rule template created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
60
server/routers/ruleTemplate/deleteRuleTemplate.ts
Normal file
60
server/routers/ruleTemplate/deleteRuleTemplate.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { ruleTemplates, templateRules, resourceTemplates } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags } from "@server/openApi";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
|
||||
const deleteRuleTemplateSchema = z.object({
|
||||
orgId: z.string().min(1),
|
||||
templateId: z.string().min(1)
|
||||
});
|
||||
|
||||
export async function deleteRuleTemplate(req: any, res: any) {
|
||||
try {
|
||||
const { orgId, templateId } = deleteRuleTemplateSchema.parse({
|
||||
orgId: req.params.orgId,
|
||||
templateId: req.params.templateId
|
||||
});
|
||||
|
||||
// Check if template exists and belongs to the organization
|
||||
const existingTemplate = await db
|
||||
.select()
|
||||
.from(ruleTemplates)
|
||||
.where(and(eq(ruleTemplates.orgId, orgId), eq(ruleTemplates.templateId, templateId)))
|
||||
.limit(1);
|
||||
|
||||
if (existingTemplate.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Rule template not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Delete template rules first (due to foreign key constraint)
|
||||
await db
|
||||
.delete(templateRules)
|
||||
.where(eq(templateRules.templateId, templateId));
|
||||
|
||||
// Delete resource template assignments
|
||||
await db
|
||||
.delete(resourceTemplates)
|
||||
.where(eq(resourceTemplates.templateId, templateId));
|
||||
|
||||
// Delete the template
|
||||
await db
|
||||
.delete(ruleTemplates)
|
||||
.where(and(eq(ruleTemplates.orgId, orgId), eq(ruleTemplates.templateId, templateId)));
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Rule template deleted successfully"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting rule template:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error"
|
||||
});
|
||||
}
|
||||
}
|
||||
100
server/routers/ruleTemplate/deleteTemplateRule.ts
Normal file
100
server/routers/ruleTemplate/deleteTemplateRule.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { templateRules, ruleTemplates } from "@server/db";
|
||||
import { eq, and } 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 deleteTemplateRuleParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string().min(1),
|
||||
templateId: z.string().min(1),
|
||||
ruleId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function deleteTemplateRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = deleteTemplateRuleParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, templateId, ruleId } = parsedParams.data;
|
||||
|
||||
// Check if template exists and belongs to the organization
|
||||
const existingTemplate = await db
|
||||
.select()
|
||||
.from(ruleTemplates)
|
||||
.where(and(eq(ruleTemplates.orgId, orgId), eq(ruleTemplates.templateId, templateId)))
|
||||
.limit(1);
|
||||
|
||||
if (existingTemplate.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Rule template not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if rule exists and belongs to the template
|
||||
const existingRule = await db
|
||||
.select()
|
||||
.from(templateRules)
|
||||
.where(and(eq(templateRules.templateId, templateId), eq(templateRules.ruleId, parseInt(ruleId))))
|
||||
.limit(1);
|
||||
|
||||
if (existingRule.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Template rule not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Delete the rule
|
||||
await db
|
||||
.delete(templateRules)
|
||||
.where(and(eq(templateRules.templateId, templateId), eq(templateRules.ruleId, parseInt(ruleId))));
|
||||
|
||||
// Also delete all resource rules that were created from this template rule
|
||||
try {
|
||||
const { resourceRules } = await import("@server/db");
|
||||
|
||||
await db
|
||||
.delete(resourceRules)
|
||||
.where(eq(resourceRules.templateRuleId, parseInt(ruleId)));
|
||||
} catch (error) {
|
||||
logger.error("Error deleting resource rules created from template rule:", error);
|
||||
// Don't fail the template rule deletion if resource rule deletion fails, just log it
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Template rule deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
69
server/routers/ruleTemplate/getRuleTemplate.ts
Normal file
69
server/routers/ruleTemplate/getRuleTemplate.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { ruleTemplates } from "@server/db";
|
||||
import { eq, and } 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 getRuleTemplateParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string().min(1),
|
||||
templateId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function getRuleTemplate(
|
||||
req: any,
|
||||
res: any,
|
||||
next: any
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = getRuleTemplateParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, templateId } = parsedParams.data;
|
||||
|
||||
// Get the template
|
||||
const template = await db
|
||||
.select()
|
||||
.from(ruleTemplates)
|
||||
.where(and(eq(ruleTemplates.orgId, orgId), eq(ruleTemplates.templateId, templateId)))
|
||||
.limit(1);
|
||||
|
||||
if (template.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Rule template not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: template[0],
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Rule template retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error getting rule template:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
12
server/routers/ruleTemplate/index.ts
Normal file
12
server/routers/ruleTemplate/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from "./createRuleTemplate";
|
||||
export * from "./listRuleTemplates";
|
||||
export * from "./getRuleTemplate";
|
||||
export * from "./updateRuleTemplate";
|
||||
export * from "./listTemplateRules";
|
||||
export * from "./addTemplateRule";
|
||||
export * from "./updateTemplateRule";
|
||||
export * from "./deleteTemplateRule";
|
||||
export * from "./assignTemplateToResource";
|
||||
export * from "./unassignTemplateFromResource";
|
||||
export * from "./listResourceTemplates";
|
||||
export * from "./deleteRuleTemplate";
|
||||
104
server/routers/ruleTemplate/listResourceTemplates.ts
Normal file
104
server/routers/ruleTemplate/listResourceTemplates.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resourceTemplates, ruleTemplates, resources } from "@server/db";
|
||||
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";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listResourceTemplatesParamsSchema = z
|
||||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type ListResourceTemplatesResponse = {
|
||||
templates: Awaited<ReturnType<typeof queryResourceTemplates>>;
|
||||
};
|
||||
|
||||
function queryResourceTemplates(resourceId: number) {
|
||||
return db
|
||||
.select({
|
||||
templateId: ruleTemplates.templateId,
|
||||
name: ruleTemplates.name,
|
||||
description: ruleTemplates.description,
|
||||
orgId: ruleTemplates.orgId,
|
||||
createdAt: ruleTemplates.createdAt
|
||||
})
|
||||
.from(resourceTemplates)
|
||||
.innerJoin(ruleTemplates, eq(resourceTemplates.templateId, ruleTemplates.templateId))
|
||||
.where(eq(resourceTemplates.resourceId, resourceId))
|
||||
.orderBy(ruleTemplates.createdAt);
|
||||
}
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/templates",
|
||||
description: "List templates assigned to a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.RuleTemplate],
|
||||
request: {
|
||||
params: listResourceTemplatesParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listResourceTemplates(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = listResourceTemplatesParamsSchema.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 templatesList = await queryResourceTemplates(resourceId);
|
||||
|
||||
return response<ListResourceTemplatesResponse>(res, {
|
||||
data: {
|
||||
templates: templatesList
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource templates retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
127
server/routers/ruleTemplate/listRuleTemplates.ts
Normal file
127
server/routers/ruleTemplate/listRuleTemplates.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { ruleTemplates } from "@server/db";
|
||||
import { eq, sql } 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";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listRuleTemplatesParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
const listRuleTemplatesQuerySchema = 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())
|
||||
});
|
||||
|
||||
export type ListRuleTemplatesResponse = {
|
||||
templates: Awaited<ReturnType<typeof queryRuleTemplates>>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
function queryRuleTemplates(orgId: string) {
|
||||
return db
|
||||
.select({
|
||||
templateId: ruleTemplates.templateId,
|
||||
orgId: ruleTemplates.orgId,
|
||||
name: ruleTemplates.name,
|
||||
description: ruleTemplates.description,
|
||||
createdAt: ruleTemplates.createdAt
|
||||
})
|
||||
.from(ruleTemplates)
|
||||
.where(eq(ruleTemplates.orgId, orgId))
|
||||
.orderBy(ruleTemplates.createdAt);
|
||||
}
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/rule-templates",
|
||||
description: "List rule templates for an organization.",
|
||||
tags: [OpenAPITags.Org, OpenAPITags.RuleTemplate],
|
||||
request: {
|
||||
params: listRuleTemplatesParamsSchema,
|
||||
query: listRuleTemplatesQuerySchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listRuleTemplates(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = listRuleTemplatesQuerySchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
|
||||
const parsedParams = listRuleTemplatesParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const baseQuery = queryRuleTemplates(orgId);
|
||||
|
||||
let templatesList = await baseQuery.limit(limit).offset(offset);
|
||||
|
||||
// Get total count
|
||||
const countResult = await db
|
||||
.select({ count: sql<number>`cast(count(*) as integer)` })
|
||||
.from(ruleTemplates)
|
||||
.where(eq(ruleTemplates.orgId, orgId));
|
||||
|
||||
const totalCount = Number(countResult[0]?.count || 0);
|
||||
|
||||
return response<ListRuleTemplatesResponse>(res, {
|
||||
data: {
|
||||
templates: templatesList,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Rule templates retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
73
server/routers/ruleTemplate/listTemplateRules.ts
Normal file
73
server/routers/ruleTemplate/listTemplateRules.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { templateRules, ruleTemplates } from "@server/db";
|
||||
import { eq, and } 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 listTemplateRulesParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string().min(1),
|
||||
templateId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function listTemplateRules(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = listTemplateRulesParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, templateId } = parsedParams.data;
|
||||
|
||||
// Check if template exists and belongs to the organization
|
||||
const existingTemplate = await db
|
||||
.select()
|
||||
.from(ruleTemplates)
|
||||
.where(and(eq(ruleTemplates.orgId, orgId), eq(ruleTemplates.templateId, templateId)))
|
||||
.limit(1);
|
||||
|
||||
if (existingTemplate.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Rule template not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Get template rules
|
||||
const rules = await db
|
||||
.select()
|
||||
.from(templateRules)
|
||||
.where(eq(templateRules.templateId, templateId))
|
||||
.orderBy(templateRules.priority);
|
||||
|
||||
return response(res, {
|
||||
data: { rules },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Template rules retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
130
server/routers/ruleTemplate/unassignTemplateFromResource.ts
Normal file
130
server/routers/ruleTemplate/unassignTemplateFromResource.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resourceTemplates, resources, resourceRules, templateRules } from "@server/db";
|
||||
import { eq, and } 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";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const unassignTemplateFromResourceParamsSchema = z
|
||||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
templateId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/resource/{resourceId}/templates/{templateId}",
|
||||
description: "Unassign a template from a resource.",
|
||||
tags: [OpenAPITags.Resource, OpenAPITags.RuleTemplate],
|
||||
request: {
|
||||
params: unassignTemplateFromResourceParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function unassignTemplateFromResource(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = unassignTemplateFromResourceParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId, templateId } = parsedParams.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`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the template is assigned to this resource
|
||||
const [existingAssignment] = await db
|
||||
.select()
|
||||
.from(resourceTemplates)
|
||||
.where(and(eq(resourceTemplates.resourceId, resourceId), eq(resourceTemplates.templateId, templateId)))
|
||||
.limit(1);
|
||||
|
||||
if (!existingAssignment) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Template ${templateId} is not assigned to resource ${resourceId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the template assignment
|
||||
await db
|
||||
.delete(resourceTemplates)
|
||||
.where(and(eq(resourceTemplates.resourceId, resourceId), eq(resourceTemplates.templateId, templateId)));
|
||||
|
||||
// Remove all resource rules that were created from this template
|
||||
// We can now use the templateRuleId to precisely identify which rules to remove
|
||||
try {
|
||||
// Get all template rules for this template
|
||||
const templateRulesList = await db
|
||||
.select()
|
||||
.from(templateRules)
|
||||
.where(eq(templateRules.templateId, templateId))
|
||||
.orderBy(templateRules.priority);
|
||||
|
||||
if (templateRulesList.length > 0) {
|
||||
// Remove resource rules that have templateRuleId matching any of the template rules
|
||||
for (const templateRule of templateRulesList) {
|
||||
await db
|
||||
.delete(resourceRules)
|
||||
.where(and(
|
||||
eq(resourceRules.resourceId, resourceId),
|
||||
eq(resourceRules.templateRuleId, templateRule.ruleId)
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error removing template rules during unassignment:", error);
|
||||
// Don't fail the unassignment if rule removal fails, just log it
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: { resourceId, templateId },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Template unassigned from resource successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
117
server/routers/ruleTemplate/updateRuleTemplate.ts
Normal file
117
server/routers/ruleTemplate/updateRuleTemplate.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { ruleTemplates } from "@server/db";
|
||||
import { eq, and } 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 updateRuleTemplateParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string().min(1),
|
||||
templateId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
const updateRuleTemplateBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(100),
|
||||
description: z.string().max(500).optional()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function updateRuleTemplate(
|
||||
req: any,
|
||||
res: any,
|
||||
next: any
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = updateRuleTemplateParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = updateRuleTemplateBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, templateId } = parsedParams.data;
|
||||
const { name, description } = parsedBody.data;
|
||||
|
||||
// Check if template exists and belongs to the organization
|
||||
const existingTemplate = await db
|
||||
.select()
|
||||
.from(ruleTemplates)
|
||||
.where(and(eq(ruleTemplates.orgId, orgId), eq(ruleTemplates.templateId, templateId)))
|
||||
.limit(1);
|
||||
|
||||
if (existingTemplate.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Rule template not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if another template with the same name already exists (excluding current template)
|
||||
const duplicateTemplate = await db
|
||||
.select()
|
||||
.from(ruleTemplates)
|
||||
.where(and(
|
||||
eq(ruleTemplates.orgId, orgId),
|
||||
eq(ruleTemplates.name, name),
|
||||
eq(ruleTemplates.templateId, templateId)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (duplicateTemplate.length > 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
`Template with name "${name}" already exists`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update the template
|
||||
const [updatedTemplate] = await db
|
||||
.update(ruleTemplates)
|
||||
.set({
|
||||
name,
|
||||
description: description || null
|
||||
})
|
||||
.where(and(eq(ruleTemplates.orgId, orgId), eq(ruleTemplates.templateId, templateId)))
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: updatedTemplate,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Rule template updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating rule template:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
177
server/routers/ruleTemplate/updateTemplateRule.ts
Normal file
177
server/routers/ruleTemplate/updateTemplateRule.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { templateRules, ruleTemplates } from "@server/db";
|
||||
import { eq, and } 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";
|
||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "@server/lib/validators";
|
||||
|
||||
const updateTemplateRuleParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string().min(1),
|
||||
templateId: z.string().min(1),
|
||||
ruleId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
const updateTemplateRuleBodySchema = z
|
||||
.object({
|
||||
action: z.enum(["ACCEPT", "DROP"]).optional(),
|
||||
match: z.enum(["CIDR", "IP", "PATH"]).optional(),
|
||||
value: z.string().min(1).optional(),
|
||||
priority: z.number().int().optional(),
|
||||
enabled: z.boolean().optional()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function updateTemplateRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = updateTemplateRuleParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = updateTemplateRuleBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, templateId, ruleId } = parsedParams.data;
|
||||
const updateData = parsedBody.data;
|
||||
|
||||
// Check if template exists and belongs to the organization
|
||||
const existingTemplate = await db
|
||||
.select()
|
||||
.from(ruleTemplates)
|
||||
.where(and(eq(ruleTemplates.orgId, orgId), eq(ruleTemplates.templateId, templateId)))
|
||||
.limit(1);
|
||||
|
||||
if (existingTemplate.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Rule template not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if rule exists and belongs to the template
|
||||
const existingRule = await db
|
||||
.select()
|
||||
.from(templateRules)
|
||||
.where(and(eq(templateRules.templateId, templateId), eq(templateRules.ruleId, parseInt(ruleId))))
|
||||
.limit(1);
|
||||
|
||||
if (existingRule.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Template rule not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Validate the value if it's being updated
|
||||
if (updateData.value && updateData.match) {
|
||||
if (updateData.match === "CIDR" && !isValidCIDR(updateData.value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid CIDR format"
|
||||
)
|
||||
);
|
||||
}
|
||||
if (updateData.match === "IP" && !isValidIP(updateData.value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid IP address format"
|
||||
)
|
||||
);
|
||||
}
|
||||
if (updateData.match === "PATH" && !isValidUrlGlobPattern(updateData.value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid URL pattern format"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the rule
|
||||
const [updatedRule] = await db
|
||||
.update(templateRules)
|
||||
.set(updateData)
|
||||
.where(and(eq(templateRules.templateId, templateId), eq(templateRules.ruleId, parseInt(ruleId))))
|
||||
.returning();
|
||||
|
||||
// Propagate changes to all resource rules created from this template rule
|
||||
try {
|
||||
const { resourceRules } = await import("@server/db");
|
||||
|
||||
// Find all resource rules that were created from this template rule
|
||||
const affectedResourceRules = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.templateRuleId, parseInt(ruleId)));
|
||||
|
||||
if (affectedResourceRules.length > 0) {
|
||||
// Update all affected resource rules with the same changes
|
||||
// Note: We don't update priority as that should remain independent
|
||||
const propagationData = {
|
||||
...updateData,
|
||||
priority: undefined // Don't propagate priority changes
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(propagationData).forEach(key => {
|
||||
if (propagationData[key] === undefined) {
|
||||
delete propagationData[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(propagationData).length > 0) {
|
||||
await db
|
||||
.update(resourceRules)
|
||||
.set(propagationData)
|
||||
.where(eq(resourceRules.templateRuleId, parseInt(ruleId)));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error propagating template rule changes to resource rules:", error);
|
||||
// Don't fail the template rule update if propagation fails, just log it
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: updatedRule,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Template rule updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import path from "path";
|
||||
import m1 from "./scriptsPg/1.6.0";
|
||||
import m2 from "./scriptsPg/1.7.0";
|
||||
import m3 from "./scriptsPg/1.8.0";
|
||||
import m4 from "./scriptsPg/1.10.0";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
@@ -16,7 +17,8 @@ import m3 from "./scriptsPg/1.8.0";
|
||||
const migrations = [
|
||||
{ version: "1.6.0", run: m1 },
|
||||
{ version: "1.7.0", run: m2 },
|
||||
{ version: "1.8.0", run: m3 }
|
||||
{ version: "1.8.0", run: m3 },
|
||||
{ version: "1.10.0", run: m4 }
|
||||
// Add new migrations here as they are created
|
||||
] as {
|
||||
version: string;
|
||||
|
||||
@@ -25,6 +25,7 @@ import m20 from "./scriptsSqlite/1.5.0";
|
||||
import m21 from "./scriptsSqlite/1.6.0";
|
||||
import m22 from "./scriptsSqlite/1.7.0";
|
||||
import m23 from "./scriptsSqlite/1.8.0";
|
||||
import m24 from "./scriptsSqlite/1.10.0";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
@@ -49,6 +50,7 @@ const migrations = [
|
||||
{ version: "1.6.0", run: m21 },
|
||||
{ version: "1.7.0", run: m22 },
|
||||
{ version: "1.8.0", run: m23 },
|
||||
{ version: "1.10.0", run: m24 },
|
||||
// Add new migrations here as they are created
|
||||
] as const;
|
||||
|
||||
|
||||
63
server/setup/scriptsPg/1.10.0.ts
Normal file
63
server/setup/scriptsPg/1.10.0.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { db } from "@server/db/pg";
|
||||
import { ruleTemplates, templateRules, resourceTemplates } from "@server/db/pg/schema";
|
||||
|
||||
const version = "1.10.0";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
try {
|
||||
// Create rule templates table
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS "ruleTemplates" (
|
||||
"templateId" varchar PRIMARY KEY,
|
||||
"orgId" varchar NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"description" varchar,
|
||||
"createdAt" bigint NOT NULL,
|
||||
FOREIGN KEY ("orgId") REFERENCES "orgs" ("orgId") ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Create template rules table
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS "templateRules" (
|
||||
"ruleId" serial PRIMARY KEY,
|
||||
"templateId" varchar NOT NULL,
|
||||
"enabled" boolean NOT NULL DEFAULT true,
|
||||
"priority" integer NOT NULL,
|
||||
"action" varchar NOT NULL,
|
||||
"match" varchar NOT NULL,
|
||||
"value" varchar NOT NULL,
|
||||
FOREIGN KEY ("templateId") REFERENCES "ruleTemplates" ("templateId") ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Create resource templates table
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS "resourceTemplates" (
|
||||
"resourceId" integer NOT NULL,
|
||||
"templateId" varchar NOT NULL,
|
||||
PRIMARY KEY ("resourceId", "templateId"),
|
||||
FOREIGN KEY ("resourceId") REFERENCES "resources" ("resourceId") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("templateId") REFERENCES "ruleTemplates" ("templateId") ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
console.log("Added rule template tables");
|
||||
|
||||
// Add templateRuleId column to resourceRules table
|
||||
await db.execute(`
|
||||
ALTER TABLE "resourceRules"
|
||||
ADD COLUMN "templateRuleId" INTEGER
|
||||
REFERENCES "templateRules"("ruleId") ON DELETE CASCADE
|
||||
`);
|
||||
|
||||
console.log("Added templateRuleId column to resourceRules table");
|
||||
} catch (e) {
|
||||
console.log("Unable to add rule template tables and columns");
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
||||
70
server/setup/scriptsSqlite/1.10.0.ts
Normal file
70
server/setup/scriptsSqlite/1.10.0.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { APP_PATH } from "@server/lib/consts";
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
import { db } from "@server/db/sqlite";
|
||||
|
||||
const version = "1.10.0";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||
const sqliteDb = new Database(location);
|
||||
|
||||
try {
|
||||
sqliteDb.transaction(() => {
|
||||
// Create rule templates table
|
||||
sqliteDb.exec(`
|
||||
CREATE TABLE IF NOT EXISTS 'ruleTemplates' (
|
||||
'templateId' text PRIMARY KEY,
|
||||
'orgId' text NOT NULL,
|
||||
'name' text NOT NULL,
|
||||
'description' text,
|
||||
'createdAt' integer NOT NULL,
|
||||
FOREIGN KEY ('orgId') REFERENCES 'orgs' ('orgId') ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Create template rules table
|
||||
sqliteDb.exec(`
|
||||
CREATE TABLE IF NOT EXISTS 'templateRules' (
|
||||
'ruleId' integer PRIMARY KEY AUTOINCREMENT,
|
||||
'templateId' text NOT NULL,
|
||||
'enabled' integer NOT NULL DEFAULT 1,
|
||||
'priority' integer NOT NULL,
|
||||
'action' text NOT NULL,
|
||||
'match' text NOT NULL,
|
||||
'value' text NOT NULL,
|
||||
FOREIGN KEY ('templateId') REFERENCES 'ruleTemplates' ('templateId') ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Create resource templates table
|
||||
sqliteDb.exec(`
|
||||
CREATE TABLE IF NOT EXISTS 'resourceTemplates' (
|
||||
'resourceId' integer NOT NULL,
|
||||
'templateId' text NOT NULL,
|
||||
PRIMARY KEY ('resourceId', 'templateId'),
|
||||
FOREIGN KEY ('resourceId') REFERENCES 'resources' ('resourceId') ON DELETE CASCADE,
|
||||
FOREIGN KEY ('templateId') REFERENCES 'ruleTemplates' ('templateId') ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
})();
|
||||
|
||||
console.log("Added rule template tables");
|
||||
|
||||
// Add templateRuleId column to resourceRules table
|
||||
await db.run(`
|
||||
ALTER TABLE resourceRules
|
||||
ADD COLUMN templateRuleId INTEGER
|
||||
REFERENCES templateRules(ruleId) ON DELETE CASCADE
|
||||
`);
|
||||
|
||||
console.log("Added templateRuleId column to resourceRules table");
|
||||
} catch (e) {
|
||||
console.log("Unable to add rule template tables and columns");
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
||||
Reference in New Issue
Block a user