diff --git a/apps/server/src/api/v2/metric_files/get-metric-data.ts b/apps/server/src/api/v2/metric_files/get-metric-data.ts index db8961a4c..7e9b49907 100644 --- a/apps/server/src/api/v2/metric_files/get-metric-data.ts +++ b/apps/server/src/api/v2/metric_files/get-metric-data.ts @@ -1,4 +1,4 @@ -import { type AssetPermissionCheck, checkPermission } from '@buster/access-controls'; +import { hasAssetPermission } from '@buster/access-controls'; import { executeMetricQuery, getCachedMetricData, setCachedMetricData } from '@buster/data-source'; import type { Credentials } from '@buster/data-source'; import type { User } from '@buster/database'; @@ -49,18 +49,38 @@ export async function getMetricDataHandler( const { organizationId } = userOrg; + // Retrieve metric definition from database with data source info + const metric = await getMetricWithDataSource({ metricId, versionNumber }); + + if (!metric) { + throw new HTTPException(404, { + message: 'Metric not found', + }); + } + + // Verify metric belongs to user's organization + if (metric.organizationId !== organizationId) { + throw new HTTPException(403, { + message: 'You do not have permission to view this metric', + }); + } + // Check if user has permission to view this metric file - const permissionCheck: AssetPermissionCheck = { + // This follows the same pattern as report-files.ts - hasAssetPermission handles all the logic including: + // - Direct permissions + // - Workspace sharing permissions + // - Cascading permissions (dashboard, chat, collection) + // - Admin permissions + const hasAccess = await hasAssetPermission({ userId: user.id, assetId: metricId, assetType: 'metric_file', requiredRole: 'can_view', organizationId, - }; + workspaceSharing: metric.workspaceSharing ?? 'none', + }); - const permissionResult = await checkPermission(permissionCheck); - - if (!permissionResult.hasAccess) { + if (!hasAccess) { throw new HTTPException(403, { message: 'You do not have permission to view this metric', }); @@ -100,22 +120,6 @@ export async function getMetricDataHandler( // Ensure limit is within bounds const queryLimit = Math.min(Math.max(limit, 1), 5000); - // Retrieve metric definition from database with data source info - const metric = await getMetricWithDataSource({ metricId, versionNumber }); - - if (!metric) { - throw new HTTPException(404, { - message: 'Metric not found', - }); - } - - // Verify metric belongs to user's organization - if (metric.organizationId !== organizationId) { - throw new HTTPException(403, { - message: 'You do not have permission to view this metric', - }); - } - // Extract SQL query from metric content const sql = extractSqlFromMetricContent(metric.content); diff --git a/packages/access-controls/src/index.ts b/packages/access-controls/src/index.ts index 5ea561313..221476e1c 100644 --- a/packages/access-controls/src/index.ts +++ b/packages/access-controls/src/index.ts @@ -12,10 +12,14 @@ export { listPermissions, // From checks.ts checkPermission, + computeEffectivePermission, type AssetPermissionCheck, type AssetPermissionResult, // From cascading-permissions.ts checkCascadingPermissions, + checkMetricDashboardAccess, + checkMetricChatAccess, + checkMetricCollectionAccess, } from './assets'; // Export dataset permissions diff --git a/packages/database/src/queries/metrics/get-metric-with-data-source.ts b/packages/database/src/queries/metrics/get-metric-with-data-source.ts index 0417685f3..02190fd6f 100644 --- a/packages/database/src/queries/metrics/get-metric-with-data-source.ts +++ b/packages/database/src/queries/metrics/get-metric-with-data-source.ts @@ -43,6 +43,13 @@ export const MetricWithDataSourceSchema = z.object({ secretId: z.string(), dataSourceType: z.string(), versionNumber: z.number().optional(), + workspaceSharing: z.enum(['none', 'can_view', 'can_edit', 'full_access']).nullable(), + workspaceSharingEnabledBy: z.string().nullable(), + workspaceSharingEnabledAt: z.string().nullable(), + publiclyAccessible: z.boolean(), + publiclyEnabledBy: z.string().nullable(), + publicExpiryDate: z.string().nullable(), + publicPassword: z.string().nullable(), }); export type MetricWithDataSource = z.infer; @@ -68,6 +75,13 @@ export async function getMetricWithDataSource( versionHistory: metricFiles.versionHistory, secretId: dataSources.secretId, dataSourceType: dataSources.type, + workspaceSharing: metricFiles.workspaceSharing, + workspaceSharingEnabledBy: metricFiles.workspaceSharingEnabledBy, + workspaceSharingEnabledAt: metricFiles.workspaceSharingEnabledAt, + publiclyAccessible: metricFiles.publiclyAccessible, + publiclyEnabledBy: metricFiles.publiclyEnabledBy, + publicExpiryDate: metricFiles.publicExpiryDate, + publicPassword: metricFiles.publicPassword, }) .from(metricFiles) .innerJoin(dataSources, eq(metricFiles.dataSourceId, dataSources.id)) @@ -134,6 +148,13 @@ export async function getMetricWithDataSource( versionHistory, secretId: result.secretId, dataSourceType: result.dataSourceType, + workspaceSharing: result.workspaceSharing, + workspaceSharingEnabledBy: result.workspaceSharingEnabledBy, + workspaceSharingEnabledAt: result.workspaceSharingEnabledAt, + publiclyAccessible: result.publiclyAccessible, + publiclyEnabledBy: result.publiclyEnabledBy, + publicExpiryDate: result.publicExpiryDate, + publicPassword: result.publicPassword, ...(versionNumber !== undefined && { versionNumber }), };