mirror of https://github.com/buster-so/buster.git
Refactor report retrieval to include permission checks and enhance filtering options
- Updated GET reports endpoint to use `getReportsWithPermissions` for improved access control. - Added support for filtering reports based on `shared_with_me` and `only_my_reports` flags. - Introduced `getReportsWithPermissions` function to handle complex permission logic in the database layer. - Enhanced report metadata retrieval to ensure proper access validation before fetching report details.
This commit is contained in:
parent
17ee5d2197
commit
f3cf5f46c4
|
@ -1,5 +1,5 @@
|
|||
import type { User } from '@buster/database';
|
||||
import { getReportsList } from '@buster/database';
|
||||
import { getReportsWithPermissions } from '@buster/database';
|
||||
import type { GetReportsListRequest, GetReportsListResponse } from '@buster/server-shared/reports';
|
||||
import { GetReportsListRequestSchema } from '@buster/server-shared/reports';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
@ -9,12 +9,14 @@ async function getReportsListHandler(
|
|||
request: GetReportsListRequest,
|
||||
user: User
|
||||
): Promise<GetReportsListResponse> {
|
||||
const { page, page_size } = request;
|
||||
const { page, page_size, shared_with_me, only_my_reports } = request;
|
||||
|
||||
const reports = await getReportsList({
|
||||
const reports = await getReportsWithPermissions({
|
||||
userId: user.id,
|
||||
page,
|
||||
page_size,
|
||||
sharedWithMe: shared_with_me,
|
||||
onlyMyReports: only_my_reports,
|
||||
});
|
||||
|
||||
const result: GetReportsListResponse = reports;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { getReport } from '@buster/database';
|
||||
import { hasAssetPermission } from '@buster/access-controls';
|
||||
import { getReport, getReportMetadata } from '@buster/database';
|
||||
import type { GetReportResponse } from '@buster/server-shared/reports';
|
||||
import { Hono } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
@ -8,6 +9,34 @@ export async function getReportHandler(
|
|||
reportId: string,
|
||||
user: { id: string }
|
||||
): Promise<GetReportResponse> {
|
||||
// Get report metadata for access control
|
||||
let reportData: Awaited<ReturnType<typeof getReportMetadata>>;
|
||||
try {
|
||||
reportData = await getReportMetadata({ reportId });
|
||||
} catch (error) {
|
||||
console.error('Error getting report metadata:', error);
|
||||
throw new HTTPException(404, { message: 'Report not found' });
|
||||
}
|
||||
|
||||
if (!reportData) {
|
||||
throw new HTTPException(404, { message: 'Report not found' });
|
||||
}
|
||||
|
||||
// Check access using existing asset permission system
|
||||
const hasAccess = await hasAssetPermission({
|
||||
userId: user.id,
|
||||
assetId: reportId,
|
||||
assetType: 'report_file',
|
||||
requiredRole: 'can_view',
|
||||
organizationId: reportData.organizationId,
|
||||
workspaceSharing: reportData.workspaceSharing,
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
throw new HTTPException(403, { message: 'You do not have access to this report' });
|
||||
}
|
||||
|
||||
// If access is granted, get the full report data
|
||||
const report = await getReport({ reportId, userId: user.id });
|
||||
|
||||
const response: GetReportResponse = report;
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
import { type SQL, and, count, desc, eq, exists, isNull, ne, or } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { assetPermissions, reportFiles, teamsToUsers, users } from '../../schema';
|
||||
import { getUserOrganizationId } from '../organizations';
|
||||
import { type PaginatedResponse, createPaginatedResponse } from '../shared-types';
|
||||
import { withPagination } from '../shared-types/with-pagination';
|
||||
|
||||
export const GetReportsWithPermissionsInputSchema = z.object({
|
||||
userId: z.string().uuid('User ID must be a valid UUID'),
|
||||
page: z.number().optional().default(1),
|
||||
page_size: z.number().optional().default(250),
|
||||
sharedWithMe: z.boolean().optional(),
|
||||
onlyMyReports: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type GetReportsWithPermissionsInput = z.infer<typeof GetReportsWithPermissionsInputSchema>;
|
||||
|
||||
export type ReportWithPermissionItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
publicly_accessible: boolean;
|
||||
created_by_id: string;
|
||||
created_by_name: string | null;
|
||||
created_by_avatar: string | null;
|
||||
workspace_sharing: string;
|
||||
is_shared: boolean;
|
||||
permission: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get paginated list of reports that the user has access to.
|
||||
* This includes:
|
||||
* 1. Reports the user created
|
||||
* 2. Reports the user has direct permissions for
|
||||
* 3. Reports the user has team permissions for
|
||||
* 4. Reports shared with the workspace (if workspace sharing is enabled)
|
||||
*/
|
||||
export async function getReportsWithPermissions(
|
||||
input: GetReportsWithPermissionsInput
|
||||
): Promise<PaginatedResponse<ReportWithPermissionItem>> {
|
||||
const { userId, page, page_size, sharedWithMe, onlyMyReports } =
|
||||
GetReportsWithPermissionsInputSchema.parse(input);
|
||||
|
||||
// Get the user's organization ID
|
||||
const userOrg = await getUserOrganizationId(userId);
|
||||
if (!userOrg?.organizationId) {
|
||||
// Return empty result if user has no organization
|
||||
return createPaginatedResponse({
|
||||
data: [],
|
||||
page,
|
||||
page_size,
|
||||
total: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const { organizationId } = userOrg;
|
||||
|
||||
// Build the where conditions based on filters
|
||||
let whereConditions: SQL<unknown> | undefined;
|
||||
|
||||
if (onlyMyReports) {
|
||||
// Only show reports created by the user
|
||||
whereConditions = and(
|
||||
eq(reportFiles.createdBy, userId),
|
||||
isNull(reportFiles.deletedAt),
|
||||
eq(reportFiles.organizationId, organizationId)
|
||||
);
|
||||
} else if (sharedWithMe) {
|
||||
// Only show reports shared with the user (not created by them)
|
||||
whereConditions = and(
|
||||
isNull(reportFiles.deletedAt),
|
||||
eq(reportFiles.organizationId, organizationId),
|
||||
// Not created by the user
|
||||
ne(reportFiles.createdBy, userId),
|
||||
// But user has access through permissions or workspace sharing
|
||||
or(
|
||||
// Direct user permission
|
||||
exists(
|
||||
db
|
||||
.select({ one: assetPermissions.assetId })
|
||||
.from(assetPermissions)
|
||||
.where(
|
||||
and(
|
||||
eq(assetPermissions.assetId, reportFiles.id),
|
||||
eq(assetPermissions.assetType, 'report_file'),
|
||||
eq(assetPermissions.identityType, 'user'),
|
||||
eq(assetPermissions.identityId, userId),
|
||||
isNull(assetPermissions.deletedAt)
|
||||
)
|
||||
)
|
||||
),
|
||||
// Team permission
|
||||
exists(
|
||||
db
|
||||
.select({ one: assetPermissions.assetId })
|
||||
.from(assetPermissions)
|
||||
.innerJoin(
|
||||
teamsToUsers,
|
||||
and(
|
||||
eq(teamsToUsers.teamId, assetPermissions.identityId),
|
||||
eq(teamsToUsers.userId, userId),
|
||||
isNull(teamsToUsers.deletedAt)
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(assetPermissions.assetId, reportFiles.id),
|
||||
eq(assetPermissions.assetType, 'report_file'),
|
||||
eq(assetPermissions.identityType, 'team'),
|
||||
isNull(assetPermissions.deletedAt)
|
||||
)
|
||||
)
|
||||
),
|
||||
// Workspace sharing (not 'none')
|
||||
ne(reportFiles.workspaceSharing, 'none')
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Show all reports the user has access to
|
||||
whereConditions = and(
|
||||
isNull(reportFiles.deletedAt),
|
||||
eq(reportFiles.organizationId, organizationId),
|
||||
or(
|
||||
// Created by the user
|
||||
eq(reportFiles.createdBy, userId),
|
||||
// Direct user permission
|
||||
exists(
|
||||
db
|
||||
.select({ one: assetPermissions.assetId })
|
||||
.from(assetPermissions)
|
||||
.where(
|
||||
and(
|
||||
eq(assetPermissions.assetId, reportFiles.id),
|
||||
eq(assetPermissions.assetType, 'report_file'),
|
||||
eq(assetPermissions.identityType, 'user'),
|
||||
eq(assetPermissions.identityId, userId),
|
||||
isNull(assetPermissions.deletedAt)
|
||||
)
|
||||
)
|
||||
),
|
||||
// Team permission
|
||||
exists(
|
||||
db
|
||||
.select({ one: assetPermissions.assetId })
|
||||
.from(assetPermissions)
|
||||
.innerJoin(
|
||||
teamsToUsers,
|
||||
and(
|
||||
eq(teamsToUsers.teamId, assetPermissions.identityId),
|
||||
eq(teamsToUsers.userId, userId),
|
||||
isNull(teamsToUsers.deletedAt)
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(assetPermissions.assetId, reportFiles.id),
|
||||
eq(assetPermissions.assetType, 'report_file'),
|
||||
eq(assetPermissions.identityType, 'team'),
|
||||
isNull(assetPermissions.deletedAt)
|
||||
)
|
||||
)
|
||||
),
|
||||
// Workspace sharing (not 'none')
|
||||
ne(reportFiles.workspaceSharing, 'none')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Create the query with left join to get user permissions
|
||||
const getData = withPagination(
|
||||
db
|
||||
.select({
|
||||
id: reportFiles.id,
|
||||
name: reportFiles.name,
|
||||
publicly_accessible: reportFiles.publiclyAccessible,
|
||||
created_at: reportFiles.createdAt,
|
||||
updated_at: reportFiles.updatedAt,
|
||||
workspace_sharing: reportFiles.workspaceSharing,
|
||||
created_by_id: reportFiles.createdBy,
|
||||
created_by_name: users.name,
|
||||
created_by_avatar: users.avatarUrl,
|
||||
// Get the user's permission for this report
|
||||
permission: assetPermissions.role,
|
||||
})
|
||||
.from(reportFiles)
|
||||
.innerJoin(users, eq(reportFiles.createdBy, users.id))
|
||||
.leftJoin(
|
||||
assetPermissions,
|
||||
and(
|
||||
eq(assetPermissions.assetId, reportFiles.id),
|
||||
eq(assetPermissions.assetType, 'report_file'),
|
||||
eq(assetPermissions.identityType, 'user'),
|
||||
eq(assetPermissions.identityId, userId),
|
||||
isNull(assetPermissions.deletedAt)
|
||||
)
|
||||
)
|
||||
.where(whereConditions)
|
||||
.$dynamic(),
|
||||
desc(reportFiles.updatedAt), // Most recently updated reports first
|
||||
page,
|
||||
page_size
|
||||
);
|
||||
|
||||
// Create count query
|
||||
const getTotal = db.select({ count: count() }).from(reportFiles).where(whereConditions);
|
||||
|
||||
try {
|
||||
// Execute data and count queries in parallel
|
||||
const [data, totalResult] = await Promise.all([getData, getTotal]);
|
||||
|
||||
const total = totalResult[0]?.count ?? 0;
|
||||
|
||||
// Transform the data to include is_shared flag
|
||||
const transformedData = data.map((report) => ({
|
||||
...report,
|
||||
is_shared: report.created_by_id !== userId,
|
||||
// If no explicit permission but user is creator, they're the owner
|
||||
permission: report.permission || (report.created_by_id === userId ? 'owner' : null),
|
||||
}));
|
||||
|
||||
return createPaginatedResponse({
|
||||
data: transformedData,
|
||||
page,
|
||||
page_size,
|
||||
total,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching reports with permissions:', error);
|
||||
throw new Error('Failed to fetch reports with permissions');
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ export * from './get-reports-list';
|
|||
export * from './get-report';
|
||||
export * from './get-report-metadata';
|
||||
export * from './get-report-content';
|
||||
export * from './get-reports-with-permissions';
|
||||
export * from './update-report';
|
||||
export * from './replace-report-content';
|
||||
export * from './append-report-content';
|
||||
|
|
|
@ -9,8 +9,12 @@ export const ReportListItemSchema = z.object({
|
|||
created_by_id: z.string(),
|
||||
created_by_name: z.string().nullable(),
|
||||
created_by_avatar: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
publicly_accessible: z.boolean(),
|
||||
workspace_sharing: z.string(),
|
||||
is_shared: z.boolean(),
|
||||
permission: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const ReportIndividualResponseSchema = z.object({
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { z } from 'zod';
|
||||
import { PaginatedRequestSchema } from '../type-utilities/pagination';
|
||||
|
||||
export const GetReportsListRequestSchema = PaginatedRequestSchema;
|
||||
export const GetReportsListRequestSchema = PaginatedRequestSchema.extend({
|
||||
shared_with_me: z.coerce.boolean().optional(),
|
||||
only_my_reports: z.coerce.boolean().optional(),
|
||||
});
|
||||
|
||||
// Define UpdateReportRequestSchema with explicit type annotation
|
||||
export const UpdateReportRequestSchema = z
|
||||
|
|
Loading…
Reference in New Issue