mirror of
https://github.com/documenso/documenso.git
synced 2026-09-29 16:24:30 +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 { 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: {
|
||||||
|
|||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user