Handle the roles better in the verify session

This commit is contained in:
Owen
2026-03-28 17:12:21 -07:00
parent d1b2105c80
commit 00ef6d617f
4 changed files with 266 additions and 38 deletions

View File

@@ -1,4 +1,12 @@
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs, roles } from "@server/db"; import {
db,
loginPage,
LoginPage,
loginPageOrg,
Org,
orgs,
roles
} from "@server/db";
import { import {
Resource, Resource,
ResourcePassword, ResourcePassword,
@@ -12,14 +20,12 @@ import {
resources, resources,
roleResources, roleResources,
sessions, sessions,
userOrgRoles,
userOrgs,
userResources, userResources,
users, users,
ResourceHeaderAuthExtendedCompatibility, ResourceHeaderAuthExtendedCompatibility,
resourceHeaderAuthExtendedCompatibility resourceHeaderAuthExtendedCompatibility
} from "@server/db"; } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
export type ResourceWithAuth = { export type ResourceWithAuth = {
resource: Resource | null; resource: Resource | null;
@@ -121,7 +127,7 @@ export async function getRoleName(roleId: number): Promise<string | null> {
*/ */
export async function getRoleResourceAccess( export async function getRoleResourceAccess(
resourceId: number, resourceId: number,
roleId: number roleIds: number[]
) { ) {
const roleResourceAccess = await db const roleResourceAccess = await db
.select() .select()
@@ -129,12 +135,11 @@ export async function getRoleResourceAccess(
.where( .where(
and( and(
eq(roleResources.resourceId, resourceId), eq(roleResources.resourceId, resourceId),
eq(roleResources.roleId, roleId) inArray(roleResources.roleId, roleIds)
) )
) );
.limit(1);
return roleResourceAccess.length > 0 ? roleResourceAccess[0] : null; return roleResourceAccess.length > 0 ? roleResourceAccess : null;
} }
/** /**

View File

@@ -1,4 +1,4 @@
import { db, userOrgRoles } from "@server/db"; import { db, roles, userOrgRoles } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
/** /**
@@ -20,3 +20,17 @@ export async function getUserOrgRoleIds(
); );
return rows.map((r) => r.roleId); return rows.map((r) => r.roleId);
} }
export async function getUserOrgRoles(
userId: string,
orgId: string
): Promise<{ roleId: number; roleName: string }[]> {
const rows = await db
.select({ roleId: userOrgRoles.roleId, roleName: roles.name })
.from(userOrgRoles)
.innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where(
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
);
return rows;
}

View File

@@ -124,6 +124,23 @@ const getRoleResourceAccessParamsSchema = z.strictObject({
.pipe(z.int().positive("Resource ID must be a positive integer")) .pipe(z.int().positive("Resource ID must be a positive integer"))
}); });
const getResourceAccessParamsSchema = z.strictObject({
resourceId: z
.string()
.transform(Number)
.pipe(z.int().positive("Resource ID must be a positive integer"))
});
const getResourceAccessQuerySchema = z.strictObject({
roleIds: z
.union([z.array(z.string()), z.string()])
.transform((val) =>
(Array.isArray(val) ? val : [val])
.map(Number)
.filter((n) => !isNaN(n))
)
});
const getUserResourceAccessParamsSchema = z.strictObject({ const getUserResourceAccessParamsSchema = z.strictObject({
userId: z.string().min(1, "User ID is required"), userId: z.string().min(1, "User ID is required"),
resourceId: z resourceId: z
@@ -769,7 +786,7 @@ hybridRouter.get(
// Get user organization role // Get user organization role
hybridRouter.get( hybridRouter.get(
"/user/:userId/org/:orgId/role", "/user/:userId/org/:orgId/roles",
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const parsedParams = getUserOrgRoleParamsSchema.safeParse( const parsedParams = getUserOrgRoleParamsSchema.safeParse(
@@ -805,6 +822,80 @@ hybridRouter.get(
); );
} }
const userOrgRoleRows = await db
.select({ roleId: userOrgRoles.roleId, roleName: roles.name })
.from(userOrgRoles)
.innerJoin(roles, eq(roles.roleId, userOrgRoles.roleId))
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
);
return response<{ roleId: number, roleName: string }[]>(res, {
data: userOrgRoleRows,
success: true,
error: false,
message:
userOrgRoleRows.length > 0
? "User org roles retrieved successfully"
: "User has no roles in this organization",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to get user org role"
)
);
}
}
);
// DEPRICATED Get user organization role
// used for backward compatibility with old remote nodes
hybridRouter.get(
"/user/:userId/org/:orgId/role", // <- note the missing s
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = getUserOrgRoleParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { userId, orgId } = parsedParams.data;
const remoteExitNode = req.remoteExitNode;
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Remote exit node not found"
)
);
}
if (await checkExitNodeOrg(remoteExitNode.exitNodeId, orgId)) {
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"User is not authorized to access this organization"
)
);
}
// get the roles on the user
const userOrgRoleRows = await db const userOrgRoleRows = await db
.select({ roleId: userOrgRoles.roleId }) .select({ roleId: userOrgRoles.roleId })
.from(userOrgRoles) .from(userOrgRoles)
@@ -817,8 +908,35 @@ hybridRouter.get(
const roleIds = userOrgRoleRows.map((r) => r.roleId); const roleIds = userOrgRoleRows.map((r) => r.roleId);
return response<number[]>(res, { let roleId: number | null = null;
data: roleIds,
if (userOrgRoleRows.length === 0) {
// User has no roles in this organization
roleId = null;
} else if (userOrgRoleRows.length === 1) {
// User has exactly one role, return it
roleId = userOrgRoleRows[0].roleId;
} else {
// User has multiple roles
// Check if any of these roles are also assigned to a resource
// If we find a match, prefer that role; otherwise return the first role
// Get all resources that have any of these roles assigned
const roleResourceMatches = await db
.select({ roleId: roleResources.roleId })
.from(roleResources)
.where(inArray(roleResources.roleId, roleIds))
.limit(1);
if (roleResourceMatches.length > 0) {
// Return the first role that's also on a resource
roleId = roleResourceMatches[0].roleId;
} else {
// No resource match found, return the first role
roleId = userOrgRoleRows[0].roleId;
}
}
return response<{ roleId: number | null }>(res, {
data: { roleId },
success: true, success: true,
error: false, error: false,
message: message:
@@ -939,7 +1057,9 @@ hybridRouter.get(
data: role?.name ?? null, data: role?.name ?? null,
success: true, success: true,
error: false, error: false,
message: role ? "Role name retrieved successfully" : "Role not found", message: role
? "Role name retrieved successfully"
: "Role not found",
status: HttpCode.OK status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
@@ -1039,6 +1159,101 @@ hybridRouter.get(
} }
); );
// Check if role has access to resource
hybridRouter.get(
"/resource/:resourceId/access",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = getResourceAccessParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
const parsedQuery = getResourceAccessQuerySchema.safeParse(
req.query
);
const roleIds = parsedQuery.success ? parsedQuery.data.roleIds : [];
const remoteExitNode = req.remoteExitNode;
if (!remoteExitNode?.exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Remote exit node not found"
)
);
}
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (
await checkExitNodeOrg(
remoteExitNode.exitNodeId,
resource.orgId
)
) {
// If the exit node is not allowed for the org, return an error
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Exit node not allowed for this organization"
)
);
}
const roleResourceAccess = await db
.select({
resourceId: roleResources.resourceId,
roleId: roleResources.roleId
})
.from(roleResources)
.where(
and(
eq(roleResources.resourceId, resourceId),
inArray(roleResources.roleId, roleIds)
)
);
const result =
roleResourceAccess.length > 0 ? roleResourceAccess : null;
return response<{ resourceId: number; roleId: number }[] | null>(
res,
{
data: result,
success: true,
error: false,
message: result
? "Role resource access retrieved successfully"
: "Role resource access not found",
status: HttpCode.OK
}
);
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to get role resource access"
)
);
}
}
);
// Check if user has direct access to resource // Check if user has direct access to resource
hybridRouter.get( hybridRouter.get(
"/user/:userId/resource/:resourceId/access", "/user/:userId/resource/:resourceId/access",
@@ -1937,7 +2152,8 @@ hybridRouter.post(
// userAgent: data.userAgent, // TODO: add this // userAgent: data.userAgent, // TODO: add this
// headers: data.body.headers, // headers: data.body.headers,
// query: data.body.query, // query: data.body.query,
originalRequestURL: sanitizeString(logEntry.originalRequestURL) ?? "", originalRequestURL:
sanitizeString(logEntry.originalRequestURL) ?? "",
scheme: sanitizeString(logEntry.scheme) ?? "", scheme: sanitizeString(logEntry.scheme) ?? "",
host: sanitizeString(logEntry.host) ?? "", host: sanitizeString(logEntry.host) ?? "",
path: sanitizeString(logEntry.path) ?? "", path: sanitizeString(logEntry.path) ?? "",

View File

@@ -9,7 +9,7 @@ import {
getOrgLoginPage, getOrgLoginPage,
getUserSessionWithUser getUserSessionWithUser
} from "@server/db/queries/verifySessionQueries"; } from "@server/db/queries/verifySessionQueries";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; import { getUserOrgRoles } from "@server/lib/userOrgRoles";
import { import {
LoginPage, LoginPage,
Org, Org,
@@ -798,7 +798,8 @@ async function notAllowed(
) { ) {
let loginPage: LoginPage | null = null; let loginPage: LoginPage | null = null;
if (orgId) { if (orgId) {
const subscribed = await isSubscribed( // this is fine because the org login page is only a saas feature const subscribed = await isSubscribed(
// this is fine because the org login page is only a saas feature
orgId, orgId,
tierMatrix.loginPageDomain tierMatrix.loginPageDomain
); );
@@ -855,7 +856,10 @@ async function headerAuthChallenged(
) { ) {
let loginPage: LoginPage | null = null; let loginPage: LoginPage | null = null;
if (orgId) { if (orgId) {
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain); // this is fine because the org login page is only a saas feature const subscribed = await isSubscribed(
orgId,
tierMatrix.loginPageDomain
); // this is fine because the org login page is only a saas feature
if (subscribed) { if (subscribed) {
loginPage = await getOrgLoginPage(orgId); loginPage = await getOrgLoginPage(orgId);
} }
@@ -917,9 +921,9 @@ async function isUserAllowedToAccessResource(
return null; return null;
} }
const userOrgRoleIds = await getUserOrgRoleIds(user.userId, resource.orgId); const userOrgRoles = await getUserOrgRoles(user.userId, resource.orgId);
if (!userOrgRoleIds.length) { if (!userOrgRoles.length) {
return null; return null;
} }
@@ -935,23 +939,16 @@ async function isUserAllowedToAccessResource(
return null; return null;
} }
const roleNames: string[] = []; const roleResourceAccess = await getRoleResourceAccess(
for (const roleId of userOrgRoleIds) { resource.resourceId,
const roleResourceAccess = await getRoleResourceAccess( userOrgRoles.map((r) => r.roleId)
resource.resourceId, );
roleId if (roleResourceAccess && roleResourceAccess.length > 0) {
);
if (roleResourceAccess) {
const roleName = await getRoleName(roleId);
if (roleName) roleNames.push(roleName);
}
}
if (roleNames.length > 0) {
return { return {
username: user.username, username: user.username,
email: user.email, email: user.email,
name: user.name, name: user.name,
role: roleNames.join(", ") role: userOrgRoles.map((r) => r.roleName).join(", ")
}; };
} }
@@ -961,15 +958,11 @@ async function isUserAllowedToAccessResource(
); );
if (userResourceAccess) { if (userResourceAccess) {
const names = await Promise.all(
userOrgRoleIds.map((id) => getRoleName(id))
);
const role = names.filter(Boolean).join(", ") || "";
return { return {
username: user.username, username: user.username,
email: user.email, email: user.email,
name: user.name, name: user.name,
role role: userOrgRoles.map((r) => r.roleName).join(", ")
}; };
} }