Update to verify middleware & lists agenst new permissions tables

This commit is contained in:
Owen Schwartz
2024-10-06 16:19:04 -04:00
parent 0838679120
commit 20db6d450c
12 changed files with 275 additions and 128 deletions

View File

@@ -21,7 +21,7 @@ export async function getUserOrgs(req: Request, res: Response, next: NextFunctio
.where(eq(userOrgs.userId, userId));
req.userOrgs = userOrganizations.map(org => org.orgId);
// req.userOrgRoles = userOrganizations.reduce((acc, org) => {
// req.userOrgRoleIds = userOrganizations.reduce((acc, org) => {
// acc[org.orgId] = org.role;
// return acc;
// }, {} as Record<number, string>);

View File

@@ -26,7 +26,8 @@ export function verifyOrgAccess(req: Request, res: Response, next: NextFunction)
next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
} else {
// User has access, attach the user's role to the request for potential future use
req.userOrgRole = result[0].role;
req.userOrgRoleId = result[0].roleId;
req.userOrgId = orgId;
next();
}
})

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { db } from '@server/db';
import { resources, userOrgs } from '@server/db/schema';
import { resources, userOrgs, userResources, roleResources } from '@server/db/schema';
import { and, eq } from 'drizzle-orm';
import createHttpError from 'http-errors';
import HttpCode from '@server/types/HttpCode';
@@ -13,42 +13,66 @@ export async function verifyResourceAccess(req: Request, res: Response, next: Ne
return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'));
}
const resource = await db.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
try {
// Get the resource
const resource = await db.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (resource.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`resource with ID ${resourceId} not found`
if (resource.length === 0) {
return next(createHttpError(HttpCode.NOT_FOUND, `Resource with ID ${resourceId} not found`));
}
if (!resource[0].orgId) {
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `Resource with ID ${resourceId} does not have an organization ID`));
}
// Get user's role ID in the organization
const userOrgRole = await db.select()
.from(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, resource[0].orgId)))
.limit(1);
if (userOrgRole.length === 0) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
}
const userOrgRoleId = userOrgRole[0].roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = resource[0].orgId;
// Check role-based resource access first
const roleResourceAccess = await db.select()
.from(roleResources)
.where(
and(
eq(roleResources.resourceId, resourceId),
eq(roleResources.roleId, userOrgRoleId)
)
)
);
}
.limit(1);
if (!resource[0].orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`resource with ID ${resourceId} does not have an organization ID`
)
);
}
if (roleResourceAccess.length > 0) {
// User's role has access to the resource
return next();
}
db.select()
.from(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, resource[0].orgId)))
.then((result) => {
if (result.length === 0) {
next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
} else {
// User has access, attach the user's role to the request for potential future use
req.userOrgRole = result[0].role;
next();
}
})
.catch((error) => {
next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying organization access'));
});
// If role doesn't have access, check user-specific resource access
const userResourceAccess = await db.select()
.from(userResources)
.where(and(eq(userResources.userId, userId), eq(userResources.resourceId, resourceId)))
.limit(1);
if (userResourceAccess.length > 0) {
// User has direct access to the resource
return next();
}
// If we reach here, the user doesn't have access to the resource
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this resource'));
} catch (error) {
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying resource access'));
}
}

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import { db } from '@server/db';
import { sites, userOrgs } from '@server/db/schema';
import { and, eq } from 'drizzle-orm';
import { sites, userOrgs, userSites, roleSites, roles } from '@server/db/schema';
import { and, eq, or } from 'drizzle-orm';
import createHttpError from 'http-errors';
import HttpCode from '@server/types/HttpCode';
@@ -14,45 +14,66 @@ export async function verifySiteAccess(req: Request, res: Response, next: NextFu
}
if (isNaN(siteId)) {
return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid organization ID'));
return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid site ID'));
}
const site = await db.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
try {
// Get the site
const site = await db.select().from(sites).where(eq(sites.siteId, siteId)).limit(1);
if (site.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${siteId} not found`
if (site.length === 0) {
return next(createHttpError(HttpCode.NOT_FOUND, `Site with ID ${siteId} not found`));
}
if (!site[0].orgId) {
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `Site with ID ${siteId} does not have an organization ID`));
}
// Get user's role ID in the organization
const userOrgRole = await db.select()
.from(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, site[0].orgId)))
.limit(1);
if (userOrgRole.length === 0) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
}
const userOrgRoleId = userOrgRole[0].roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = site[0].orgId;
// Check role-based site access first
const roleSiteAccess = await db.select()
.from(roleSites)
.where(
and(
eq(roleSites.siteId, siteId),
eq(roleSites.roleId, userOrgRoleId)
)
)
);
}
.limit(1);
if (!site[0].orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Site with ID ${siteId} does not have an organization ID`
)
);
}
if (roleSiteAccess.length > 0) {
// User's role has access to the site
return next();
}
db.select()
.from(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, site[0].orgId)))
.then((result) => {
if (result.length === 0) {
next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
} else {
// User has access, attach the user's role to the request for potential future use
req.userOrgRole = result[0].role;
next();
}
})
.catch((error) => {
next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying organization access'));
});
// If role doesn't have access, check user-specific site access
const userSiteAccess = await db.select()
.from(userSites)
.where(and(eq(userSites.userId, userId), eq(userSites.siteId, siteId)))
.limit(1);
if (userSiteAccess.length > 0) {
// User has direct access to the site
return next();
}
// If we reach here, the user doesn't have access to the site
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this site'));
} catch (error) {
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying site access'));
}
}

