add roles input on resource and make spacing more consistent

This commit is contained in:
Milo Schwartz 2024-11-15 18:25:27 -05:00
parent 8e64b5e0e9
commit 28bae40390
No known key found for this signature in database
36 changed files with 1235 additions and 724 deletions

View file

@ -35,8 +35,7 @@ export enum ActionsEnum {
listUsers = "listUsers",
listSiteRoles = "listSiteRoles",
listResourceRoles = "listResourceRoles",
addRoleSite = "addRoleSite",
addRoleResource = "addRoleResource",
setResourceRoles = "setResourceRoles",
removeRoleResource = "removeRoleResource",
removeRoleSite = "removeRoleSite",
// addRoleAction = "addRoleAction",

View file

@ -25,7 +25,6 @@ export const sites = sqliteTable("sites", {
export const resources = sqliteTable("resources", {
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
fullDomain: text("fullDomain", { length: 2048 }),
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade",
}),
@ -33,7 +32,7 @@ export const resources = sqliteTable("resources", {
onDelete: "cascade",
}),
name: text("name").notNull(),
subdomain: text("subdomain"),
subdomain: text("subdomain").notNull(),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
});

View file

@ -1,10 +1,16 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { roles, userOrgs } from "@server/db/schema";
import { and, eq } from "drizzle-orm";
import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const verifyRoleAccessSchema = z.object({
roleIds: z.array(z.number().int().positive()).optional(),
});
export async function verifyRoleAccess(
req: Request,
@ -12,7 +18,7 @@ export async function verifyRoleAccess(
next: NextFunction
) {
const userId = req.user?.userId;
const roleId = parseInt(
const singleRoleId = parseInt(
req.params.roleId || req.body.roleId || req.query.roleId
);
@ -22,61 +28,61 @@ export async function verifyRoleAccess(
);
}
if (isNaN(roleId)) {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid role ID"));
const parsedBody = verifyRoleAccessSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { roleIds } = parsedBody.data;
const allRoleIds = roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
if (allRoleIds.length === 0) {
return next();
}
try {
const role = await db
const rolesData = await db
.select()
.from(roles)
.where(eq(roles.roleId, roleId))
.limit(1);
.where(inArray(roles.roleId, allRoleIds));
if (role.length === 0) {
if (rolesData.length !== allRoleIds.length) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Role with ID ${roleId} not found`
"One or more roles not found"
)
);
}
if (!req.userOrg) {
// Check user access to each role's organization
for (const role of rolesData) {
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, role[0].orgId!)
eq(userOrgs.orgId, role.orgId!)
)
)
.limit(1);
req.userOrg = userOrgRole[0];
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
if (userOrgRole.length === 0) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
`User does not have access to organization for role ID ${role.roleId}`
)
);
}
}
if (req.userOrg.orgId !== role[0].orgId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Role does not belong to the organization"
)
);
}
req.userOrgRoleId = req.userOrg.roleId;
req.userOrgId = req.userOrg.orgId;
return next();
} catch (error) {
logger.error("Error verifying role access:", error);

View file

@ -20,6 +20,7 @@ import {
verifyTargetAccess,
verifyRoleAccess,
verifyUserAccess,
verifyUserInRole,
} from "./auth";
import { verifyUserHasAction } from "./auth/verifyUserHasAction";
import { ActionsEnum } from "@server/auth/actions";
@ -135,12 +136,13 @@ authenticated.post(
); // maybe make this /invite/create instead
authenticated.post("/invite/accept", user.acceptInvite);
// authenticated.get(
// "/resource/:resourceId/roles",
// verifyResourceAccess,
// verifyUserHasAction(ActionsEnum.listResourceRoles),
// resource.listResourceRoles
// );
authenticated.get(
"/resource/:resourceId/roles",
verifyResourceAccess,
verifyUserHasAction(ActionsEnum.listResourceRoles),
resource.listResourceRoles
);
authenticated.get(
"/resource/:resourceId",
verifyResourceAccess,
@ -251,20 +253,15 @@ authenticated.post(
// verifyUserHasAction(ActionsEnum.listRoleSites),
// role.listRoleSites
// );
// authenticated.put(
// "/role/:roleId/resource",
// verifyRoleAccess,
// verifyUserInRole,
// verifyUserHasAction(ActionsEnum.addRoleResource),
// role.addRoleResource
// );
// authenticated.delete(
// "/role/:roleId/resource",
// verifyRoleAccess,
// verifyUserInRole,
// verifyUserHasAction(ActionsEnum.removeRoleResource),
// role.removeRoleResource
// );
authenticated.post(
"/resource/:resourceId/roles",
verifyResourceAccess,
verifyRoleAccess,
verifyUserHasAction(ActionsEnum.setResourceRoles),
role.addRoleResource
);
// authenticated.get(
// "/role/:roleId/resources",
// verifyRoleAccess,

View file

@ -15,6 +15,7 @@ import createHttpError from "http-errors";
import { eq, and } from "drizzle-orm";
import stoi from "@server/utils/stoi";
import { fromError } from "zod-validation-error";
import { subdomainSchema } from "@server/schemas/subdomainSchema";
const createResourceParamsSchema = z.object({
siteId: z
@ -28,7 +29,7 @@ const createResourceParamsSchema = z.object({
const createResourceSchema = z
.object({
name: z.string().min(1).max(255),
subdomain: z.string().min(1).max(255).optional(),
subdomain: subdomainSchema,
})
.strict();
@ -87,12 +88,9 @@ export async function createResource(
);
}
const fullDomain = `${subdomain}.${org[0].domain}`;
const newResource = await db
.insert(resources)
.values({
fullDomain,
siteId,
orgId,
name,

View file

@ -13,6 +13,23 @@ const listResourceRolesSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
});
async function query(resourceId: number) {
return await db
.select({
roleId: roles.roleId,
name: roles.name,
description: roles.description,
isAdmin: roles.isAdmin,
})
.from(roleResources)
.innerJoin(roles, eq(roleResources.roleId, roles.roleId))
.where(eq(roleResources.resourceId, resourceId));
}
export type ListResourceRolesResponse = {
roles: NonNullable<Awaited<ReturnType<typeof query>>>;
};
export async function listResourceRoles(
req: Request,
res: Response,
@ -31,19 +48,12 @@ export async function listResourceRoles(
const { resourceId } = parsedParams.data;
const resourceRolesList = await db
.select({
roleId: roles.roleId,
name: roles.name,
description: roles.description,
isAdmin: roles.isAdmin,
})
.from(roleResources)
.innerJoin(roles, eq(roleResources.roleId, roles.roleId))
.where(eq(roleResources.resourceId, resourceId));
const resourceRolesList = await query(resourceId);
return response(res, {
data: resourceRolesList,
return response<ListResourceRolesResponse>(res, {
data: {
roles: resourceRolesList,
},
success: true,
error: false,
message: "Resource roles retrieved successfully",

View file

@ -0,0 +1,111 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { roleResources, roles } from "@server/db/schema";
import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { eq, and, ne } from "drizzle-orm";
const setResourceRolesBodySchema = z.object({
roleIds: z.array(z.number().int().positive()),
});
const setResourceRolesParamsSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
});
export async function addRoleResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = setResourceRolesBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { roleIds } = parsedBody.data;
const parsedParams = setResourceRolesParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
// get this org's admin role
const adminRole = await db
.select()
.from(roles)
.where(
and(
eq(roles.name, "Admin"),
eq(roles.orgId, req.userOrg!.orgId)
)
)
.limit(1);
if (!adminRole.length) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Admin role not found"
)
);
}
if (roleIds.includes(adminRole[0].roleId)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Admin role cannot be assigned to resources"
)
);
}
await db.transaction(async (trx) => {
await trx.delete(roleResources).where(
and(
eq(roleResources.resourceId, resourceId),
ne(roleResources.roleId, adminRole[0].roleId) // delete all but the admin role
)
);
const newRoleResources = await Promise.all(
roleIds.map((roleId) =>
trx
.insert(roleResources)
.values({ roleId, resourceId })
.returning()
)
);
return response(res, {
data: {},
success: true,
error: false,
message: "Roles set for resource successfully",
status: HttpCode.CREATED,
});
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -8,6 +8,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { subdomainSchema } from "@server/schemas/subdomainSchema";
const updateResourceParamsSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
@ -16,7 +17,7 @@ const updateResourceParamsSchema = z.object({
const updateResourceBodySchema = z
.object({
name: z.string().min(1).max(255).optional(),
subdomain: z.string().min(1).max(255).optional(),
subdomain: subdomainSchema.optional(),
ssl: z.boolean().optional(),
// siteId: z.number(),
})

View file

@ -1,70 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { roleResources } from "@server/db/schema";
import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const addRoleResourceParamsSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()),
});
const addRoleResourceSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
});
export async function addRoleResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = addRoleResourceSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { resourceId } = parsedBody.data;
const parsedParams = addRoleResourceParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { roleId } = parsedParams.data;
const newRoleResource = await db
.insert(roleResources)
.values({
roleId,
resourceId,
})
.returning();
return response(res, {
data: newRoleResource[0],
success: true,
error: false,
message: "Resource added to role successfully",
status: HttpCode.CREATED,
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -1,5 +1,5 @@
export * from "./addRoleAction";
export * from "./addRoleResource";
export * from "../resource/setResourceRoles";
export * from "./addRoleSite";
export * from "./createRole";
export * from "./deleteRole";

View file

@ -18,10 +18,15 @@ export async function traefikConfigProvider(
schema.resources,
eq(schema.targets.resourceId, schema.resources.resourceId)
)
.innerJoin(
schema.orgs,
eq(schema.resources.orgId, schema.orgs.orgId)
)
.where(
and(
eq(schema.targets.enabled, true),
isNotNull(schema.resources.fullDomain)
isNotNull(schema.resources.subdomain),
isNotNull(schema.orgs.domain)
)
);
@ -60,15 +65,22 @@ export async function traefikConfigProvider(
for (const item of all) {
const target = item.targets;
const resource = item.resources;
const org = item.orgs;
const routerName = `${target.targetId}-router`;
const serviceName = `${target.targetId}-service`;
if (!resource.fullDomain) {
if (!resource || !resource.subdomain) {
continue;
}
const domainParts = resource.fullDomain.split(".");
if (!org || !org.domain) {
continue;
}
const fullDomain = `${resource.subdomain}.${org.domain}`;
const domainParts = fullDomain.split(".");
let wildCard;
if (domainParts.length <= 2) {
wildCard = `*.${domainParts.join(".")}`;
@ -97,7 +109,7 @@ export async function traefikConfigProvider(
],
middlewares: resource.ssl ? [badgerMiddlewareName] : [],
service: serviceName,
rule: `Host(\`${resource.fullDomain}\`)`,
rule: `Host(\`${fullDomain}\`)`,
...(resource.ssl ? { tls } : {}),
};
@ -107,7 +119,7 @@ export async function traefikConfigProvider(
entryPoints: [config.traefik.http_entrypoint],
middlewares: [redirectMiddlewareName],
service: serviceName,
rule: `Host(\`${resource.fullDomain}\`)`,
rule: `Host(\`${fullDomain}\`)`,
};
}

View file

@ -0,0 +1,9 @@
import { z } from "zod";
export const subdomainSchema = z
.string()
.regex(
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/,
"Invalid subdomain format"
)
.min(1, "Subdomain must be at least 1 character long");