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 { 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 { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { sValidator } from '@hono/standard-validator'; import { sValidator } from '@hono/standard-validator';
import { DocumentStatus, EnvelopeType } from '@prisma/client'; import { DocumentStatus, EnvelopeType } from '@prisma/client';
@@ -75,12 +74,35 @@ export const downloadRoute = new Hono<HonoEnv>()
version, 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({ const envelopeItem = await prisma.envelopeItem.findFirst({
where: { where: {
id: envelopeItemId, id: envelopeItemId,
envelope: { envelope: envelopeWhereInput,
team: buildTeamWhereQuery({ teamId: apiToken.teamId, userId: apiToken.user.id }),
},
}, },
include: { include: {
envelope: { envelope: {
+32 -7
View File
@@ -1,18 +1,21 @@
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session'; import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; 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 { 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 { sha256 } from '@documenso/lib/universal/crypto';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server'; import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { canAccessTeamDocument } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { import {
type DocumentDataType, type DocumentDataType,
DocumentStatus, DocumentStatus,
type DocumentVisibility,
type EnvelopeType, type EnvelopeType,
EnvelopeType as EnvelopeTypeEnum, EnvelopeType as EnvelopeTypeEnum,
type RecipientRole, type RecipientRole,
type SigningStatus, type SigningStatus,
TeamMemberRole,
type TemplateType, type TemplateType,
TemplateType as TemplateTypeEnum, TemplateType as TemplateTypeEnum,
} from '@prisma/client'; } from '@prisma/client';
@@ -262,30 +265,51 @@ const handlePendingFileRequest = async ({
type CheckEnvelopeFileAccessOptions = { type CheckEnvelopeFileAccessOptions = {
userId: number; userId: number;
teamId: number; teamId: number;
envelopeId: string;
envelopeType: EnvelopeType; envelopeType: EnvelopeType;
templateType: TemplateType; templateType: TemplateType;
visibility: DocumentVisibility;
}; };
/** /**
* Check whether a user has access to an envelope's file. * Check whether a user has access to an envelope's file.
* *
* First checks team membership. If that fails and the envelope is an * Mirrors the tRPC read paths (owner / team role permitting `visibility` / team
* ORGANISATION template (not a document), falls back to checking whether * email). If that fails and the envelope is an ORGANISATION template (not a
* the user belongs to any team in the same organisation. * 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 ({ export const checkEnvelopeFileAccess = async ({
userId, userId,
teamId, teamId,
envelopeId,
envelopeType, envelopeType,
templateType, templateType,
visibility,
}: CheckEnvelopeFileAccessOptions): Promise<boolean> => { }: 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) { const envelope = await prisma.envelope.findFirst({ where: envelopeWhereInput, select: { id: true } });
return 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) { if (envelopeType === EnvelopeTypeEnum.TEMPLATE && templateType === TemplateTypeEnum.ORGANISATION) {
const rolesWithAccess = Object.values(TeamMemberRole).filter((role) => canAccessTeamDocument(role, visibility));
const orgAccess = await prisma.team.findFirst({ const orgAccess = await prisma.team.findFirst({
where: { where: {
id: teamId, 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({ const hasAccess = await checkEnvelopeFileAccess({
userId, userId,
teamId: envelope.teamId, teamId: envelope.teamId,
envelopeId,
envelopeType: envelope.type, envelopeType: envelope.type,
templateType: envelope.templateType, templateType: envelope.templateType,
visibility: envelope.visibility,
}); });
if (!hasAccess) { 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) { if (!envelopeItem.documentData) {
@@ -182,17 +184,14 @@ export const filesRoute = new Hono<HonoEnv>()
const hasDownloadAccess = await checkEnvelopeFileAccess({ const hasDownloadAccess = await checkEnvelopeFileAccess({
userId: session.user.id, userId: session.user.id,
teamId: envelope.teamId, teamId: envelope.teamId,
envelopeId,
envelopeType: envelope.type, envelopeType: envelope.type,
templateType: envelope.templateType, templateType: envelope.templateType,
visibility: envelope.visibility,
}); });
if (!hasDownloadAccess) { if (!hasDownloadAccess) {
return c.json( return c.json({ error: 'User does not have access to this envelope' }, 403);
{
error: 'User does not have access to the team that this envelope is associated with',
},
403,
);
} }
if (!envelopeItem.documentData) { if (!envelopeItem.documentData) {
@@ -54,7 +54,7 @@ route.get(
return c.json({ error: 'Not found' }, 404); 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({ const envelopeItem = await prisma.envelopeItem.findFirst({
where: { where: {
id: envelopeItemId, id: envelopeItemId,
@@ -69,6 +69,7 @@ route.get(
type: true, type: true,
teamId: true, teamId: true,
templateType: true, templateType: true,
visibility: true,
}, },
}, },
}, },
@@ -82,8 +83,10 @@ route.get(
const hasAccess = await checkEnvelopeFileAccess({ const hasAccess = await checkEnvelopeFileAccess({
userId, userId,
teamId: envelopeItem.envelope.teamId, teamId: envelopeItem.envelope.teamId,
envelopeId,
envelopeType: envelopeItem.envelope.type, envelopeType: envelopeItem.envelope.type,
templateType: envelopeItem.envelope.templateType, templateType: envelopeItem.envelope.templateType,
visibility: envelopeItem.envelope.visibility,
}); });
if (!hasAccess) { if (!hasAccess) {