@@ -29,27 +29,100 @@ export async function isDatabricksReadOnlySession(adapter: DatabricksAdapter): P
2929 const catalogResult = await adapter . raw ( 'SELECT current_catalog() as catalog' ) ;
3030 const currentCatalog = catalogResult . rows [ 0 ] ?. catalog ;
3131
32- // Check if user can create tables (write permission indicator)
32+ // Define write permissions that we want to check for
33+ // Note: EXECUTE is excluded as it only allows running functions/procedures, not direct data modification
34+ const writePermissions = [ 'CREATE' , 'MODIFY' , 'DELETE' , 'DROP' , 'ALTER' , 'UPDATE' , 'INSERT' , 'WRITE_FILES' , 'REFRESH' ] ;
35+
36+ // Collect all grants to check
37+ const allGrants : any [ ] = [ ] ;
38+
3339 try {
34- // For Unity Catalog, we need to specify the catalog name
40+ // Check catalog-level permissions
3541 if ( currentCatalog ) {
36- await adapter . raw ( `SHOW GRANTS ON CATALOG ${ currentCatalog } ` ) ;
37- } else {
38- // If no catalog is set, try to check schema permissions
39- const schemaResult = await adapter . raw ( 'SELECT current_schema() as schema' ) ;
40- const currentSchema = schemaResult . rows [ 0 ] ?. schema ;
41- if ( currentSchema ) {
42- await adapter . raw ( `SHOW GRANTS ON SCHEMA ${ currentSchema } ` ) ;
42+ const catalogGrants = await adapter . raw ( `SHOW GRANTS ON CATALOG ${ currentCatalog } ` ) ;
43+ allGrants . push ( ...( catalogGrants . rows || [ ] ) ) ;
44+ }
45+
46+ // Check schema-level permissions
47+ const schemaResult = await adapter . raw ( 'SELECT current_schema() as schema' ) ;
48+ const currentSchema = schemaResult . rows [ 0 ] ?. schema ;
49+ if ( currentSchema && currentCatalog ) {
50+ try {
51+ const schemaGrants = await adapter . raw ( `SHOW GRANTS ON SCHEMA ${ currentCatalog } .${ currentSchema } ` ) ;
52+ allGrants . push ( ...( schemaGrants . rows || [ ] ) ) ;
53+ } catch ( error ) {
54+ // Schema grants might fail if no specific schema permissions
55+ console . debug ( 'Could not fetch schema grants:' , error ) ;
4356 }
4457 }
4558
46- // If we can show grants, parse them to check for write permissions
47- // In Databricks, write permissions include: CREATE, MODIFY, DELETE, etc.
48- // For now, we'll do a simple check
49- return false ; // Assume not read-only if we can see grants
50- } catch ( error ) {
51- // If we can't even show grants, assume read-only
59+ // Note: Databricks doesn't support "SHOW GRANTS TO user" syntax
60+ // We can only show grants ON specific objects, not grants TO a user
61+ // The catalog and schema level grants above should capture user permissions
62+
63+ // Parse grants to check for write permissions
64+ let hasExecutePermission = false ;
65+ for ( const grant of allGrants ) {
66+ // The grant row might have different column names depending on Databricks version
67+ // Common formats: action_type, ActionType, privilege, Privilege
68+ const action = grant . action_type || grant . ActionType || grant . privilege || grant . Privilege || '' ;
69+ const upperAction = action . toUpperCase ( ) ;
70+
71+ // Check for EXECUTE permission
72+ if ( upperAction . includes ( 'EXECUTE' ) ) {
73+ hasExecutePermission = true ;
74+ }
75+
76+ // Check if any write permission exists
77+ if ( writePermissions . some ( perm => upperAction . includes ( perm ) ) ) {
78+ console . log ( `Found write permission: ${ action } for user ${ currentUser } ` ) ;
79+ return false ; // Has write permissions, not read-only
80+ }
81+ }
82+
83+ // Warn if EXECUTE permission is found
84+ if ( hasExecutePermission ) {
85+ console . warn ( `⚠️ Warning: User ${ currentUser } has EXECUTE permission. While this connection is allowed, ` +
86+ 'be aware that EXECUTE permission could potentially run functions that modify data. ' +
87+ 'Ensure that only read-only functions are available in this environment.' ) ;
88+ }
89+
90+ // If we only found SELECT or no permissions, it's read-only
5291 return true ;
92+
93+ } catch ( error ) {
94+ // If we can't fetch grants, we need to test actual operations
95+ console . debug ( 'Could not fetch grants, testing with actual operations:' , error ) ;
96+
97+ // Try to perform a write operation that should fail if read-only
98+ try {
99+ // Try to create a temporary view (least invasive write operation)
100+ const testViewName = `_mcp_permission_test_${ Date . now ( ) } ` ;
101+ await adapter . raw ( `CREATE OR REPLACE TEMPORARY VIEW ${ testViewName } AS SELECT 1 as test` ) ;
102+
103+ // If we got here, we have write permissions - clean up
104+ try {
105+ await adapter . raw ( `DROP VIEW IF EXISTS ${ testViewName } ` ) ;
106+ } catch ( cleanupError ) {
107+ // Ignore cleanup errors
108+ }
109+
110+ return false ; // Has write permissions
111+ } catch ( writeError : any ) {
112+ // Check if error is due to lack of permissions
113+ const errorMessage = writeError . message || writeError . toString ( ) ;
114+ if ( errorMessage . includes ( 'PERMISSION_DENIED' ) ||
115+ errorMessage . includes ( 'does not have privilege' ) ||
116+ errorMessage . includes ( 'insufficient privileges' ) ||
117+ errorMessage . includes ( 'CREATE' ) ||
118+ errorMessage . includes ( 'denied' ) ) {
119+ return true ; // Confirmed read-only
120+ }
121+
122+ // Unknown error, assume read-only for safety
123+ console . warn ( 'Unknown error during permission test:' , writeError ) ;
124+ return true ;
125+ }
53126 }
54127 } catch ( error ) {
55128 console . error ( 'Error checking Databricks permissions:' , error ) ;
@@ -72,19 +145,63 @@ export async function checkDatabricksPermissions(adapter: DatabricksAdapter): Pr
72145 const currentCatalog = catalogResult . rows [ 0 ] ?. catalog || 'unknown' ;
73146 const currentSchema = schemaResult . rows [ 0 ] ?. schema || 'unknown' ;
74147
75- // Try to gather permissions
76- const permissions : string [ ] = [ ] ;
148+ // Collect all unique permissions
149+ const permissionsSet = new Set < string > ( ) ;
77150
78151 // Check basic SELECT permission
79152 try {
80153 await adapter . raw ( 'SELECT 1' ) ;
81- permissions . push ( 'SELECT' ) ;
154+ permissionsSet . add ( 'SELECT' ) ;
82155 } catch ( error ) {
83156 // No SELECT permission
84157 }
85158
86- // For now, we'll assume read-only if we only have SELECT
87- const isStrictlyReadOnly = permissions . length === 1 && permissions [ 0 ] === 'SELECT' ;
159+ // Collect grants from various levels
160+ try {
161+ // Catalog grants
162+ if ( currentCatalog && currentCatalog !== 'unknown' ) {
163+ const catalogGrants = await adapter . raw ( `SHOW GRANTS ON CATALOG ${ currentCatalog } ` ) ;
164+ for ( const grant of catalogGrants . rows || [ ] ) {
165+ const action = grant . action_type || grant . ActionType || grant . privilege || grant . Privilege || '' ;
166+ if ( action ) {
167+ permissionsSet . add ( action . toUpperCase ( ) ) ;
168+ }
169+ }
170+ }
171+
172+ // Schema grants
173+ if ( currentSchema && currentSchema !== 'unknown' && currentCatalog && currentCatalog !== 'unknown' ) {
174+ try {
175+ const schemaGrants = await adapter . raw ( `SHOW GRANTS ON SCHEMA ${ currentCatalog } .${ currentSchema } ` ) ;
176+ for ( const grant of schemaGrants . rows || [ ] ) {
177+ const action = grant . action_type || grant . ActionType || grant . privilege || grant . Privilege || '' ;
178+ if ( action ) {
179+ permissionsSet . add ( action . toUpperCase ( ) ) ;
180+ }
181+ }
182+ } catch ( error ) {
183+ // Schema grants might not be available
184+ }
185+ }
186+
187+ // Note: Databricks doesn't support "SHOW GRANTS TO user" syntax
188+ // The catalog and schema level grants above should capture user permissions
189+ } catch ( error ) {
190+ console . debug ( 'Could not fetch all grants:' , error ) ;
191+ }
192+
193+ const permissions = Array . from ( permissionsSet ) . sort ( ) ;
194+
195+ // Define write permissions
196+ // Note: EXECUTE is excluded as it only allows running functions/procedures, not direct data modification
197+ const writePermissions = [ 'CREATE' , 'MODIFY' , 'DELETE' , 'DROP' , 'ALTER' , 'UPDATE' , 'INSERT' , 'WRITE_FILES' , 'REFRESH' ] ;
198+
199+ // Check if strictly read-only (only SELECT or no permissions)
200+ const hasWritePermissions = permissions . some ( perm =>
201+ writePermissions . some ( writePerm => perm . includes ( writePerm ) )
202+ ) ;
203+
204+ const isStrictlyReadOnly = ! hasWritePermissions ;
88205
89206 return {
90207 isStrictlyReadOnly,
0 commit comments