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

View File

@@ -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<boolean> {
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
);
}

View File

@@ -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<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 parsedGroupsSet = new Set<string>();