Pull roles from resource policies

Fixes #3256
This commit is contained in:
Owen
2026-06-12 14:06:57 -07:00
parent d985bfd3a6
commit 471ae98204
2 changed files with 198 additions and 69 deletions
+96 -24
View File
@@ -1,6 +1,12 @@
import { db } from "@server/db"; import { db } from "@server/db";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray, isNull, or } from "drizzle-orm";
import { roleResources, userResources } from "@server/db"; import {
rolePolicies,
roleResources,
resources,
userPolicies,
userResources
} from "@server/db";
export async function canUserAccessResource({ export async function canUserAccessResource({
userId, userId,
@@ -11,9 +17,14 @@ export async function canUserAccessResource({
resourceId: number; resourceId: number;
roleIds: number[]; roleIds: number[];
}): Promise<boolean> { }): Promise<boolean> {
const roleResourceAccess = const [
roleResourceAccess,
rolePolicyAccess,
userResourceAccess,
userPolicyAccess
] = await Promise.all([
roleIds.length > 0 roleIds.length > 0
? await db ? db
.select() .select()
.from(roleResources) .from(roleResources)
.where( .where(
@@ -23,26 +34,87 @@ export async function canUserAccessResource({
) )
) )
.limit(1) .limit(1)
: []; : [],
roleIds.length > 0
if (roleResourceAccess.length > 0) { ? db
return true; .select({
} roleId: rolePolicies.roleId,
resourcePolicyId: rolePolicies.resourcePolicyId
const userResourceAccess = await db })
.select() .from(rolePolicies)
.from(userResources) .innerJoin(
.where( resources,
and( // Shared policy wins; only use default policy when no shared
eq(userResources.userId, userId), // policy is assigned to the resource.
eq(userResources.resourceId, resourceId) 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 (
return true; roleResourceAccess.length > 0 ||
} rolePolicyAccess.length > 0 ||
userResourceAccess.length > 0 ||
return false; userPolicyAccess.length > 0
);
} }
+102 -45
View File
@@ -20,6 +20,7 @@ import {
logsDb, logsDb,
newts, newts,
roles, roles,
rolePolicies,
roleResources, roleResources,
roleSiteResources, roleSiteResources,
resources, resources,
@@ -40,7 +41,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; 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 { canUserAccessResource } from "@server/auth/canUserAccessResource";
import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource"; import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource";
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA"; import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
@@ -435,50 +436,106 @@ export async function signSshKey(
usernameToUse = userOrg.pamUsername; usernameToUse = userOrg.pamUsername;
} }
const roleRows = type RoleSshMeta = {
type === "private" roleId: number;
? await db sshSudoCommands: string | null;
.select({ sshUnixGroups: string | null;
sshSudoCommands: roles.sshSudoCommands, sshCreateHomeDir: boolean | null;
sshUnixGroups: roles.sshUnixGroups, sshSudoMode: string | null;
sshCreateHomeDir: roles.sshCreateHomeDir, };
sshSudoMode: roles.sshSudoMode
}) let roleRows: RoleSshMeta[] = [];
.from(roles)
.innerJoin( if (type === "private") {
roleSiteResources, roleRows = await db
eq(roleSiteResources.roleId, roles.roleId) .select({
) roleId: roles.roleId,
.where( sshSudoCommands: roles.sshSudoCommands,
and( sshUnixGroups: roles.sshUnixGroups,
inArray(roles.roleId, roleIds), sshCreateHomeDir: roles.sshCreateHomeDir,
eq( sshSudoMode: roles.sshSudoMode
roleSiteResources.siteResourceId, })
(resource as SiteResource).siteResourceId .from(roles)
) .innerJoin(
) roleSiteResources,
) eq(roleSiteResources.roleId, roles.roleId)
: await db )
.select({ .where(
sshSudoCommands: roles.sshSudoCommands, and(
sshUnixGroups: roles.sshUnixGroups, inArray(roles.roleId, roleIds),
sshCreateHomeDir: roles.sshCreateHomeDir, eq(
sshSudoMode: roles.sshSudoMode roleSiteResources.siteResourceId,
}) (resource as SiteResource).siteResourceId
.from(roles) )
.innerJoin( )
roleResources, );
eq(roleResources.roleId, roles.roleId) } else {
) const publicResourceId = (resource as Resource).resourceId;
.where( const [directRoleRows, policyRoleRows] = await Promise.all([
and( db
inArray(roles.roleId, roleIds), .select({
eq( roleId: roles.roleId,
roleResources.resourceId, sshSudoCommands: roles.sshSudoCommands,
(resource as Resource).resourceId 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<number, RoleSshMeta>();
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 parsedSudoCommands: string[] = [];
const parsedGroupsSet = new Set<string>(); const parsedGroupsSet = new Set<string>();