Compare commits

...
Author SHA1 Message Date
Cursor AgentandMarc Seitz d86b8ff693 fix: use non-root item check instead of _count.accessControls for granular filter
The previous _count.accessControls === 0 check incorrectly treated
groups that only had auto-granted root-level permissions as
admin-curated, blocking them from receiving future root auto-grants.

Replace with a proper non-root item check: fetch each group's access
controls (itemId + itemType), then batch-query the referenced
DataroomFolders and DataroomDocuments to determine which are non-root
(folder.parentId !== null or document.folderId !== null). Only groups
that have at least one access control targeting a non-root item are
considered admin-curated and excluded from auto-grants.

Groups whose access controls only reference root-level items (folders
with parentId=null, documents with folderId=null) continue to receive
new root auto-grants as expected.

Extracted a shared getNonRootItemIds() helper in both files that
efficiently batch-resolves item root/non-root status.

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-02-16 02:57:14 +00:00
Cursor AgentandMarc Seitz 892000ab8e fix: prevent root-level items from auto-granting access to groups with granular permissions
When a permission group or viewer group has existing granular access
controls configured (e.g., only allowed to view a single folder), new
documents or folders uploaded to the root of the dataroom should NOT
automatically receive view permissions for those groups.

Previously, the INHERIT_FROM_PARENT strategy would blindly grant all
groups view-only access to new root-level items, which defeated the
purpose of granular permissions. A link restricted to only show one
folder would suddenly also show newly uploaded root documents.

Now, the applyRootLevelPermissions and applyDefaultFolderPermissions
functions check if a group already has any access controls configured.
If it does, the group is skipped for auto-permission assignment, since
the admin has intentionally curated that group's access.

