Merge pull request #4317 from umami-software/opencode/nimble-planet
Fix board share entity authorization
This commit is contained in:
7 files changed
+252
-10
No files matched your search
@@ -1,8 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
import { BOARD_TYPES, normalizeBoardType } from '@/lib/boards';
|
||||
import type { BoardParameters } from '@/lib/types';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, ok, serverError, unauthorized } from '@/lib/response';
|
||||
import { canDeleteBoard, canUpdateBoard, canViewBoard } from '@/permissions';
|
||||
import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
|
||||
import { canDeleteBoard, canUpdateBoard, canViewBoard, canViewBoardEntities } from '@/permissions';
|
||||
import { deleteBoard, getBoard, updateBoard } from '@/queries/prisma';
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ boardId: string }> }) {
|
||||
@@ -54,6 +55,21 @@ export async function POST(request: Request, { params }: { params: Promise<{ boa
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
if (type !== undefined || parameters !== undefined) {
|
||||
const currentBoard = await getBoard(boardId);
|
||||
|
||||
if (!currentBoard) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const nextType = type ?? currentBoard.type;
|
||||
const nextParameters = (parameters ?? currentBoard.parameters) as BoardParameters;
|
||||
|
||||
if (!(await canViewBoardEntities(auth, nextType, nextParameters))) {
|
||||
return badRequest({ message: 'Board contains inaccessible entities.' });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const board = await updateBoard(boardId, { type, name, description, parameters });
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import { z } from 'zod';
|
||||
import { BOARD_TYPES, normalizeBoardType } from '@/lib/boards';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { badRequest, json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions';
|
||||
import { canCreateTeamWebsite, canCreateWebsite, canViewBoardEntities } from '@/permissions';
|
||||
import { createBoard, getUserBoards } from '@/queries/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
@@ -58,6 +58,10 @@ export async function POST(request: Request) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
if (!(await canViewBoardEntities(auth, body.type, body.parameters))) {
|
||||
return badRequest({ message: 'Board contains inaccessible entities.' });
|
||||
}
|
||||
|
||||
const data = {
|
||||
...body,
|
||||
type: normalizeBoardType(body.type),
|
||||
|
||||
@@ -5,8 +5,12 @@ import { createToken } from '@/lib/jwt';
|
||||
import prisma from '@/lib/prisma';
|
||||
import redis from '@/lib/redis';
|
||||
import { json, notFound } from '@/lib/response';
|
||||
import type { BoardParameters, WhiteLabel } from '@/lib/types';
|
||||
import { getBoard, getLink, getPixel, getShareByCode, getWebsite } from '@/queries/prisma';
|
||||
import type { Auth, BoardParameters, WhiteLabel } from '@/lib/types';
|
||||
import { canViewLink, canViewPixel, canViewWebsite } from '@/permissions';
|
||||
import { getBoard, getLink, getPixel, getShareByCode, getUser, getWebsite } from '@/queries/prisma';
|
||||
|
||||
type BoardEntityIds = ReturnType<typeof getBoardEntityIds>;
|
||||
type OwnedEntity = { userId?: string | null; teamId?: string | null } | null;
|
||||
|
||||
async function getAccountId(entity: { userId?: string; teamId?: string }): Promise<string | null> {
|
||||
if (entity.userId) {
|
||||
@@ -44,6 +48,85 @@ async function getWhiteLabel(accountId: string): Promise<WhiteLabel | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function filterEntityIds(
|
||||
ids: string[],
|
||||
canView: (id: string) => Promise<boolean>,
|
||||
): Promise<string[]> {
|
||||
const results = await Promise.all(
|
||||
ids.map(async id => {
|
||||
try {
|
||||
return (await canView(id)) ? id : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return results.filter((id): id is string => !!id);
|
||||
}
|
||||
|
||||
async function getTeamUserIds(teamId: string) {
|
||||
const teamUsers = await prisma.client.teamUser.findMany({
|
||||
where: { teamId },
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
return new Set(teamUsers.map(({ userId }) => userId));
|
||||
}
|
||||
|
||||
function isOwnedByTeam(entity: OwnedEntity, teamId: string, teamUserIds: Set<string>) {
|
||||
return entity?.teamId === teamId || !!(entity?.userId && teamUserIds.has(entity.userId));
|
||||
}
|
||||
|
||||
async function filterBoardEntityIdsForShare(
|
||||
entity: { userId?: string | null; teamId?: string | null },
|
||||
ids: BoardEntityIds,
|
||||
): Promise<BoardEntityIds> {
|
||||
if (entity.teamId) {
|
||||
const teamUserIds = await getTeamUserIds(entity.teamId);
|
||||
|
||||
return {
|
||||
websiteIds: await filterEntityIds(
|
||||
ids.websiteIds,
|
||||
async id => isOwnedByTeam(await getWebsite(id), entity.teamId, teamUserIds),
|
||||
),
|
||||
pixelIds: await filterEntityIds(
|
||||
ids.pixelIds,
|
||||
async id => isOwnedByTeam(await getPixel(id), entity.teamId, teamUserIds),
|
||||
),
|
||||
linkIds: await filterEntityIds(
|
||||
ids.linkIds,
|
||||
async id => isOwnedByTeam(await getLink(id), entity.teamId, teamUserIds),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (!entity.userId) {
|
||||
return { websiteIds: [], pixelIds: [], linkIds: [] };
|
||||
}
|
||||
|
||||
const user = await getUser(entity.userId);
|
||||
|
||||
if (!user) {
|
||||
return { websiteIds: [], pixelIds: [], linkIds: [] };
|
||||
}
|
||||
|
||||
const auth: Auth = {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
isAdmin: user.role === ROLES.admin,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
websiteIds: await filterEntityIds(ids.websiteIds, id => canViewWebsite(auth, id)),
|
||||
pixelIds: await filterEntityIds(ids.pixelIds, id => canViewPixel(auth, id)),
|
||||
linkIds: await filterEntityIds(ids.linkIds, id => canViewLink(auth, id)),
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
|
||||
@@ -70,9 +153,10 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
|
||||
type: board.type,
|
||||
parameters: board.parameters as BoardParameters,
|
||||
});
|
||||
data.websiteIds = boardEntityIds.websiteIds;
|
||||
data.pixelIds = boardEntityIds.pixelIds;
|
||||
data.linkIds = boardEntityIds.linkIds;
|
||||
const authorizedEntityIds = await filterBoardEntityIdsForShare(board, boardEntityIds);
|
||||
data.websiteIds = authorizedEntityIds.websiteIds;
|
||||
data.pixelIds = authorizedEntityIds.pixelIds;
|
||||
data.linkIds = authorizedEntityIds.linkIds;
|
||||
} else if (share.shareType === ENTITY_TYPE.website) {
|
||||
entity = await getWebsite(share.entityId);
|
||||
if (!entity) return notFound();
|
||||
|
||||
Reference in new issue
Block a user