fix: enforce document visibility on envelope file routes

This commit is contained in:
Catalin Pit
2026-09-17 13:20:33 +03:00
parent e73450dd08
commit 9d024a07f7
4 changed files with 68 additions and 19 deletions
+26 -4
View File
@@ -5,7 +5,6 @@ import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-aud
import { generateCertificatePdf } from '@documenso/lib/server-only/pdf/generate-certificate-pdf';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { sValidator } from '@hono/standard-validator';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
@@ -75,12 +74,35 @@ export const downloadRoute = new Hono<HonoEnv>()
version,
});
const envelopeItemReference = await prisma.envelopeItem.findUnique({
where: {
id: envelopeItemId,
},
select: {
envelopeId: true,
},
});
if (!envelopeItemReference) {
return c.json({ error: 'Envelope item not found' }, 404);
}
// Apply the same owner / team-role visibility / team-email rules as every
// other envelope read path, rather than bare team membership.
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: envelopeItemReference.envelopeId,
},
type: null,
userId: apiToken.user.id,
teamId: apiToken.teamId,
});
const envelopeItem = await prisma.envelopeItem.findFirst({
where: {
id: envelopeItemId,
envelope: {
team: buildTeamWhereQuery({ teamId: apiToken.teamId, userId: apiToken.user.id }),
},
envelope: envelopeWhereInput,
},
include: {
envelope: {
+32 -7
View File
@@ -1,18 +1,21 @@
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { generatePartialSignedPdf } from '@documenso/lib/server-only/pdf/generate-partial-signed-pdf';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { sha256 } from '@documenso/lib/universal/crypto';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { canAccessTeamDocument } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import {
type DocumentDataType,
DocumentStatus,
type DocumentVisibility,
type EnvelopeType,
EnvelopeType as EnvelopeTypeEnum,
type RecipientRole,
type SigningStatus,
TeamMemberRole,
type TemplateType,
TemplateType as TemplateTypeEnum,
} from '@prisma/client';
@@ -262,30 +265,51 @@ const handlePendingFileRequest = async ({
type CheckEnvelopeFileAccessOptions = {
userId: number;
teamId: number;
envelopeId: string;
envelopeType: EnvelopeType;
templateType: TemplateType;
visibility: DocumentVisibility;
};
/**
* Check whether a user has access to an envelope's file.
*
* First checks team membership. If that fails and the envelope is an
* ORGANISATION template (not a document), falls back to checking whether
* the user belongs to any team in the same organisation.
* Mirrors the tRPC read paths (owner / team role permitting `visibility` / team
* email). If that fails and the envelope is an ORGANISATION template (not a
* document), falls back to checking whether the user belongs to any team in the
* same organisation with a role that permits the template's `visibility`.
*/
export const checkEnvelopeFileAccess = async ({
userId,
teamId,
envelopeId,
envelopeType,
templateType,
visibility,
}: CheckEnvelopeFileAccessOptions): Promise<boolean> => {
const team = await getTeamById({ userId, teamId }).catch(() => null);
try {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: { type: 'envelopeId', id: envelopeId },
type: envelopeType,
userId,
teamId,
});
if (team) {
return true;
const envelope = await prisma.envelope.findFirst({ where: envelopeWhereInput, select: { id: true } });
if (envelope) {
return true;
}
} catch (error) {
// NOT_FOUND means the user is not a member of the envelope's team. Anything else is a real failure.
if (!(error instanceof AppError && error.code === AppErrorCode.NOT_FOUND)) {
throw error;
}
}
if (envelopeType === EnvelopeTypeEnum.TEMPLATE && templateType === TemplateTypeEnum.ORGANISATION) {
const rolesWithAccess = Object.values(TeamMemberRole).filter((role) => canAccessTeamDocument(role, visibility));
const orgAccess = await prisma.team.findFirst({
where: {
id: teamId,
@@ -301,6 +325,7 @@ export const checkEnvelopeFileAccess = async ({
},
},
},
teamRole: { in: rolesWithAccess },
},
},
},
+6 -7
View File
@@ -110,12 +110,14 @@ export const filesRoute = new Hono<HonoEnv>()
const hasAccess = await checkEnvelopeFileAccess({
userId,
teamId: envelope.teamId,
envelopeId,
envelopeType: envelope.type,
templateType: envelope.templateType,
visibility: envelope.visibility,
});
if (!hasAccess) {
return c.json({ error: 'User does not have access to the team that this envelope is associated with' }, 403);
return c.json({ error: 'User does not have access to this envelope' }, 403);
}
if (!envelopeItem.documentData) {
@@ -182,17 +184,14 @@ export const filesRoute = new Hono<HonoEnv>()
const hasDownloadAccess = await checkEnvelopeFileAccess({
userId: session.user.id,
teamId: envelope.teamId,
envelopeId,
envelopeType: envelope.type,
templateType: envelope.templateType,
visibility: envelope.visibility,
});
if (!hasDownloadAccess) {
return c.json(
{
error: 'User does not have access to the team that this envelope is associated with',
},
403,
);
return c.json({ error: 'User does not have access to this envelope' }, 403);
}
if (!envelopeItem.documentData) {
@@ -54,7 +54,7 @@ route.get(
return c.json({ error: 'Not found' }, 404);
}
// Note: We authenticate whether the user can access this in the `getTeamById` below.
// Note: We authorize whether the user can access this in `checkEnvelopeFileAccess` below.
const envelopeItem = await prisma.envelopeItem.findFirst({
where: {
id: envelopeItemId,
@@ -69,6 +69,7 @@ route.get(
type: true,
teamId: true,
templateType: true,
visibility: true,
},
},
},
@@ -82,8 +83,10 @@ route.get(
const hasAccess = await checkEnvelopeFileAccess({
userId,
teamId: envelopeItem.envelope.teamId,
envelopeId,
envelopeType: envelopeItem.envelope.type,
templateType: envelopeItem.envelope.templateType,
visibility: envelopeItem.envelope.visibility,
});
if (!hasAccess) {