This also means 'hidden by default' continues to work correctly (it
never created permissions), and groups with no access controls yet
still get the default root-level view-only access as before.

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-02-16 01:28:39 +00:00
2 changed files with 198 additions and 44 deletions
@@ -145,6 +145,53 @@ async function applyPermissionStrategy(
}
}
/**
* Given a list of access control entries (itemId + itemType), batch-queries
* the referenced DataroomFolders and DataroomDocuments to find which ones
* are non-root (folder.parentId !== null or document.folderId !== null).
* Returns the set of non-root itemIds.
*/
async function getNonRootItemIds(
accessControls: { itemId: string; itemType: ItemType }[],
): Promise<Set<string>> {
if (accessControls.length === 0) return new Set();
const folderItemIds = [
...new Set(
accessControls
.filter((ac) => ac.itemType === ItemType.DATAROOM_FOLDER)
.map((ac) => ac.itemId),
),
];
const documentItemIds = [
...new Set(
accessControls
.filter((ac) => ac.itemType === ItemType.DATAROOM_DOCUMENT)
.map((ac) => ac.itemId),
),
];
const [nonRootFolders, nonRootDocuments] = await Promise.all([
folderItemIds.length > 0
? prisma.dataroomFolder.findMany({
where: { id: { in: folderItemIds }, parentId: { not: null } },
select: { id: true },
})
: Promise.resolve([]),
documentItemIds.length > 0
? prisma.dataroomDocument.findMany({
where: { id: { in: documentItemIds }, folderId: { not: null } },
select: { id: true },
})
: Promise.resolve([]),
]);
return new Set([
...nonRootFolders.map((f) => f.id),
...nonRootDocuments.map((d) => d.id),
]);
}
async function applyRootLevelPermissions(
dataroomId: string,
dataroomDocuments: {
@@ -153,51 +200,82 @@ async function applyRootLevelPermissions(
folderId: string | null;
}[],
) {
// Get both ViewerGroups and PermissionGroups
// Fetch groups together with their existing access controls so we can
// distinguish "admin-curated granular" groups from groups that only have
// previously auto-granted root-level permissions.
const [viewerGroups, permissionGroups] = await Promise.all([
prisma.viewerGroup.findMany({
where: { dataroomId },
select: { id: true },
select: {
id: true,
accessControls: {
select: { itemId: true, itemType: true },
},
},
}),
prisma.permissionGroup.findMany({
where: { dataroomId },
select: { id: true },
select: {
id: true,
accessControls: {
select: { itemId: true, itemType: true },
},
},
}),
]);
// Collect every access-control entry across all groups and resolve which
// referenced items live inside a subfolder (non-root).
const allAccessControls = [
...viewerGroups.flatMap((g) => g.accessControls),
...permissionGroups.flatMap((g) => g.accessControls),
];
const nonRootItemIds = await getNonRootItemIds(allAccessControls);
// A group is considered admin-curated when it has at least one access
// control pointing at a non-root item (subfolder or document inside a
// folder). Groups that only have root-level access controls were
// auto-granted and should continue to receive new root auto-grants.
const hasCuratedPermissions = (
acs: { itemId: string; itemType: ItemType }[],
) => acs.some((ac) => nonRootItemIds.has(ac.itemId));
const viewerGroupPermissionsToCreate: any[] = [];
const permissionGroupPermissionsToCreate: any[] = [];
// ViewerGroup permissions - all get view-only access
viewerGroups.forEach((group) => {
dataroomDocuments.forEach((doc) => {
viewerGroupPermissionsToCreate.push({
groupId: group.id,
itemId: doc.id,
itemType: ItemType.DATAROOM_DOCUMENT,
canView: true,
canDownload: false,
// ViewerGroup permissions skip groups with admin-curated non-root access
viewerGroups
.filter((group) => !hasCuratedPermissions(group.accessControls))
.forEach((group) => {
dataroomDocuments.forEach((doc) => {
viewerGroupPermissionsToCreate.push({
groupId: group.id,
itemId: doc.id,
itemType: ItemType.DATAROOM_DOCUMENT,
canView: true,
canDownload: false,
});
});
});
});
// PermissionGroup permissions - all get view-only access
permissionGroups.forEach((group) => {
dataroomDocuments.forEach((doc) => {
permissionGroupPermissionsToCreate.push({
groupId: group.id,
itemId: doc.id,
itemType: ItemType.DATAROOM_DOCUMENT,
canView: true,
canDownload: false,
canDownloadOriginal: false,
// PermissionGroup permissions skip groups with admin-curated non-root access
permissionGroups
.filter((group) => !hasCuratedPermissions(group.accessControls))
.forEach((group) => {
dataroomDocuments.forEach((doc) => {
permissionGroupPermissionsToCreate.push({
groupId: group.id,
itemId: doc.id,
itemType: ItemType.DATAROOM_DOCUMENT,
canView: true,
canDownload: false,
canDownloadOriginal: false,
});
});
});
});
// Apply permissions in a transaction
await prisma.$transaction(async (tx) => {
// Create new permissions
if (viewerGroupPermissionsToCreate.length > 0) {
await tx.viewerGroupAccessControls.createMany({
data: viewerGroupPermissionsToCreate,
@@ -21,11 +21,60 @@ async function applyFolderPermissions(
}
}
/**
* Given a list of access control entries (itemId + itemType), batch-queries
* the referenced DataroomFolders and DataroomDocuments to find which ones
* are non-root (folder.parentId !== null or document.folderId !== null).
* Returns the set of non-root itemIds.
*/
async function getNonRootItemIds(
accessControls: { itemId: string; itemType: ItemType }[],
): Promise<Set<string>> {
if (accessControls.length === 0) return new Set();
const folderItemIds = [
...new Set(
accessControls
.filter((ac) => ac.itemType === ItemType.DATAROOM_FOLDER)
.map((ac) => ac.itemId),
),
];
const documentItemIds = [
...new Set(
accessControls
.filter((ac) => ac.itemType === ItemType.DATAROOM_DOCUMENT)
.map((ac) => ac.itemId),
),
];
const [nonRootFolders, nonRootDocuments] = await Promise.all([
folderItemIds.length > 0
? prisma.dataroomFolder.findMany({
where: { id: { in: folderItemIds }, parentId: { not: null } },
select: { id: true },
})
: Promise.resolve([]),
documentItemIds.length > 0
? prisma.dataroomDocument.findMany({
where: { id: { in: documentItemIds }, folderId: { not: null } },
select: { id: true },
})
: Promise.resolve([]),
]);
return new Set([
...nonRootFolders.map((f) => f.id),
...nonRootDocuments.map((d) => d.id),
]);
}
async function applyDefaultFolderPermissions(
dataroomId: string,
folderId: string,
folderPath?: string,
) {
// Fetch groups with their access controls so we can distinguish
// admin-curated groups from groups with only auto-granted root permissions.
const [dataroom, viewerGroups, permissionGroups] = await Promise.all([
prisma.dataroom.findUnique({
where: { id: dataroomId },
@@ -38,6 +87,9 @@ async function applyDefaultFolderPermissions(
where: { dataroomId },
select: {
id: true,
accessControls: {
select: { itemId: true, itemType: true },
},
},
}),
prisma.permissionGroup.findMany({
@@ -45,6 +97,9 @@ async function applyDefaultFolderPermissions(
select: {
id: true,
name: true,
accessControls: {
select: { itemId: true, itemType: true },
},
},
}),
]);
@@ -61,7 +116,23 @@ async function applyDefaultFolderPermissions(
return;
}
// Fallback to default behavior (for root folders or non-inherit strategies)
// Resolve which existing access-control items are non-root so we can tell
// apart admin-curated groups from groups that only received prior
// auto-granted root-level permissions.
const allAccessControls = [
...viewerGroups.flatMap((g) => g.accessControls),
...permissionGroups.flatMap((g) => g.accessControls),
];
const nonRootItemIds = await getNonRootItemIds(allAccessControls);
const hasCuratedPermissions = (
acs: { itemId: string; itemType: ItemType }[],
) => acs.some((ac) => nonRootItemIds.has(ac.itemId));
// Fallback to default behavior (for root folders or non-inherit strategies).
// Groups with admin-curated non-root permissions are excluded new root
// folders should not be auto-granted to groups the admin intentionally
// restricted to specific subfolders.
const allPermissionGroupData: {
groupId: string;
itemId: string;
@@ -76,29 +147,34 @@ async function applyDefaultFolderPermissions(
dataroom.defaultPermissionStrategy ===
DefaultPermissionStrategy.INHERIT_FROM_PARENT
) {
permissionGroups.forEach((group) => {
allPermissionGroupData.push({
groupId: group.id,
itemId: folderId,
itemType: ItemType.DATAROOM_FOLDER,
canView: true, // Root folders get view permissions by default
canDownload: false,
canDownloadOriginal: false,
permissionGroups
.filter((group) => !hasCuratedPermissions(group.accessControls))
.forEach((group) => {
allPermissionGroupData.push({
groupId: group.id,
itemId: folderId,
itemType: ItemType.DATAROOM_FOLDER,
canView: true, // Root folders get view permissions by default
canDownload: false,
canDownloadOriginal: false,
});
});
});
}
// For other strategies (ASK_EVERY_TIME, HIDDEN_BY_DEFAULT), don't auto-create permissions
}
const viewerGroupData = viewerGroups.map((group) => ({
groupId: group.id,
itemId: folderId,
itemType: ItemType.DATAROOM_FOLDER,
canView:
dataroom.defaultPermissionStrategy ===
DefaultPermissionStrategy.INHERIT_FROM_PARENT,
canDownload: false,
}));
// Only auto-grant root folder access to viewer groups without curated non-root permissions
const viewerGroupData = viewerGroups
.filter((group) => !hasCuratedPermissions(group.accessControls))
.map((group) => ({
groupId: group.id,
itemId: folderId,
itemType: ItemType.DATAROOM_FOLDER,
canView:
dataroom.defaultPermissionStrategy ===
DefaultPermissionStrategy.INHERIT_FROM_PARENT,
canDownload: false,
}));
await Promise.all([
viewerGroupData.length > 0 &&