mirror of https://github.com/buster-so/buster.git
electric context bug fix with sql helpers.
This commit is contained in:
parent
ce8497acfe
commit
db3a0a7c50
|
@ -4,12 +4,10 @@ import { errorResponse } from '../../../utils/response';
|
||||||
import { extractParamFromWhere } from './_helpers';
|
import { extractParamFromWhere } from './_helpers';
|
||||||
|
|
||||||
export const chatsProxyRouter = async (url: URL, _userId: string, c: Context) => {
|
export const chatsProxyRouter = async (url: URL, _userId: string, c: Context) => {
|
||||||
const matches = extractParamFromWhere(url, 'id');
|
const chatId = extractParamFromWhere(url, 'id');
|
||||||
const chatId = matches?.[0];
|
|
||||||
|
|
||||||
if (!chatId) {
|
if (!chatId) {
|
||||||
errorResponse('Chat ID (id) is required', 403);
|
throw errorResponse('Chat ID (id) is required', 403);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// User must have access to the chat
|
// User must have access to the chat
|
||||||
|
@ -19,8 +17,7 @@ export const chatsProxyRouter = async (url: URL, _userId: string, c: Context) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userHasAccessToChat) {
|
if (!userHasAccessToChat) {
|
||||||
errorResponse('You do not have access to this chat', 403);
|
throw errorResponse('You do not have access to this chat', 403);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
|
|
|
@ -46,8 +46,7 @@ const app = new Hono()
|
||||||
return response;
|
return response;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
console.error('Error fetching data from Electric Shape', _error);
|
console.error('Error fetching data from Electric Shape', _error);
|
||||||
errorResponse('Error fetching data from Electric Shape', 500);
|
throw errorResponse('Error fetching data from Electric Shape', 500);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,10 @@ import { errorResponse } from '../../../utils/response';
|
||||||
import { extractParamFromWhere } from './_helpers';
|
import { extractParamFromWhere } from './_helpers';
|
||||||
|
|
||||||
export const messagesProxyRouter = async (url: URL, _userId: string, c: Context) => {
|
export const messagesProxyRouter = async (url: URL, _userId: string, c: Context) => {
|
||||||
const matches = extractParamFromWhere(url, 'chat_id');
|
const chatId = extractParamFromWhere(url, 'chat_id');
|
||||||
const chatId = matches?.[0];
|
|
||||||
|
|
||||||
if (!chatId) {
|
if (!chatId) {
|
||||||
errorResponse('Chat ID is required', 403);
|
throw errorResponse('Chat ID is required', 403);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userHasAccessToChat = await canUserAccessChat({
|
const userHasAccessToChat = await canUserAccessChat({
|
||||||
|
@ -18,8 +16,7 @@ export const messagesProxyRouter = async (url: URL, _userId: string, c: Context)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userHasAccessToChat) {
|
if (!userHasAccessToChat) {
|
||||||
errorResponse('You do not have access to this chat', 403);
|
throw errorResponse('You do not have access to this chat', 403);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
|
|
|
@ -17,23 +17,16 @@ export async function validateSqlPermissions(
|
||||||
dataSourceSyntax?: string
|
dataSourceSyntax?: string
|
||||||
): Promise<PermissionValidationResult> {
|
): Promise<PermissionValidationResult> {
|
||||||
try {
|
try {
|
||||||
console.info('[validateSqlPermissions] Starting validation for userId:', userId);
|
|
||||||
console.info('[validateSqlPermissions] SQL query:', sql);
|
|
||||||
console.info('[validateSqlPermissions] Data source syntax:', dataSourceSyntax);
|
|
||||||
|
|
||||||
// Extract physical tables from SQL
|
// Extract physical tables from SQL
|
||||||
const tablesInQuery = extractPhysicalTables(sql, dataSourceSyntax);
|
const tablesInQuery = extractPhysicalTables(sql, dataSourceSyntax);
|
||||||
console.info('[validateSqlPermissions] Tables extracted from SQL:', JSON.stringify(tablesInQuery, null, 2));
|
|
||||||
|
|
||||||
if (tablesInQuery.length === 0) {
|
if (tablesInQuery.length === 0) {
|
||||||
// No tables referenced (might be a function call or constant select)
|
// No tables referenced (might be a function call or constant select)
|
||||||
console.info('[validateSqlPermissions] No tables found in query, allowing access');
|
|
||||||
return { isAuthorized: true, unauthorizedTables: [] };
|
return { isAuthorized: true, unauthorizedTables: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's permissioned datasets
|
// Get user's permissioned datasets
|
||||||
const permissionedDatasets = await getPermissionedDatasets(userId, 0, 1000);
|
const permissionedDatasets = await getPermissionedDatasets(userId, 0, 1000);
|
||||||
console.info('[validateSqlPermissions] Found', permissionedDatasets.length, 'permissioned datasets for user');
|
|
||||||
|
|
||||||
// Extract all allowed tables from datasets
|
// Extract all allowed tables from datasets
|
||||||
const allowedTables: ParsedTable[] = [];
|
const allowedTables: ParsedTable[] = [];
|
||||||
|
@ -41,15 +34,10 @@ export async function validateSqlPermissions(
|
||||||
for (const dataset of permissionedDatasets) {
|
for (const dataset of permissionedDatasets) {
|
||||||
if (dataset.ymlFile) {
|
if (dataset.ymlFile) {
|
||||||
const tables = extractTablesFromYml(dataset.ymlFile);
|
const tables = extractTablesFromYml(dataset.ymlFile);
|
||||||
console.info('[validateSqlPermissions] Extracted', tables.length, 'tables from dataset:', dataset.name || 'unnamed');
|
|
||||||
console.info('[validateSqlPermissions] Tables from YML:', JSON.stringify(tables, null, 2));
|
|
||||||
allowedTables.push(...tables);
|
allowedTables.push(...tables);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info('[validateSqlPermissions] Total allowed tables:', allowedTables.length);
|
|
||||||
console.info('[validateSqlPermissions] All allowed tables:', JSON.stringify(allowedTables, null, 2));
|
|
||||||
|
|
||||||
// Check each table in query against permissions
|
// Check each table in query against permissions
|
||||||
const unauthorizedTables: string[] = [];
|
const unauthorizedTables: string[] = [];
|
||||||
|
|
||||||
|
@ -60,8 +48,6 @@ export async function validateSqlPermissions(
|
||||||
for (const allowedTable of allowedTables) {
|
for (const allowedTable of allowedTables) {
|
||||||
const matches = tablesMatch(queryTable, allowedTable);
|
const matches = tablesMatch(queryTable, allowedTable);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
console.info('[validateSqlPermissions] Table match found:',
|
|
||||||
`Query: ${JSON.stringify(queryTable)} matches Allowed: ${JSON.stringify(allowedTable)}`);
|
|
||||||
isAuthorized = true;
|
isAuthorized = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -78,11 +64,9 @@ export async function validateSqlPermissions(
|
||||||
unauthorizedTables
|
unauthorizedTables
|
||||||
};
|
};
|
||||||
|
|
||||||
console.info('[validateSqlPermissions] Final result:', JSON.stringify(result, null, 2));
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[validateSqlPermissions] Error during validation:', error);
|
|
||||||
return {
|
return {
|
||||||
isAuthorized: false,
|
isAuthorized: false,
|
||||||
unauthorizedTables: [],
|
unauthorizedTables: [],
|
||||||
|
|
|
@ -34,15 +34,11 @@ const DIALECT_MAPPING: Record<string, string> = {
|
||||||
|
|
||||||
function getParserDialect(dataSourceSyntax?: string): string {
|
function getParserDialect(dataSourceSyntax?: string): string {
|
||||||
if (!dataSourceSyntax) {
|
if (!dataSourceSyntax) {
|
||||||
console.warn('[getParserDialect] No data source syntax provided, defaulting to postgresql');
|
|
||||||
return 'postgresql';
|
return 'postgresql';
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialect = DIALECT_MAPPING[dataSourceSyntax.toLowerCase()];
|
const dialect = DIALECT_MAPPING[dataSourceSyntax.toLowerCase()];
|
||||||
if (!dialect) {
|
if (!dialect) {
|
||||||
console.warn(
|
|
||||||
`[getParserDialect] Unknown data source syntax: ${dataSourceSyntax}, defaulting to postgresql`
|
|
||||||
);
|
|
||||||
return 'postgresql';
|
return 'postgresql';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,19 +51,14 @@ function getParserDialect(dataSourceSyntax?: string): string {
|
||||||
*/
|
*/
|
||||||
export function extractPhysicalTables(sql: string, dataSourceSyntax?: string): ParsedTable[] {
|
export function extractPhysicalTables(sql: string, dataSourceSyntax?: string): ParsedTable[] {
|
||||||
const dialect = getParserDialect(dataSourceSyntax);
|
const dialect = getParserDialect(dataSourceSyntax);
|
||||||
console.info('[extractPhysicalTables] Using dialect:', dialect, 'for syntax:', dataSourceSyntax);
|
|
||||||
|
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.info('[extractPhysicalTables] Parsing SQL:', sql);
|
|
||||||
|
|
||||||
// Parse SQL into AST with the appropriate dialect
|
// Parse SQL into AST with the appropriate dialect
|
||||||
const ast = parser.astify(sql, { database: dialect });
|
const ast = parser.astify(sql, { database: dialect });
|
||||||
|
|
||||||
// Get all table references from parser with the appropriate dialect
|
// Get all table references from parser with the appropriate dialect
|
||||||
const allTables = parser.tableList(sql, { database: dialect });
|
const allTables = parser.tableList(sql, { database: dialect });
|
||||||
console.info('[extractPhysicalTables] Raw table list from parser:', allTables);
|
|
||||||
|
|
||||||
// Extract CTE names to exclude them
|
// Extract CTE names to exclude them
|
||||||
const cteNames = new Set<string>();
|
const cteNames = new Set<string>();
|
||||||
|
@ -91,20 +82,16 @@ export function extractPhysicalTables(sql: string, dataSourceSyntax?: string): P
|
||||||
const processedTables = new Set<string>();
|
const processedTables = new Set<string>();
|
||||||
|
|
||||||
for (const tableRef of allTables) {
|
for (const tableRef of allTables) {
|
||||||
console.info('[extractPhysicalTables] Processing table reference:', tableRef);
|
|
||||||
const parsed = parseTableReference(tableRef);
|
const parsed = parseTableReference(tableRef);
|
||||||
console.info('[extractPhysicalTables] Parsed table:', JSON.stringify(parsed));
|
|
||||||
|
|
||||||
// Skip if it's a CTE
|
// Skip if it's a CTE
|
||||||
if (cteNames.has(parsed.table.toLowerCase())) {
|
if (cteNames.has(parsed.table.toLowerCase())) {
|
||||||
console.info('[extractPhysicalTables] Skipping CTE:', parsed.table);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip duplicates
|
// Skip duplicates
|
||||||
const tableKey = `${parsed.database || ''}.${parsed.schema || ''}.${parsed.table}`;
|
const tableKey = `${parsed.database || ''}.${parsed.schema || ''}.${parsed.table}`;
|
||||||
if (processedTables.has(tableKey)) {
|
if (processedTables.has(tableKey)) {
|
||||||
console.info('[extractPhysicalTables] Skipping duplicate:', tableKey);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,13 +99,8 @@ export function extractPhysicalTables(sql: string, dataSourceSyntax?: string): P
|
||||||
physicalTables.push(parsed);
|
physicalTables.push(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info(
|
|
||||||
'[extractPhysicalTables] Final physical tables:',
|
|
||||||
JSON.stringify(physicalTables, null, 2)
|
|
||||||
);
|
|
||||||
return physicalTables;
|
return physicalTables;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[extractPhysicalTables] Error parsing SQL:', error);
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to parse SQL: ${error instanceof Error ? error.message : 'Unknown error'}`
|
`Failed to parse SQL: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
);
|
);
|
||||||
|
@ -230,30 +212,14 @@ export function normalizeTableIdentifier(identifier: ParsedTable): string {
|
||||||
* For example, "schema.table" matches "database.schema.table" if schema and table match
|
* For example, "schema.table" matches "database.schema.table" if schema and table match
|
||||||
*/
|
*/
|
||||||
export function tablesMatch(queryTable: ParsedTable, permissionTable: ParsedTable): boolean {
|
export function tablesMatch(queryTable: ParsedTable, permissionTable: ParsedTable): boolean {
|
||||||
console.info('[tablesMatch] Comparing tables:');
|
|
||||||
console.info('[tablesMatch] Query table:', JSON.stringify(queryTable));
|
|
||||||
console.info('[tablesMatch] Permission table:', JSON.stringify(permissionTable));
|
|
||||||
|
|
||||||
// Exact table name must match
|
// Exact table name must match
|
||||||
if (queryTable.table.toLowerCase() !== permissionTable.table.toLowerCase()) {
|
if (queryTable.table.toLowerCase() !== permissionTable.table.toLowerCase()) {
|
||||||
console.info(
|
|
||||||
'[tablesMatch] Table names do not match:',
|
|
||||||
queryTable.table,
|
|
||||||
'vs',
|
|
||||||
permissionTable.table
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If permission specifies schema, query must match
|
// If permission specifies schema, query must match
|
||||||
if (permissionTable.schema && queryTable.schema) {
|
if (permissionTable.schema && queryTable.schema) {
|
||||||
if (permissionTable.schema.toLowerCase() !== queryTable.schema.toLowerCase()) {
|
if (permissionTable.schema.toLowerCase() !== queryTable.schema.toLowerCase()) {
|
||||||
console.info(
|
|
||||||
'[tablesMatch] Schemas do not match:',
|
|
||||||
queryTable.schema,
|
|
||||||
'vs',
|
|
||||||
permissionTable.schema
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -261,12 +227,6 @@ export function tablesMatch(queryTable: ParsedTable, permissionTable: ParsedTabl
|
||||||
// If permission specifies database, query must match
|
// If permission specifies database, query must match
|
||||||
if (permissionTable.database && queryTable.database) {
|
if (permissionTable.database && queryTable.database) {
|
||||||
if (permissionTable.database.toLowerCase() !== queryTable.database.toLowerCase()) {
|
if (permissionTable.database.toLowerCase() !== queryTable.database.toLowerCase()) {
|
||||||
console.info(
|
|
||||||
'[tablesMatch] Databases do not match:',
|
|
||||||
queryTable.database,
|
|
||||||
'vs',
|
|
||||||
permissionTable.database
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,11 +234,9 @@ export function tablesMatch(queryTable: ParsedTable, permissionTable: ParsedTabl
|
||||||
// If permission has schema but query doesn't, it's not a match
|
// If permission has schema but query doesn't, it's not a match
|
||||||
// (we require explicit schema matching for security)
|
// (we require explicit schema matching for security)
|
||||||
if (permissionTable.schema && !queryTable.schema) {
|
if (permissionTable.schema && !queryTable.schema) {
|
||||||
console.info('[tablesMatch] Permission requires schema but query has none');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info('[tablesMatch] Tables match!');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,20 +257,12 @@ export function extractTablesFromYml(ymlContent: string): ParsedTable[] {
|
||||||
const tables: ParsedTable[] = [];
|
const tables: ParsedTable[] = [];
|
||||||
const processedTables = new Set<string>();
|
const processedTables = new Set<string>();
|
||||||
|
|
||||||
console.info('[extractTablesFromYml] Starting YML extraction');
|
|
||||||
console.info('[extractTablesFromYml] YML content:', `${ymlContent.substring(0, 200)}...`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse YML content
|
// Parse YML content
|
||||||
const parsed = yaml.parse(ymlContent);
|
const parsed = yaml.parse(ymlContent);
|
||||||
console.info(
|
|
||||||
'[extractTablesFromYml] Parsed YML structure:',
|
|
||||||
`${JSON.stringify(parsed, null, 2).substring(0, 500)}...`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check for flat format (top-level name, schema, database)
|
// Check for flat format (top-level name, schema, database)
|
||||||
if (parsed?.name && !parsed?.models && (parsed?.schema || parsed?.database)) {
|
if (parsed?.name && !parsed?.models && (parsed?.schema || parsed?.database)) {
|
||||||
console.info('[extractTablesFromYml] Found flat format dataset');
|
|
||||||
const parsedTable: ParsedTable = {
|
const parsedTable: ParsedTable = {
|
||||||
table: parsed.name,
|
table: parsed.name,
|
||||||
fullName: parsed.name,
|
fullName: parsed.name,
|
||||||
|
@ -334,7 +284,6 @@ export function extractTablesFromYml(ymlContent: string): ParsedTable[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info('[extractTablesFromYml] Flat format table:', JSON.stringify(parsedTable));
|
|
||||||
const key = normalizeTableIdentifier(parsedTable);
|
const key = normalizeTableIdentifier(parsedTable);
|
||||||
if (!processedTables.has(key)) {
|
if (!processedTables.has(key)) {
|
||||||
processedTables.add(key);
|
processedTables.add(key);
|
||||||
|
@ -344,13 +293,7 @@ export function extractTablesFromYml(ymlContent: string): ParsedTable[] {
|
||||||
|
|
||||||
// Look for models array
|
// Look for models array
|
||||||
if (parsed?.models && Array.isArray(parsed.models)) {
|
if (parsed?.models && Array.isArray(parsed.models)) {
|
||||||
console.info(
|
|
||||||
'[extractTablesFromYml] Found models array with',
|
|
||||||
parsed.models.length,
|
|
||||||
'models'
|
|
||||||
);
|
|
||||||
for (const model of parsed.models) {
|
for (const model of parsed.models) {
|
||||||
console.info('[extractTablesFromYml] Processing model:', JSON.stringify(model));
|
|
||||||
// Process models that have name and at least schema or database
|
// Process models that have name and at least schema or database
|
||||||
if (model.name && (model.schema || model.database)) {
|
if (model.name && (model.schema || model.database)) {
|
||||||
const parsedTable: ParsedTable = {
|
const parsedTable: ParsedTable = {
|
||||||
|
@ -374,26 +317,17 @@ export function extractTablesFromYml(ymlContent: string): ParsedTable[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info('[extractTablesFromYml] Parsed model table:', JSON.stringify(parsedTable));
|
|
||||||
const key = normalizeTableIdentifier(parsedTable);
|
const key = normalizeTableIdentifier(parsedTable);
|
||||||
if (!processedTables.has(key)) {
|
if (!processedTables.has(key)) {
|
||||||
processedTables.add(key);
|
processedTables.add(key);
|
||||||
tables.push(parsedTable);
|
tables.push(parsedTable);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
'[extractTablesFromYml] Skipping model without schema/database:',
|
|
||||||
JSON.stringify(model)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
// If YML parsing fails, return empty array
|
// If YML parsing fails, return empty array
|
||||||
console.error('[extractTablesFromYml] Failed to parse YML:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info('[extractTablesFromYml] Total tables extracted:', tables.length);
|
|
||||||
console.info('[extractTablesFromYml] Extracted tables:', JSON.stringify(tables, null, 2));
|
|
||||||
return tables;
|
return tables;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue