mirror of
https://github.com/documenso/documenso.git
synced 2026-09-29 08:14:31 +10:00
fix: enforce document visibility on envelope file routes
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user