Merge pull request #1185 from buster-so/wells-bus-1965-update-frontend-to-use-password-for-public-access-when-it-is

Wells bus 1965 update frontend to use password for public access when it is
This commit is contained in:
wellsbunk5 2025-09-26 13:43:12 -06:00 committed by GitHub
commit c105a94738
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 84 additions and 21 deletions

View File

@ -19,6 +19,7 @@ import { HTTPException } from 'hono/http-exception';
import yaml from 'js-yaml';
import { getPubliclyEnabledByUser } from '../../../../shared-helpers/get-publicly-enabled-by-user';
import { getMetricsInAncestorAssetFromMetricIds } from '../../../../shared-helpers/metric-helpers';
import { throwUnauthorizedError } from '../../../../shared-helpers/asset-public-access';
interface GetDashboardHandlerParams {
dashboardId: string;
@ -130,8 +131,11 @@ export async function getDashboardHandler(
if (!hasAccess || !effectiveRole) {
// This should never be hit because we have already thrown errors for no public access
console.warn(`Permission denied for user ${user.id} to dashboard ${dashboardId}`);
throw new HTTPException(403, {
message: "You don't have permission to view this dashboard",
throwUnauthorizedError({
publiclyAccessible: dashboardFile.publiclyAccessible ?? false,
publicExpiryDate: dashboardFile.publicExpiryDate ?? undefined,
publicPassword: dashboardFile.publicPassword ?? undefined,
userSuppliedPassword: password,
});
}

View File

@ -10,6 +10,7 @@ import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import { getMetricsInAncestorAssetFromMetricIds } from '../../../../shared-helpers/metric-helpers';
import { standardErrorHandler } from '../../../../utils/response';
import { throwUnauthorizedError } from '../../../../shared-helpers/asset-public-access';
export async function getReportHandler(
reportId: string,
@ -40,7 +41,12 @@ export async function getReportHandler(
});
if (!permission.hasAccess || !permission.effectiveRole) {
throw new HTTPException(403, { message: 'You do not have permission to view this report' });
throwUnauthorizedError({
publiclyAccessible: report.publicly_accessible ?? false,
publicExpiryDate: report.public_expiry_date ?? undefined,
publicPassword: report.public_password ?? undefined,
userSuppliedPassword: password,
});
}
const metrics = await getMetricsInAncestorAssetFromMetricIds(metricIds, user);

View File

@ -6,6 +6,7 @@ import {
import { getUserOrganizationId } from '@buster/database/queries';
import type { AssetType } from '@buster/server-shared/assets';
import { HTTPException } from 'hono/http-exception';
import z from 'zod';
export const checkIfAssetIsEditable = async ({
user,
@ -48,3 +49,48 @@ export const checkIfAssetIsEditable = async ({
throw new HTTPException(403, { message: 'You do not have permission to edit this asset' });
}
};
export const ThrowUnauthorizedErrorSchema = z.object({
publiclyAccessible: z.boolean(),
publicExpiryDate: z.string().optional(),
publicPassword: z.string().optional(),
userSuppliedPassword: z.string().optional(),
});
export type ThrowUnauthorizedErrorParams = z.infer<typeof ThrowUnauthorizedErrorSchema>;
// Decides the appropriate error to throw based on the public access settings
export function throwUnauthorizedError(params: ThrowUnauthorizedErrorParams): never {
const { publiclyAccessible, publicExpiryDate, publicPassword, userSuppliedPassword } = params;
if (publiclyAccessible) {
if (publicExpiryDate) {
try {
if (new Date(publicExpiryDate) < new Date()) {
throw new HTTPException(403, {
message: 'Public access has expired',
});
}
} catch {
throw new HTTPException(403, {
message: 'Public access expired',
});
}
}
if (publicPassword) {
if (!userSuppliedPassword) {
throw new HTTPException(418, {
message: 'Password required for public access',
});
}
if (userSuppliedPassword !== publicPassword) {
throw new HTTPException(403, {
message: 'Incorrect password for public access',
});
}
}
}
throw new HTTPException(403, {
message: 'You do not have permission to access this asset',
});
}

View File

@ -308,7 +308,7 @@ describe('metric-helpers', () => {
const options: MetricAccessOptions = { publicAccessPreviouslyVerified: false };
await expect(fetchAndProcessMetricData('metric-123', mockUser, options)).rejects.toThrow(
new HTTPException(403, { message: "You don't have permission to view this metric" })
new HTTPException(403, { message: 'You do not have permission to access this asset' })
);
});
@ -345,7 +345,7 @@ describe('metric-helpers', () => {
const options: MetricAccessOptions = { publicAccessPreviouslyVerified: false };
await expect(fetchAndProcessMetricData('metric-123', mockUser, options)).rejects.toThrow(
new HTTPException(403, { message: "You don't have permission to view this metric" })
new HTTPException(403, { message: 'Public access expired' })
);
});
@ -363,7 +363,7 @@ describe('metric-helpers', () => {
const options: MetricAccessOptions = { publicAccessPreviouslyVerified: false };
await expect(fetchAndProcessMetricData('metric-123', mockUser, options)).rejects.toThrow(
new HTTPException(403, { message: "You don't have permission to view this metric" })
new HTTPException(418, { message: 'Password required for public access' })
);
});
@ -384,7 +384,7 @@ describe('metric-helpers', () => {
};
await expect(fetchAndProcessMetricData('metric-123', mockUser, options)).rejects.toThrow(
new HTTPException(403, { message: "You don't have permission to view this metric" })
new HTTPException(403, { message: 'Incorrect password for public access' })
);
});

View File

@ -20,6 +20,7 @@ import { HTTPException } from 'hono/http-exception';
import yaml from 'js-yaml';
import { z } from 'zod';
import { getPubliclyEnabledByUser } from './get-publicly-enabled-by-user';
import { throwUnauthorizedError } from './asset-public-access';
export const MetricAccessOptionsSchema = z.object({
/** If public access has been verified by a parent resource set to true */
@ -86,8 +87,11 @@ export async function fetchAndProcessMetricData(
effectiveRole = permissionResult.effectiveRole ? permissionResult.effectiveRole : effectiveRole;
if (!permissionResult.hasAccess || !effectiveRole) {
throw new HTTPException(403, {
message: "You don't have permission to view this metric",
throwUnauthorizedError({
publiclyAccessible: metricFile.publiclyAccessible ?? false,
publicExpiryDate: metricFile.publicExpiryDate ?? undefined,
publicPassword: metricFile.publicPassword ?? undefined,
userSuppliedPassword: password,
});
}

View File

@ -94,4 +94,4 @@ export const InputTextArea = React.forwardRef<InputTextAreaRef, InputTextAreaPro
}
);
InputTextArea.displayName = 'InputTextArea';
InputTextArea.displayName = 'InputTextArea';

View File

@ -33,8 +33,12 @@ export function getPermissionCacheKey(
userId: string,
assetId: string,
assetType: AssetType,
requiredRole: AssetPermissionRole
requiredRole: AssetPermissionRole,
password?: string
): CacheKey {
if (password) {
return `${userId}:${assetId}:${assetType}:${requiredRole}:${password}`;
}
return `${userId}:${assetId}:${assetType}:${requiredRole}`;
}
@ -45,9 +49,10 @@ export function getCachedPermission(
userId: string,
assetId: string,
assetType: AssetType,
requiredRole: AssetPermissionRole
requiredRole: AssetPermissionRole,
password?: string
): AssetPermissionResult | undefined {
const key = getPermissionCacheKey(userId, assetId, assetType, requiredRole);
const key = getPermissionCacheKey(userId, assetId, assetType, requiredRole, password);
const cached = permissionCache.get(key);
if (cached !== undefined) {
@ -67,9 +72,10 @@ export function setCachedPermission(
assetId: string,
assetType: AssetType,
requiredRole: AssetPermissionRole,
result: AssetPermissionResult
result: AssetPermissionResult,
password?: string
): void {
const key = getPermissionCacheKey(userId, assetId, assetType, requiredRole);
const key = getPermissionCacheKey(userId, assetId, assetType, requiredRole, password);
permissionCache.set(key, result);
}

View File

@ -48,7 +48,7 @@ export async function checkPermission(check: AssetPermissionCheck): Promise<Asse
} = check;
// Check cache first (only for single role checks)
const cached = getCachedPermission(userId, assetId, assetType, requiredRole);
const cached = getCachedPermission(userId, assetId, assetType, requiredRole, userSuppliedPassword);
if (cached !== undefined) {
return cached;
}
@ -81,10 +81,7 @@ export async function checkPermission(check: AssetPermissionCheck): Promise<Asse
if (dbResult.accessPath !== undefined) {
result.accessPath = dbResult.accessPath;
}
// Only cache single role checks
if (!Array.isArray(requiredRole)) {
setCachedPermission(userId, assetId, assetType, requiredRole, result);
}
setCachedPermission(userId, assetId, assetType, requiredRole, result);
return result;
}
}
@ -127,7 +124,7 @@ export async function checkPermission(check: AssetPermissionCheck): Promise<Asse
effectiveRole: accessRole,
accessPath: 'public' as const,
};
setCachedPermission(userId, assetId, assetType, requiredRole, result);
setCachedPermission(userId, assetId, assetType, requiredRole, result, userSuppliedPassword);
return result;
}
}