View File

@@ -73,7 +73,8 @@ export async function verifyTargetAccess(req: Request, res: Response, next: Next
next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
} else {
// User has access, attach the user's role to the request for potential future use
req.userOrgRole = result[0].role;
req.userOrgRoleId = result[0].roleId;
req.userOrgId = resource[0].orgId!;
next();
}
})

View File

@@ -62,7 +62,7 @@ export async function listOrgs(req: Request, res: Response, next: NextFunction):
// // Add the user's role for each organization
// const organizationsWithRoles = organizations.map(org => ({
// ...org,
// userRole: req.userOrgRoles[org.orgId],
// userRole: req.userOrgRoleIds[org.orgId],
// }));
return res.status(HttpCode.OK).send(

View File

@@ -1,11 +1,11 @@
import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import { db } from '@server/db';
import { resources, sites } from '@server/db/schema';
import { resources, sites, userResources, roleResources } from '@server/db/schema';
import response from "@server/utils/response";
import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors';
import { sql, eq } from 'drizzle-orm';
import { sql, eq, and, or, inArray } from 'drizzle-orm';
const listResourcesParamsSchema = z.object({
siteId: z.coerce.number().int().positive().optional(),
@@ -19,30 +19,50 @@ const listResourcesSchema = z.object({
offset: z.coerce.number().int().nonnegative().default(0),
});
export async function listResources(req: Request, res: Response, next: NextFunction): Promise<any> {
interface RequestWithOrgAndRole extends Request {
userOrgRoleId?: number;
orgId?: number;
}
export async function listResources(req: RequestWithOrgAndRole, res: Response, next: NextFunction): Promise<any> {
try {
// Check if the user has permission to list resources
// const LIST_RESOURCES_ACTION_ID = 3; // Assume 3 is the action ID for listing resources
// const hasPermission = await checkUserActionPermission(LIST_RESOURCES_ACTION_ID, req);
// if (!hasPermission) {
// return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list resources'));
// }
const parsedQuery = listResourcesSchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedQuery.error.errors.map(e => e.message).join(', ')
)
);
return next(createHttpError(HttpCode.BAD_REQUEST, parsedQuery.error.errors.map(e => e.message).join(', ')));
}
const { limit, offset } = parsedQuery.data;
const parsedParams = listResourcesParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ')
)
);
return next(createHttpError(HttpCode.BAD_REQUEST, parsedParams.error.errors.map(e => e.message).join(', ')));
}
const { siteId, orgId } = parsedParams.data;
if (orgId && orgId !== req.orgId) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
}
// Get the list of resources the user has access to
const accessibleResources = await db
.select({ resourceId: sql<string>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})` })
.from(userResources)
.fullJoin(roleResources, eq(userResources.resourceId, roleResources.resourceId))
.where(
or(
eq(userResources.userId, req.user!.id),
eq(roleResources.roleId, req.userOrgRoleId!)
)
);
const accessibleResourceIds = accessibleResources.map(resource => resource.resourceId);
let baseQuery: any = db
.select({
resourceId: resources.resourceId,
@@ -51,16 +71,21 @@ export async function listResources(req: Request, res: Response, next: NextFunct
siteName: sites.name,
})
.from(resources)
.leftJoin(sites, eq(resources.siteId, sites.siteId));
.leftJoin(sites, eq(resources.siteId, sites.siteId))
.where(inArray(resources.resourceId, accessibleResourceIds));
let countQuery: any = db.select({ count: sql<number>`cast(count(*) as integer)` }).from(resources);
let countQuery: any = db
.select({ count: sql<number>`cast(count(*) as integer)` })
.from(resources)
.where(inArray(resources.resourceId, accessibleResourceIds));
if (siteId) {
baseQuery = baseQuery.where(eq(resources.siteId, siteId));
countQuery = countQuery.where(eq(resources.siteId, siteId));
} else if (orgId) {
baseQuery = baseQuery.where(eq(resources.orgId, orgId));
countQuery = countQuery.where(eq(resources.orgId, orgId));
} else {
// If orgId is provided, it's already checked to match req.orgId
baseQuery = baseQuery.where(eq(resources.orgId, req.orgId!));
countQuery = countQuery.where(eq(resources.orgId, req.orgId!));
}
const resourcesList = await baseQuery.limit(limit).offset(offset);

View File

@@ -1,11 +1,12 @@
import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import { db } from '@server/db';
import { sites, orgs, exitNodes } from '@server/db/schema';
import { sites, orgs, exitNodes, userSites, roleSites } from '@server/db/schema';
import response from "@server/utils/response";
import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors';
import { sql, eq } from 'drizzle-orm';
import { sql, eq, and, or, inArray } from 'drizzle-orm';
// import { checkUserActionPermission } from './checkUserActionPermission'; // Import the function we created earlier
const listSitesParamsSchema = z.object({
orgId: z.string().optional().transform(Number).pipe(z.number().int().positive()),
@@ -18,29 +19,41 @@ const listSitesSchema = z.object({
export async function listSites(req: Request, res: Response, next: NextFunction): Promise<any> {
try {
// Check if the user has permission to list sites
// const LIST_SITES_ACTION_ID = 1; // Assume 1 is the action ID for listing sites
// const hasPermission = await checkUserActionPermission(LIST_SITES_ACTION_ID, req);
// if (!hasPermission) {
// return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites'));
// }
const parsedQuery = listSitesSchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedQuery.error.errors.map(e => e.message).join(', ')
)
);
return next(createHttpError(HttpCode.BAD_REQUEST, parsedQuery.error.errors.map(e => e.message).join(', ')));
}
const { limit, offset } = parsedQuery.data;
const parsedParams = listSitesParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ')
)
);
return next(createHttpError(HttpCode.BAD_REQUEST, parsedParams.error.errors.map(e => e.message).join(', ')));
}
const { orgId } = parsedParams.data;
if (orgId && orgId !== req.userOrgId) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
}
const { orgId } = parsedParams.data;
const accessibleSites = await db
.select({ siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})` })
.from(userSites)
.fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId))
.where(
or(
eq(userSites.userId, req.user!.id),
eq(roleSites.roleId, req.userOrgRoleId!)
)
);
const accessibleSiteIds = accessibleSites.map(site => site.siteId);
let baseQuery: any = db
.select({
@@ -56,9 +69,12 @@ export async function listSites(req: Request, res: Response, next: NextFunction)
})
.from(sites)
.leftJoin(orgs, eq(sites.orgId, orgs.orgId))
.leftJoin(exitNodes, eq(sites.exitNode, exitNodes.exitNodeId));
.where(inArray(sites.siteId, accessibleSiteIds));
let countQuery: any = db.select({ count: sql<number>`cast(count(*) as integer)` }).from(sites);
let countQuery: any = db
.select({ count: sql<number>`cast(count(*) as integer)` })
.from(sites)
.where(inArray(sites.siteId, accessibleSiteIds));
if (orgId) {
baseQuery = baseQuery.where(eq(sites.orgId, orgId));