From 471ae982042d7711fcb57f56dea6d7a674e08438 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 12 Jun 2026 14:06:57 -0700 Subject: [PATCH] Pull roles from resource policies Fixes #3256 --- server/auth/canUserAccessResource.ts | 120 ++++++++++++++---- server/private/routers/ssh/signSshKey.ts | 147 ++++++++++++++++------- 2 files changed, 198 insertions(+), 69 deletions(-) diff --git a/server/auth/canUserAccessResource.ts b/server/auth/canUserAccessResource.ts index 2c8911490..e7d6307cf 100644 --- a/server/auth/canUserAccessResource.ts +++ b/server/auth/canUserAccessResource.ts @@ -1,6 +1,12 @@ import { db } from "@server/db"; -import { and, eq, inArray } from "drizzle-orm"; -import { roleResources, userResources } from "@server/db"; +import { and, eq, inArray, isNull, or } from "drizzle-orm"; +import { + rolePolicies, + roleResources, + resources, + userPolicies, + userResources +} from "@server/db"; export async function canUserAccessResource({ userId, @@ -11,9 +17,14 @@ export async function canUserAccessResource({ resourceId: number; roleIds: number[]; }): Promise { - const roleResourceAccess = + const [ + roleResourceAccess, + rolePolicyAccess, + userResourceAccess, + userPolicyAccess + ] = await Promise.all([ roleIds.length > 0 - ? await db + ? db .select() .from(roleResources) .where( @@ -23,26 +34,87 @@ export async function canUserAccessResource({ ) ) .limit(1) - : []; - - if (roleResourceAccess.length > 0) { - return true; - } - - const userResourceAccess = await db - .select() - .from(userResources) - .where( - and( - eq(userResources.userId, userId), - eq(userResources.resourceId, resourceId) + : [], + roleIds.length > 0 + ? db + .select({ + roleId: rolePolicies.roleId, + resourcePolicyId: rolePolicies.resourcePolicyId + }) + .from(rolePolicies) + .innerJoin( + resources, + // Shared policy wins; only use default policy when no shared + // policy is assigned to the resource. + or( + eq( + resources.resourcePolicyId, + rolePolicies.resourcePolicyId + ), + and( + isNull(resources.resourcePolicyId), + eq( + resources.defaultResourcePolicyId, + rolePolicies.resourcePolicyId + ) + ) + ) + ) + .where( + and( + eq(resources.resourceId, resourceId), + inArray(rolePolicies.roleId, roleIds) + ) + ) + .limit(1) + : [], + db + .select() + .from(userResources) + .where( + and( + eq(userResources.userId, userId), + eq(userResources.resourceId, resourceId) + ) ) - ) - .limit(1); + .limit(1), + db + .select({ + userId: userPolicies.userId, + resourcePolicyId: userPolicies.resourcePolicyId + }) + .from(userPolicies) + .innerJoin( + resources, + // Shared policy wins; only use default policy when no shared + // policy is assigned to the resource. + or( + eq( + resources.resourcePolicyId, + userPolicies.resourcePolicyId + ), + and( + isNull(resources.resourcePolicyId), + eq( + resources.defaultResourcePolicyId, + userPolicies.resourcePolicyId + ) + ) + ) + ) + .where( + and( + eq(resources.resourceId, resourceId), + eq(userPolicies.userId, userId) + ) + ) + .limit(1) + ]); - if (userResourceAccess.length > 0) { - return true; - } - - return false; + return ( + roleResourceAccess.length > 0 || + rolePolicyAccess.length > 0 || + userResourceAccess.length > 0 || + userPolicyAccess.length > 0 + ); } diff --git a/server/private/routers/ssh/signSshKey.ts b/server/private/routers/ssh/signSshKey.ts index bcf8beab7..fc2319d53 100644 --- a/server/private/routers/ssh/signSshKey.ts +++ b/server/private/routers/ssh/signSshKey.ts @@ -20,6 +20,7 @@ import { logsDb, newts, roles, + rolePolicies, roleResources, roleSiteResources, resources, @@ -40,7 +41,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { and, eq, inArray, or } from "drizzle-orm"; +import { and, eq, inArray, isNull, or } from "drizzle-orm"; import { canUserAccessResource } from "@server/auth/canUserAccessResource"; import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource"; import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA"; @@ -435,50 +436,106 @@ export async function signSshKey( usernameToUse = userOrg.pamUsername; } - const roleRows = - type === "private" - ? await db - .select({ - sshSudoCommands: roles.sshSudoCommands, - sshUnixGroups: roles.sshUnixGroups, - sshCreateHomeDir: roles.sshCreateHomeDir, - sshSudoMode: roles.sshSudoMode - }) - .from(roles) - .innerJoin( - roleSiteResources, - eq(roleSiteResources.roleId, roles.roleId) - ) - .where( - and( - inArray(roles.roleId, roleIds), - eq( - roleSiteResources.siteResourceId, - (resource as SiteResource).siteResourceId - ) - ) - ) - : await db - .select({ - sshSudoCommands: roles.sshSudoCommands, - sshUnixGroups: roles.sshUnixGroups, - sshCreateHomeDir: roles.sshCreateHomeDir, - sshSudoMode: roles.sshSudoMode - }) - .from(roles) - .innerJoin( - roleResources, - eq(roleResources.roleId, roles.roleId) - ) - .where( - and( - inArray(roles.roleId, roleIds), - eq( - roleResources.resourceId, - (resource as Resource).resourceId - ) - ) - ); + type RoleSshMeta = { + roleId: number; + sshSudoCommands: string | null; + sshUnixGroups: string | null; + sshCreateHomeDir: boolean | null; + sshSudoMode: string | null; + }; + + let roleRows: RoleSshMeta[] = []; + + if (type === "private") { + roleRows = await db + .select({ + roleId: roles.roleId, + sshSudoCommands: roles.sshSudoCommands, + sshUnixGroups: roles.sshUnixGroups, + sshCreateHomeDir: roles.sshCreateHomeDir, + sshSudoMode: roles.sshSudoMode + }) + .from(roles) + .innerJoin( + roleSiteResources, + eq(roleSiteResources.roleId, roles.roleId) + ) + .where( + and( + inArray(roles.roleId, roleIds), + eq( + roleSiteResources.siteResourceId, + (resource as SiteResource).siteResourceId + ) + ) + ); + } else { + const publicResourceId = (resource as Resource).resourceId; + const [directRoleRows, policyRoleRows] = await Promise.all([ + db + .select({ + roleId: roles.roleId, + sshSudoCommands: roles.sshSudoCommands, + sshUnixGroups: roles.sshUnixGroups, + sshCreateHomeDir: roles.sshCreateHomeDir, + sshSudoMode: roles.sshSudoMode + }) + .from(roles) + .innerJoin( + roleResources, + eq(roleResources.roleId, roles.roleId) + ) + .where( + and( + inArray(roles.roleId, roleIds), + eq(roleResources.resourceId, publicResourceId) + ) + ), + db + .select({ + roleId: roles.roleId, + sshSudoCommands: roles.sshSudoCommands, + sshUnixGroups: roles.sshUnixGroups, + sshCreateHomeDir: roles.sshCreateHomeDir, + sshSudoMode: roles.sshSudoMode + }) + .from(roles) + .innerJoin( + rolePolicies, + eq(rolePolicies.roleId, roles.roleId) + ) + .innerJoin( + resources, + or( + eq( + resources.resourcePolicyId, + rolePolicies.resourcePolicyId + ), + and( + isNull(resources.resourcePolicyId), + eq( + resources.defaultResourcePolicyId, + rolePolicies.resourcePolicyId + ) + ) + ) + ) + .where( + and( + inArray(roles.roleId, roleIds), + eq(resources.resourceId, publicResourceId) + ) + ) + ]); + + const uniqueByRoleId = new Map(); + for (const row of [...directRoleRows, ...policyRoleRows]) { + if (!uniqueByRoleId.has(row.roleId)) { + uniqueByRoleId.set(row.roleId, row); + } + } + roleRows = Array.from(uniqueByRoleId.values()); + } const parsedSudoCommands: string[] = []; const parsedGroupsSet = new Set();