fix: tighten member access validation and lookup costs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-04-19 22:15:12 -05:00
1 parent f5cf100ecf
commit ebfaf499ad
3 files changed
+32 -6

No files matched your search

+1 -1
View File
@@ -121,7 +121,7 @@ export const archiveCompanyMemberSchema = z.object({
reassignment: z
.object({
assigneeAgentId: z.string().uuid().optional().nullable(),
assigneeUserId: z.string().optional().nullable(),
assigneeUserId: z.string().uuid().optional().nullable(),
})
.optional()
.nullable(),
+29 -4
View File
@@ -18,6 +18,7 @@ import {
companies,
companyLogos,
companyMemberships,
instanceUserRoles,
invites,
joinRequests,
principalPermissionGrants,
@@ -1075,11 +1076,18 @@ async function getProtectedMemberReason(
access: ReturnType<typeof accessService>,
companyId: string,
member: { principalId: string; principalType: string; membershipRole: string | null },
opts?: {
actorRole?: HumanCompanyMembershipRole | null;
instanceAdminUserIds?: ReadonlySet<string>;
},
): Promise<string | null> {
if (member.principalType !== "user") return "Only human company members can be removed.";
if (req.actor.type !== "board") return "Board access is required to remove members.";
if (member.principalId === req.actor.userId) return "You cannot remove yourself.";
if (await access.isInstanceAdmin(member.principalId)) {
const isTargetInstanceAdmin = opts?.instanceAdminUserIds
? opts.instanceAdminUserIds.has(member.principalId)
: await access.isInstanceAdmin(member.principalId);
if (isTargetInstanceAdmin) {
return "Instance admins cannot be removed from company access.";
}
@@ -1089,7 +1097,7 @@ async function getProtectedMemberReason(
if (targetRole === "owner") return "Board owners cannot be removed from company access.";
if (targetRole === "admin") return "Company admins cannot be removed from company access.";
const actorRole = await resolveActorHumanRole(req, access, companyId);
const actorRole = opts?.actorRole ?? await resolveActorHumanRole(req, access, companyId);
if (!actorRole) return "Only active company members can remove users.";
if (humanRoleRank[targetRole] >= humanRoleRank[actorRole]) {
return "You can only remove users below your company role.";
@@ -1110,13 +1118,30 @@ async function assertCanManageCompanyMember(
async function addCompanyMemberRemovalAccess(
req: Request,
db: Db,
access: ReturnType<typeof accessService>,
companyId: string,
members: CompanyMemberRecord[],
) {
const actorRole = await resolveActorHumanRole(req, access, companyId);
const userIds = [...new Set(members
.filter((member) => member.principalType === "user")
.map((member) => member.principalId))];
const instanceAdminUserIds = userIds.length > 0
? new Set(
await db
.select({ userId: instanceUserRoles.userId })
.from(instanceUserRoles)
.where(and(inArray(instanceUserRoles.userId, userIds), eq(instanceUserRoles.role, "instance_admin")))
.then((rows) => rows.map((row) => row.userId)),
)
: new Set<string>();
return Promise.all(
members.map(async (member) => {
const reason = await getProtectedMemberReason(req, access, companyId, member);
const reason = await getProtectedMemberReason(req, access, companyId, member, {
actorRole,
instanceAdminUserIds,
});
return {
...member,
removal: {
@@ -3691,7 +3716,7 @@ export function accessRoutes(
loadCompanyAccessSummary(req, access, companyId),
]);
res.json({
members: await addCompanyMemberRemovalAccess(req, access, companyId, members),
members: await addCompanyMemberRemovalAccess(req, db, access, companyId, members),
access: currentAccess,
});
});
+2 -1
View File
@@ -84,7 +84,8 @@ async function resolveCompanyUser(db: Db, companyId: string, rawSlug: string): P
eq(companyMemberships.principalType, "user"),
),
)
.orderBy(desc(companyMemberships.updatedAt));
.orderBy(desc(companyMemberships.updatedAt))
.limit(200);
return rows.find((row) => userSlugCandidates(row).includes(slug)) ?? null;
}